MySQL vs PostgreSQL en un VPS de 1GB RAM: qué es realmente usable (y las configuraciones que lo hacen)

¿Te fue útil?

Alquilas un VPS pequeño, instalas una base de datos “de verdad” y cinco minutos después la máquina está intercambiando memoria como si pagara por página. La aplicación se siente bien en desarrollo y luego en producción todo se vuelve un desastre a cámara lenta: las consultas se detienen, el load average sube y tu monitor de disponibilidad empieza a escribir poesía.

Con 1GB de RAM no puedes permitirte romanticismos con bases de datos. Debes ser específico. Esta es una guía práctica y opinada para ejecutar MySQL (InnoDB) o PostgreSQL en un VPS de 1GB sin autolesionarte: las cuentas de memoria, las configuraciones que importan y las comprobaciones que te dicen qué está pasando realmente.

La única pregunta que importa en 1GB: ¿cuál es tu cuello de botella?

En un VPS de 1GB siempre estás pagando algún tipo de impuesto: presión de memoria, latencia de disco, CPU steal o sobrecoste por conexiones. La elección de base de datos importa, pero no tanto como la forma de tu carga de trabajo y si has limitado las trampas obvias. Si tu app abre 200 conexiones, tanto MySQL como PostgreSQL sufrirán. Si tu almacenamiento es lento y fuerzas durabilidad con syncs frecuentes y caches diminutos, ambos se sentirán como si ejecutaran en un pager de 1999.

El objetivo no es “rápido”. El objetivo es “predecible”. Predecible significa:

  • El uso de memoria tiene un techo que puedes explicar.
  • Las escrituras a disco están amortiguadas (o al menos no son espasmódicas).
  • Las conexiones están acotadas y en pooling.
  • El mantenimiento en segundo plano no te sorprende a las 3 a. m.

Ese es el estándar. Cualquier cosa menor es comedia improvisada, y tus usuarios no compraron entradas.

Veredicto de usabilidad: ¿quién es realmente usable en 1GB?

Si quieres la respuesta directa

Ambos son utilizables con 1GB de RAM para cargas pequeñas, pero fallan de maneras diferentes:

  • PostgreSQL es más sensible a “demasiadas conexiones” y a sorpresas de memoria por consulta (sorts, hashes, work_mem). Es extremadamente estable cuando fijas límites duros y usas pooling. Además, es mejor para no corromper tu modelo mental.
  • MySQL (InnoDB) es más sensible a caches mal dimensionadas y al comportamiento de flush, y puede consumir memoria silenciosamente en buffers por conexión si se lo permites. Puede sentirse más “ágil” para lecturas/escrituras OLTP simples cuando se ajusta de forma conservadora, especialmente con contadores bajos de conexiones.

Qué recomiendo (opinión personal)

Si construyes una aplicación web general, especialmente cualquier cosa con consultas de reporte, migraciones y “añadiremos una función después”, usa PostgreSQL y coloca PgBouncer delante. Las líneas de seguridad operativa son más claras y las asunciones del ecosistema (migraciones, constraints, comportamiento transaccional de DDL) a menudo reducen la rareza a nivel de aplicación.

Si ejecutas una carga OLTP simple tipo clave-valor con un framework que espera MySQL, o ya tienes músculo operacional con MySQL, usa MySQL y limita las conexiones duramente. Solo no finjas que las configuraciones por defecto son “amigables con servidores pequeños”. Son “amigables con benchmarks de proveedor”.

Una frase para recordar: En 1GB, PostgreSQL es un buen ciudadano si le pones correa; MySQL es un buen ciudadano si le das las porciones correctas.

Broma corta #1: Un VPS de 1GB es como un estudio: puedes recibir invitados, pero no si todos traen muebles.

Modelos de memoria: por qué “cabe” no es un plan

El presupuesto de 1GB que tienes realmente

“1GB RAM” es marketing. Tu kernel toma algo. Tu monitorización toma algo. Tus sesiones SSH, cron y demonios de logs toman algo. Si es una distro moderna, systemd existe solo para recordarte que la entropía es real.

En un VPS de 1GB, un presupuesto seguro y realista para la memoria del proceso de la base de datos suele ser entre 500–750MB, dependiendo de qué más esté ejecutándose. Si ejecutas la app en el mismo VPS, reduce ese presupuesto. Si ejecutas Docker, réstalo otra vez, y después cuestiona tus elecciones de vida.

PostgreSQL en una frase

PostgreSQL tiene memoria compartida (principalmente shared_buffers) y mucha memoria por backend: cada conexión es un proceso (a menos que uses opciones más nuevas como modos --single, pero no es tu caso). La memoria por consulta la gobierna work_mem (sort/hash) y puede multiplicarse entre operaciones concurrentes. Esta es la trampa clásica: fijas work_mem en algo que parece razonable, luego una consulta usa varios a la vez en conexiones múltiples y pasas de “bien” a “OOM killer” en un deploy.

MySQL en una frase

MySQL con InnoDB tiene un gran cache compartido (innodb_buffer_pool_size) más buffers por conexión (sort/join/read buffers) y memoria interna (dictionary, adaptive hash, etc.). El buffer pool suele ser la palanca principal, y normalmente quieres que sea lo bastante grande para evitar lecturas constantes desde disco pero no tan grande como para dejar sin memoria al cache de página del SO y a todo lo demás. En 1GB, el buffer pool “correcto” suele ser más pequeño que tus instintos.

Dos reglas que previenen la mayoría de los desastres en 1GB

  1. Limita las conexiones para que la memoria por conexión no te multiplique hacia el fallo.
  2. Reserva memoria para el SO y para el cache del sistema de archivos; las bases de datos no obtienen derechos exclusivos de RAM solo por ser dramáticas.

Datos interesantes y contexto histórico (tómalos y suena sabio en reuniones)

  1. PostgreSQL desciende del proyecto POSTGRES en UC Berkeley (años 80), y la parte “SQL” se añadió después—el ADN de diseño prioriza extensibilidad y corrección sobre “simplemente enviar”.
  2. MySQL se volvió un estándar web a finales de los 90/principios de los 2000 en gran parte porque era ligero y fácil de desplegar, no porque fuera lo mejor en transacciones en ese momento.
  3. InnoDB fue originalmente un motor de terceros; se convirtió en el predeterminado en MySQL 5.5. Ese cambio cambió el juego operativo: la recuperación tras crash y MVCC se volvieron moneda corriente para usuarios de MySQL.
  4. El vacuum de PostgreSQL existe porque MVCC mantiene versiones antiguas de filas; si no haces vacuum, no solo obtienes bloat—eventualmente tendrás riesgo de wraparound de IDs de transacción.
  5. MySQL históricamente tuvo valores por defecto de durabilidad diferentes según settings como innodb_flush_log_at_trx_commit; muchas historias de “MySQL es rápido” eran en realidad “MySQL no sincronizaba cada commit”.
  6. El WAL de PostgreSQL funciona conceptualmente similar a otros motores serios: la durabilidad viene de escribir la intención en un log antes de volcar páginas de datos.
  7. El doublewrite buffer de InnoDB existe para protegerse contra escrituras parciales de página; intercambia escrituras extra por menos corrupciones tras crashes.
  8. El planner de PostgreSQL puede ser muy inteligente, pero en cajas diminutas también puede ser extremadamente caro si le pides ordenar resultados intermedios grandes en memoria que no tienes.
  9. El query cache de MySQL fue alguna vez una “característica de rendimiento” que con frecuencia se convertía en un bug de rendimiento; fue desaprobado y eliminado por buenas razones.

Y una idea operativa parafraseada que he visto sobrevivir en cada revisión de incidentes: la fiabilidad viene de diseñar sistemas que esperan fallos, no de pretender que puedes prevenirlos atribuida a John Allspaw.

Configuración base: MySQL en 1GB que no se come a sí mismo

Cuando MySQL tiene sentido en 1GB

MySQL funciona bien aquí si tu carga consiste mayormente en consultas indexadas simples, mantienes las conexiones bajas y no intentas convertir el VPS en un trofeo de benchmark. La condición de victoria es latencia estable bajo concurrencia moderada.

Un objetivo de memoria sensato para MySQL/InnoDB

Asume que puedes reservar 600–700MB para mysqld en un VPS de 1GB dedicado solo a la base de datos. Si alojas la app en la misma máquina, reduce eso a 400–500MB. Ahora asigna:

  • innodb_buffer_pool_size: típicamente 256–512MB
  • innodb_log_file_size: modesto (por ejemplo, 64–128MB) para mantener el tiempo de recuperación razonable
  • buffers por conexión: mantenlos pequeños; limita las conexiones

Fragmento de configuración base (MySQL 8-ish)

Esto es intencionalmente conservador. Puedes relajar después; no puedes deshacer un OOM en una máquina muerta.

cr0x@server:~$ sudo bash -lc 'cat >/etc/mysql/mysql.conf.d/99-vps-1gb.cnf <<"EOF"
[mysqld]
# Core
max_connections = 60
skip_name_resolve = 1

# InnoDB memory
innodb_buffer_pool_size = 384M
innodb_buffer_pool_instances = 1

# InnoDB durability / IO
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 1
sync_binlog = 1
innodb_io_capacity = 200
innodb_io_capacity_max = 400

# Redo log (MySQL 8 uses innodb_redo_log_capacity; keep modest)
innodb_redo_log_capacity = 256M

# Temp / sort behavior
tmp_table_size = 32M
max_heap_table_size = 32M
sort_buffer_size = 1M
join_buffer_size = 256K
read_buffer_size = 256K
read_rnd_buffer_size = 512K

# Avoid surprise thread memory
thread_cache_size = 16

# Slow query visibility
slow_query_log = 1
long_query_time = 0.5
log_slow_admin_statements = 1
EOF
systemctl restart mysql'
...output...

Notas que importan:

  • max_connections no es negociable en 1GB. Si tu app necesita más, tu app necesita pooling.
  • O_DIRECT ayuda a evitar el doble cacheado (InnoDB buffer pool + cache del SO). En algunas combinaciones VPS/almacenamiento es una ventaja; en otras es “aceptable”. Prueba, pero no te obsesiones.
  • Los pequeños buffers por conexión son el héroe silencioso. Buffers grandes por hilo son cómo te despiertas con OOM en un pico de tráfico.

Binary logs en 1GB: elige una postura

Si no necesitas replicación o recuperación punto en el tiempo, desactiva los binary logs para reducir la presión de escritura. Si los necesitas, mantenlos pero rota agresivamente y vigila el disco.

Configuración base: PostgreSQL en 1GB que se mantiene en pie

Cuando PostgreSQL tiene sentido en 1GB

PostgreSQL es excelente aquí si valoras la corrección, las restricciones y la semántica predecible, y estás dispuesto a gestionar las conexiones. No es “pesado”; es honesto sobre lo que cuesta el trabajo.

Un objetivo de memoria sensato para PostgreSQL

En un VPS de 1GB dedicado a BD, presupuestea aproximadamente 600–750MB para los procesos de Postgres más la memoria compartida, y deja el resto para el cache del SO y la cordura. Luego:

  • shared_buffers: 128–256MB (rara vez más en 1GB)
  • work_mem: 2–8MB (sí, tan bajo; la concurrencia multiplica)
  • maintenance_work_mem: 64–128MB (vacuum y builds de índices lo necesitan, pero no pongas 512MB en 1GB a menos que te guste el paging)
  • max_connections: mantenlo bajo y usa PgBouncer

Fragmento de configuración base (PostgreSQL 14–17)

cr0x@server:~$ sudo bash -lc 'PGVER=$(psql -V | awk "{print \$3}" | cut -d. -f1); \
CONF="/etc/postgresql/$PGVER/main/postgresql.conf"; \
cat >>"$CONF" <<"EOF"

# 1GB VPS baseline (conservative)
max_connections = 40

shared_buffers = 192MB
effective_cache_size = 512MB

work_mem = 4MB
maintenance_work_mem = 96MB

wal_buffers = 8MB
checkpoint_timeout = 10min
max_wal_size = 1GB
min_wal_size = 256MB
checkpoint_completion_target = 0.9

# Autovacuum: keep it on, but don’t let it stampede
autovacuum_max_workers = 2
autovacuum_naptime = 30s
autovacuum_vacuum_cost_limit = 1000
autovacuum_vacuum_cost_delay = 10ms

# Observability
log_min_duration_statement = 500
log_checkpoints = on
log_autovacuum_min_duration = 1000
EOF
systemctl restart postgresql'
...output...

Por qué estas elecciones:

  • effective_cache_size es una pista para el planner, no una asignación. En 1GB, no mientas en exceso.
  • Un work_mem pequeño reduce el riesgo de OOM. Si una consulta específica necesita más, configúrala por sesión para ese trabajo.
  • Autovacuum no es opcional. Si lo desactivas para “ahorrar recursos”, solo estás pidiendo prestado problemas con intereses depredadores.

Broma corta #2: Desactivar autovacuum en Postgres es como apagar la alarma de humo porque hace ruido.

Las conexiones te matarán: pooling y límites duros

Por qué PostgreSQL se siente “peor” con tormentas ingenuas de conexiones

Cada conexión de PostgreSQL es un proceso. Eso no es un fallo moral; es un diseño que hace que el aislamiento y la observabilidad sean claros. En 1GB también significa que 150 conexiones son básicamente un ataque de denegación de servicio que te has autoencargado.

PgBouncer no es “extra”, es el cinturón de seguridad

Pon PgBouncer en modo transaction pooling para tráfico web típico. Te permite mantener max_connections bajo mientras atiendes más concurrencia de clientes de forma segura.

cr0x@server:~$ sudo bash -lc 'apt-get update -y && apt-get install -y pgbouncer'
...output...
cr0x@server:~$ sudo bash -lc 'cat >/etc/pgbouncer/pgbouncer.ini <<"EOF"
[databases]
appdb = host=127.0.0.1 port=5432 dbname=appdb

[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432

auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt

pool_mode = transaction
max_client_conn = 200
default_pool_size = 20
reserve_pool_size = 5

server_idle_timeout = 60
query_timeout = 30
log_connections = 1
log_disconnections = 1
EOF
systemctl restart pgbouncer'
...output...

Para MySQL, el pooling sigue siendo inteligente. Muchas apps “manejan pooling” abriendo y cerrando conexiones constantemente, lo cual no es pooling; es cardio. Usa un pooler real en el runtime de tu app y mantén bajas las conexiones del servidor.

Almacenamiento y E/S: el disco del VPS es el jefe oculto

En planes de VPS pequeños, tu disco suele ser el componente más lento y el menos predecible. Puedes estar en un SSD compartido con créditos de ráfaga, o en un “SSD” que se comporta como un HDD educado. Las bases de datos hacen visible el comportamiento del disco porque escriben consistentemente y hacen fsync.

Los ajustes de durabilidad son ajustes de rendimiento

Cuando ejecutas valores durables por defecto (innodb_flush_log_at_trx_commit=1, commit síncrono en Postgres, fsync en WAL), estás eligiendo explícitamente esperar a que el almacenamiento confirme las escrituras. Eso es correcto. Si lo relajas, cambias corrección por velocidad.

En un VPS de 1GB para cargas hobby, podrías aceptar una durabilidad menos estricta. En producción, ten cuidado: solo descubrirás tu tolerancia real al riesgo después de haber perdido datos. Ese no es el momento para tener la conversación.

El cache del sistema de archivos importa incluso si tienes un buffer cache

El page cache de Linux sigue ayudando. Cachea metadata del sistema de archivos, tablespaces y páginas frecuentemente accedidas que la base de datos no ha fijado. Privar al SO de memoria es como obtener picos de latencia aleatorios incluso cuando el cache propio de la BD “es grande”. Por eso “dar InnoDB 80%” no es una verdad universal—especialmente no en 1GB.

Swap: el villano con un propósito

En 1GB, generalmente prefiero tener algo de swap (incluso un pequeño swapfile) para absorber picos breves en vez de disparar el OOM killer al instante. Pero si estás intercambiando constantemente, ya estás hundido. El swap es un airbag, no un motor.

Guion de diagnóstico rápido: encuentra el cuello de botella en minutos

Este es el orden que te evita entrar en una espiral de ajuste que dure una semana.

Primero: ¿es presión de memoria o E/S?

  1. Revisa swap y memoria: si el swap sube rápido o ocurren kills por OOM, deja de tunear SQL y limita primero conexiones/memoria.
  2. Revisa espera de E/S: un alto %iowait significa que esperas al disco; tu plan de consultas puede estar bien, tu disco puede estar triste.

Segundo: ¿está sobrecargada la base de datos o la app?

  1. Conteo de conexiones: ¿estás cerca del máximo de conexiones? Si sí, estás haciendo cola y thrashing.
  2. Consultas lentas: ¿unas pocas consultas dominan el tiempo? Si sí, arregla esas antes de “tunear el servidor”.

Tercero: ¿son checkpoints/flushes o contención de locks?

  1. PostgreSQL: revisa la frecuencia de checkpoints y el comportamiento del autovacuum; también busca locks bloqueando.
  2. MySQL: revisa la presión del redo log, misses del buffer pool y esperas de locks.

Cuarto: verifica almacenamiento y restricciones del kernel

  1. Espacio en disco: discos llenos causan caos.
  2. CPU steal: en hosts compartidos, tu “CPU” puede ser mayormente teórica.

Tareas prácticas (con comandos): observar, decidir, actuar

Estas son las comprobaciones que ejecuto en incidentes de VPS pequeños. Cada una incluye qué significa la salida y qué decisión tomar.

Tarea 1: Confirmar RAM, swap y presión

cr0x@server:~$ free -m
              total        used        free      shared  buff/cache   available
Mem:            987         612          54          12         320         248
Swap:          1023         310         713

Significado: Un available bajo y uso no trivial de swap indica presión de memoria. Si el swap crece durante el pico, vas hacia picos de latencia.

Decisión: Reduce max connections, reduce caches de BD ligeramente, añade pooling o añade RAM. No empieces por añadir índices.

Tarea 2: Ver si estás limitado por E/S (alto iowait)

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 1  0 317440  55296  12000 310000    0    8   120   980  300  450 12  6 72 10  0
 2  1 317680  41000  11800 309500    0   64   400  2200  500  900 18  8 58 16  0

Significado: wa (I/O wait) en las decenas indica que el disco es un cuello de botella, especialmente cuando se correlaciona con latencia.

Decisión: Reduce la amplificación de escritura (pacing de checkpoints/flush), evita grandes sort a disco y considera almacenamiento más rápido o mover la BD.

Tarea 3: Identificar los consumidores de memoria principales

cr0x@server:~$ ps -eo pid,comm,rss,pmem --sort=-rss | head
 1823 postgres   98200  9.9
 1744 postgres   94120  9.5
 1602 postgres   90500  9.2
 1320 mysqld     82000  8.3
  911 node       61000  6.1

Significado: Muchos backends de Postgres consumiendo ~90MB cada uno sugiere memoria por conexión + trabajo por consulta. MySQL aparece como un proceso grande.

Decisión: Para Postgres: reduce conexiones y work_mem; añade PgBouncer. Para MySQL: revisa tamaño del buffer pool y buffers por hilo.

Tarea 4: Comprobar si el kernel está matando procesos (OOM)

cr0x@server:~$ dmesg -T | tail -n 20
[Mon Dec 30 10:12:14 2025] Out of memory: Killed process 1823 (postgres) total-vm:1620400kB, anon-rss:210000kB, file-rss:1200kB, shmem-rss:0kB

Significado: Llegaste al límite. Postgres fue matado; probablemente perdiste trabajo en vuelo y la app puede estar haciendo reintentos agresivos.

Decisión: Inmediatamente limita conexiones, reduce settings de memoria y estabiliza. Luego encuentra la causa raíz del pico (consulta, despliegue, trabajo cron).

Tarea 5: Confirmar espacio en disco y saneidad de inodos

cr0x@server:~$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        25G   22G  2.1G  92% /

Significado: 92% no es “lleno”, pero está lo bastante cerca como para que autovacuum/archivos temporales/WAL te metan en problemas.

Decisión: Limpia logs, rota, archiva binlogs/WAL antiguos y mantén al menos unos GB libres. En discos pequeños, margen es vida.

Tarea 6: Medir latencia de almacenamiento rápidamente

cr0x@server:~$ iostat -x 1 3
Device            r/s     w/s   r_await   w_await  aqu-sz  %util
vda              8.00   35.00     6.10    28.40    1.20   92.00

Significado: Alto w_await y alto %util significan que el disco está saturado en escrituras.

Decisión: Reduce ráfagas de escritura (checkpoints de Postgres, flush de MySQL) y mueve trabajos batch pesados fuera de horas pico. Si está constantemente saturado, mejora el almacenamiento/plan.

Tarea 7: PostgreSQL—revisa conteo y estados de conexiones

cr0x@server:~$ sudo -u postgres psql -c "select state, count(*) from pg_stat_activity group by 1 order by 2 desc;"
   state   | count
-----------+-------
 idle      |    22
 active    |     9
 idle in transaction | 6
(3 rows)

Significado: “idle in transaction” es clásico: sesiones reteniendo locks y riesgo de bloat.

Decisión: Arregla el manejo de transacciones en la app; configura idle_in_transaction_session_timeout. El pooling ayuda pero no arregla código malo.

Tarea 8: PostgreSQL—encuentra las consultas lentas por tiempo total

cr0x@server:~$ sudo -u postgres psql -c "select query, calls, total_exec_time::int as total_ms, mean_exec_time::int as mean_ms from pg_stat_statements order by total_exec_time desc limit 5;"
                               query                                | calls | total_ms | mean_ms
--------------------------------------------------------------------+-------+----------+---------
 select * from events where user_id = $1 order by created_at desc    |  1200 |    98000 |      81
 update accounts set last_seen = now() where id = $1                 |  4500 |    42000 |       9
(2 rows)

Significado: La primera consulta es candidata a un índice como (user_id, created_at desc) y quizá a seleccionar menos columnas.

Decisión: Arregla los que más pesan antes de tocar settings globales. El tuning de 1GB no salvará un full table scan por cada petición.

Tarea 9: PostgreSQL—revisa la presión de checkpoints

cr0x@server:~$ sudo -u postgres psql -c "select checkpoints_timed, checkpoints_req, buffers_checkpoint, buffers_backend from pg_stat_bgwriter;"
 checkpoints_timed | checkpoints_req | buffers_checkpoint | buffers_backend
-------------------+-----------------+--------------------+----------------
                18 |              42 |            8123456 |         456789
(1 row)

Significado: Muchos checkpoints solicitados sugieren que el WAL se llena rápido (rafagas intensas de escritura), lo que puede causar picos de I/O.

Decisión: Aumenta max_wal_size modestamente (si el disco lo permite), ajusta checkpoints y reduce escrituras en ráfaga. No lo pongas enorme en discos pequeños.

Tarea 10: PostgreSQL—ver si autovacuum está al día

cr0x@server:~$ sudo -u postgres psql -c "select relname, n_dead_tup, last_autovacuum from pg_stat_user_tables order by n_dead_tup desc limit 5;"
 relname  | n_dead_tup |        last_autovacuum
----------+------------+-------------------------------
 events   |     182345 | 2025-12-30 09:50:12.12345+00
 logs     |      93440 | 
(2 rows)

Significado: Muchas tuplas muertas y falta de last_autovacuum en una tabla caliente sugiere que vacuum no da abasto o los umbrales están mal.

Decisión: Ajusta factores de autovacuum por tabla, añade índices con cuidado y asegúrate de que vacuum no esté bloqueado por transacciones largas.

Tarea 11: MySQL—ver variables relevantes de memoria

cr0x@server:~$ sudo mysql -e "show variables where Variable_name in ('max_connections','innodb_buffer_pool_size','tmp_table_size','max_heap_table_size','sort_buffer_size','join_buffer_size');"
+-------------------------+-----------+
| Variable_name           | Value     |
+-------------------------+-----------+
| innodb_buffer_pool_size | 402653184 |
| join_buffer_size        | 262144    |
| max_connections         | 60        |
| max_heap_table_size     | 33554432  |
| sort_buffer_size        | 1048576   |
| tmp_table_size          | 33554432  |
+-------------------------+-----------+

Significado: Confirma que los límites están aplicados; los tamaños parecen conservadores.

Decisión: Si ves buffers por hilo enormes o max_connections en las centenas, arregla eso antes de perseguir planes de consulta.

Tarea 12: MySQL—ver tasa de aciertos del buffer pool y lecturas

cr0x@server:~$ sudo mysql -e "show global status like 'Innodb_buffer_pool_read%';"
+---------------------------------------+-----------+
| Variable_name                         | Value     |
+---------------------------------------+-----------+
| Innodb_buffer_pool_read_requests      | 184003211 |
| Innodb_buffer_pool_reads              | 1200345   |
+---------------------------------------+-----------+

Significado: Algunas lecturas desde disco son normales. Si Innodb_buffer_pool_reads sube rápido relativo a requests, el working set no cabe y estás ligado al I/O.

Decisión: Si el disco es lo suficientemente rápido, quizá esté bien. Si no, aumenta buffer pool un poco (sin dejar al SO sin memoria) o reduce dataset/consultas.

Tarea 13: MySQL—ver tablas temporales y si van a disco

cr0x@server:~$ sudo mysql -e "show global status like 'Created_tmp%tables';"
+-------------------------+--------+
| Variable_name           | Value  |
+-------------------------+--------+
| Created_tmp_disk_tables | 18420  |
| Created_tmp_tables      | 40210  |
+-------------------------+--------+

Significado: Una gran fracción de tmp tables en disco indica sorts/joins derramando, lo que perjudica en almacenamiento lento.

Decisión: Añade índices, reescribe consultas o aumenta límites de tablas temporales si la memoria lo permite. En 1GB, arreglar consultas vence a la fuerza bruta de memoria.

Tarea 14: Confirmar uso de conexiones (MySQL)

cr0x@server:~$ sudo mysql -e "show global status like 'Threads_connected'; show global status like 'Max_used_connections';"
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Threads_connected | 48    |
+-------------------+-------+
+----------------------+-------+
| Variable_name        | Value |
+----------------------+-------+
| Max_used_connections | 59    |
+----------------------+-------+

Significado: Estás tocando el límite. Eso puede ser cola sana (si la latencia está bien) o un cuello de botella (si las peticiones fallan por timeout).

Decisión: Si hay timeouts, añade pooling y reduce concurrencia en la app. Aumentar max_connections en 1GB suele solo aumentar el radio de explosión.

Tarea 15: Revisar tiempo de steal de CPU (realidad de VPS compartido)

cr0x@server:~$ mpstat 1 3
Linux 6.1.0 (server) 	12/30/2025 	_x86_64_	(1 CPU)

12:10:01 AM  %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
12:10:02 AM  18.0  0.0  8.0    10.0  0.0   1.0   12.0   0.0    0.0  51.0

Significado: %steal al 12% significa que el hipervisor te está quitando CPU. Tu tuning no puede pelear con tu proveedor.

Decisión: Considera un mejor plan/proveedor, reduce la carga o mueve la BD a otro sitio. No desperdicies días ajustando queries mientras el host está sobrevendido.

Tarea 16: Confirmar latencia desde la propia base de datos

cr0x@server:~$ sudo -u postgres psql -c "select now(), pg_sleep(0.1), now();"
              now              | pg_sleep |              now
-------------------------------+----------+-------------------------------
 2025-12-30 00:10:10.0101+00   |          | 2025-12-30 00:10:10.1109+00
(1 row)

Significado: Si esta llamada simple es lenta o se queda bloqueada, el problema es a nivel de sistema (E/S, CPU steal, thrash de memoria), no de consulta.

Decisión: Deja de culpar al ORM y mira el host.

Tres microhistorias corporativas desde las trincheras

1) Incidente causado por una suposición errónea: “Es solo 1GB, pero es solo una máquina de dev”

Tenían un pequeño servicio interno en un VPS de 1GB. “Interno” significaba “solo usado por ingenieros”, que es una forma educada de decir “nadie lo midió y todos asumieron que estaba bien”. Eligieron PostgreSQL, pusieron max_connections a 200 porque la app explotaba a veces y fijaron work_mem en 64MB porque un blog decía que ordenar en memoria es más rápido.

Funcionó durante semanas. Luego se lanzó un nuevo dashboard que ejecutaba un puñado de consultas de agregación por carga de página. Cada consulta usó múltiples sorts y hashes. Los ingenieros abrieron el dashboard en paralelo durante un incidente. La base de datos no degradó de forma graciosa; se precipitó por un acantilado. El kernel empezó a matar backends. El dashboard reintentó. Las consultas reintentadas crearon más backends. Puedes adivinar el resto.

La suposición fue: “64MB es poco; el servidor tiene 1GB”. La realidad: work_mem es por operación, por conexión, y las consultas pueden usar varias a la vez. Multiplica por concurrencia y has construido un generador de OOM.

La solución no fue heroica. Bajaron max_connections a 40, redujeron work_mem a 4MB, pusieron PgBouncer adelante y movieron el dashboard para ejecutar consultas pesadas de forma asíncrona con resultados cacheados. El servicio “interno” volvió a ser aburrido, que es el mayor cumplido que le puedes hacer a una base de datos.

2) Optimización que salió mal: “Hagamos que los checkpoints desaparezcan”

Otro equipo ejecutaba Postgres en un VPS pequeño y se quejaba de picos periódicos de latencia. Vieron logs de checkpoints y concluyeron que los checkpoints eran el enemigo. Su solución fue aumentar max_wal_size agresivamente y estirar checkpoint_timeout para que los checkpoints sucedieran menos seguido.

Por un tiempo, pareció mejor. Menos picos. Todos declararon victoria y siguieron. Entonces tuvieron un reboot no planificado (mantenimiento del proveedor). La recuperación tardó mucho más de lo esperado y el servicio falló la ventana de SLO. No hubo pérdida de datos, pero el informe del incidente fue incómodo porque la “optimización” fue el único cambio notable reciente.

Aprendieron el trade-off: menos checkpoints puede significar WAL más grande para reproducir, y en almacenamiento lento eso puede ser doloroso. En discos pequeños de VPS, un WAL mayor también ocupa espacio que desplaza todo lo demás, aumentando la probabilidad de quedarte sin disco en el peor momento.

La solución aburrida: volver a valores conservadores de checkpoints y luego realmente resolver la causa raíz—ráfagas de escritura de trabajos batch. Ralentizaron esos trabajos, ajustaron parámetros de costo de autovacuum para esparcir la E/S y aceptaron que algo de actividad de checkpoints es normal. Una base de datos que nunca checkpointea no está “optimizada”; solo está procrastinando ruidosamente.

3) Práctica aburrida pero correcta que salvó el día: límites de conexiones + logs de consultas lentas

Un SaaS pequeño ejecutaba MySQL en instancias de 1GB para shards de clientes. Nada sofisticado, solo OLTP. Tenían una regla estricta: cada shard tenía un límite duro de max_connections, buffers por hilo conservadores y slow query logging activado con un umbral bajo. Además, tenían una revisión semanal donde alguien ojeaba muestras de consultas lentas buscando regresiones.

Un viernes, una nueva función introdujo una consulta que era “aceptable” en staging pero patológica para un shard de cliente con un dataset sesgado. La consulta hacía un join sin el índice correcto y derramó tablas temporales a disco. En un VPS de 1GB con E/S mediocre, así conviertes una sola petición en una denegación de servicio.

La diferencia: el shard no se descontroló. Los límites de conexión evitaron que una manada explotara la memoria. Los logs de consultas lentas hicieron al culpable obvio en minutos. Revirtieron la función para ese shard, añadieron el índice correcto y redeployaron tras verificar.

Sin tuning heroico. Sin noches de “quizá Linux está roto”. Solo guardarraíles y un rastro de evidencia. El incidente fue aburrido, y aburrido es exactamente lo que quieres.

Errores comunes: síntoma → causa raíz → solución

1) Síntoma: timeouts repentinos, picos de load average, swap subiendo

Causa raíz: Demasiadas conexiones + memoria por conexión (backends de Postgres, buffers por hilo de MySQL) causando presión de memoria y thrash de swap.

Solución: Limita conexiones del servidor; añade pooling; disminuye work_mem (Postgres) y buffers por hilo (MySQL). Considera un pequeño swapfile si no tenías, pero trátalo como red de seguridad, no como capacidad.

2) Síntoma: picos periódicos de latencia cada pocos minutos

Causa raíz: Rafagas de checkpoints/flush y saturación de escritura del almacenamiento.

Solución: Para Postgres, ajusta el pacing de checkpoints (checkpoint_completion_target) y permite un tamaño moderado de WAL; para MySQL, revisa flushing y settings de redo. Reduce trabajos con escrituras en ráfaga y evita transacciones masivas.

3) Síntoma: uso de disco de PostgreSQL crece aunque los datos “no cambien mucho”

Causa raíz: Bloat por MVCC + retraso de vacuum, frecuentemente empeorado por transacciones largas o autovacuum deshabilitado.

Solución: Mantén autovacuum activado; busca transacciones largas; ajusta umbrales de autovacuum por tabla; considera VACUUM (ANALYZE) o REINDEX periódicos en ventanas de mantenimiento.

4) Síntoma: MySQL se siente rápido y luego se cuelga en escrituras

Causa raíz: Presión de redo log y latencia de fsync; posiblemente capacidad de redo pequeña combinada con ráfagas de escritura.

Solución: Mantén la capacidad de redo moderada, evita transacciones enormes y valida tu almacenamiento. Si el disco del VPS es errático, ningún parámetro lo hará honesto.

5) Síntoma: conexiones “idle in transaction” se acumulan (Postgres)

Causa raíz: La app inicia una transacción y olvida hacer commit/rollback, reteniendo locks y previniendo limpieza por vacuum.

Solución: Corrige el alcance de transacciones en la app; fija idle_in_transaction_session_timeout; monitoriza pg_stat_activity y mata sesiones ofensivas cuando haga falta.

6) Síntoma: CPU alta, pero bajo rendimiento de consultas

Causa raíz: Planes de consulta malos, índices faltantes o sorts/hashes caros que derraman a disco; a veces CPU steal.

Solución: Usa EXPLAIN (ANALYZE, BUFFERS) (Postgres) o EXPLAIN (MySQL), arregla índices y verifica %steal. Si steal es alto, tu “tuning de CPU” es teatro.

7) Síntoma: todo se ralentiza tras “aumentar caches”

Causa raíz: Dejar sin memoria al SO y al cache del sistema de archivos; swapping; tareas en background compitiendo por memoria.

Solución: Reduce ligeramente los tamaños de cache de BD, deja cabeza de memoria y vuelve a medir. En 1GB, dejar 200MB libres puede rendir más que exprimir cada MB en la BD.

Listas de verificación / plan paso a paso

Paso a paso: elegir MySQL vs PostgreSQL para 1GB

  1. Cuenta tu concurrencia: si necesitas muchos clientes concurrentes, planifica pooling independientemente de la BD.
  2. Clasifica las consultas: mayormente OLTP indexado simple vs OLTP + reporting mixto. Las cargas mixtas favorecen la semántica y tooling de Postgres, pero solo si controlas conexiones.
  3. Decide postura de durabilidad: si no aceptas pérdida de datos, no “optimices” eliminando fsyncs.
  4. Revisa la calidad del almacenamiento: si el disco es lento, ajusta para menos escrituras y ráfagas más pequeñas; considera mover la BD fuera del VPS.
  5. Elige los valores aburridos de esta guía y cambia solo una variable a la vez midiendo.

Paso a paso: endurecer un host de BD de 1GB

  1. Instala monitorización (mínimo: disco, memoria, swap, load, iowait).
  2. Configura un swapfile si no existe (pequeño, por ejemplo 1GB) y ajusta vm.swappiness razonablemente para tu distro.
  3. Limita conexiones de BD (max_connections), luego aplica pooling en la app.
  4. Activa slow query logging (log_min_duration_statement o slow log de MySQL) con un umbral que detecte el dolor temprano (por ejemplo, 500ms).
  5. Verifica que autovacuum esté activo (Postgres) y no esté bloqueado; verifica que InnoDB esté en uso (MySQL) y que el buffer pool no sea absurdo.
  6. Mantén márgenes de espacio libre en disco y rota logs. Un disco lleno convierte “problema de rendimiento” en “incidente”.
  7. Realiza una prueba de carga que refleje la realidad, no la esperanza.

Paso a paso: afinar sin culto a la carga

  1. Obtén una línea base: latencia, throughput, CPU, iowait, memoria.
  2. Arregla la peor consulta primero (la que más tiempo total consume), no la “más discutida”.
  3. Solo entonces considera dimensionar caches: buffer pool / shared buffers.
  4. Vuelve a comprobar tras cambios. Si no puedes medir mejora, revierte y sigue adelante.

Preguntas frecuentes

1) ¿Puedo ejecutar PostgreSQL en 1GB sin PgBouncer?

Puedes, pero apuestas tu estabilidad a que tu app nunca abra demasiadas conexiones. Si es un servicio pequeño y con pooling estricto en el runtime, bien. Si no, usa PgBouncer y duerme tranquilo.

2) ¿Cuál es un shared_buffers seguro en 1GB?

Usualmente 128–256MB. Si pones 512MB en una caja de 1GB y además permites muchas conexiones, estás construyendo un apretón de memoria. Deja espacio para el SO y para los procesos backend.

3) ¿Cuál es un innodb_buffer_pool_size seguro en 1GB?

A menudo 256–512MB. Si el VPS también corre la app, inclínate por 256–384MB. El resto de memoria no está “desperdiciada”; previene swapping y ayuda al cache del SO.

4) ¿Debo desactivar fsync o commits síncronos para ir más rápido?

Sólo si aceptas plenamente perder transacciones recientes en caso de pérdida de energía o crash del host. En un VPS no controlas los modos de fallo. En producción, mantén la durabilidad activada y optimiza en otro lado.

5) ¿El swap es bueno o malo para bases de datos en máquinas pequeñas?

Algo de swap es bueno como amortiguador. Intercambio constante es fatal. Si ves crecimiento sostenido de swap durante carga normal, tienes un problema de presupuesto de memoria, no de configuración de swap.

6) ¿Qué base de datos es “más ligera” en memoria?

Ninguna por defecto. MySQL suele estar dominado por un proceso grande (buffer pool) más buffers por conexión; Postgres tiende a escalar la memoria con las conexiones. Con límites sensatos y pooling, ambos caben. Sin límites, ambos te pueden arruinar el día.

7) ¿Cuál es el ajuste más importante en 1GB?

max_connections. Controla la multiplicación de memoria en el peor caso y tu perfil de contención. Si fallas en esto, el resto del tuning es decorativo.

8) ¿Cómo sé si necesito más RAM o mejor disco?

Si estás swappeando o recibiendo kills por OOM, necesitas más RAM o menor concurrencia. Si %iowait es alto y w_await es grande, necesitas mejor disco o menos escrituras. A menudo necesitas ambos, pero uno suele gritar más fuerte.

9) ¿Puedo “añadir índices” para arreglar rendimiento en 1GB?

Los índices ayudan lecturas y pueden perjudicar escrituras. En 1GB con almacenamiento lento, demasiados índices aumentan la amplificación de escritura y el trabajo de vacuum. Añade los índices correctos para las consultas principales, no todos los que tu ORM sugiere.

10) ¿Debo ejecutar la app y la BD en el mismo VPS de 1GB?

Solo para despliegues muy pequeños. Colocar ambos juntos aumenta la contención y hace los incidentes más difíciles de razonar. Si debes, reduce caches de BD y aplica pooling de conexiones agresivamente.

Próximos pasos que puedes hacer hoy

  1. Elige un límite de conexiones (Postgres 40, MySQL 60 es un punto de partida razonable) y aplica pooling.
  2. Aplica la configuración base conservadora para la BD elegida, reinicia y verifica que los settings se aplicaron.
  3. Activa la visibilidad de consultas lentas (umbral 500ms) y recoge evidencia durante un día.
  4. Ejecuta el guion de diagnóstico rápido la próxima vez que haya un pico: memoria → iowait → conexiones → consultas top → checkpoints/flushes.
  5. Decide tu disparador de upgrade: si regularmente estás cerca de los límites, swappeando o saturando disco, 2GB de RAM o mejor almacenamiento compran más estabilidad que la astucia.

Si quieres una regla final para bases de datos en VPS de 1GB: acota tu peor caso. Para eso sirven los settings. El resto es discutir con la física.

← Anterior
Volúmenes Docker: montajes bind vs volúmenes nombrados — qué sobrevive mejor a las migraciones
Siguiente →
Casillas y botones de radio personalizados con CSS puro: patrones accesibles que no engañan

Deja un comentario