Tienes un VPS con 4 GB de RAM. Unos cuantos sitios web. Una base de datos. Y ahora un pager, un ticket o un correo de cliente que dice: “El sitio va lento.” Nada es más humillante que ver una máquina de $10/mes intentar ser una plataforma empresarial porque alguien activó un plugin que “solo ejecuta una consulta”.
Esta es una guía de campo para dejar MySQL o PostgreSQL lo suficientemente estable y rápido para cargas de trabajo de sitios web en hardware de VPS pequeño. No es una fantasía de benchmarks. No es un volcado de configuraciones. Lo que configuras primero, lo que mides primero y lo que dejas de hacer antes de que te cueste fines de semana.
Primera decisión: MySQL o PostgreSQL para sitios web en 4GB
En un VPS de 4GB, la “mejor base de datos” es la que puedes mantener predecible bajo presión de memoria y tráfico con ráfagas. Tu enemigo no es el rendimiento teórico. Son las tormentas de swap, las avalanchas de conexiones y los picos de latencia de almacenamiento que convierten “suficientemente rápido” en “¿por qué la compra está agotándose?”.
Elige MySQL (InnoDB) cuando:
- Tu stack ya es nativo de MySQL (WordPress, Magento, muchas apps PHP) y no quieres ser la persona que reescribe todo “por diversión”.
- Quieres una historia de caché relativamente directa: el InnoDB buffer pool es el gran control y se comporta como tal.
- Necesitas replicación fácil de operar con herramientas comunes y aceptas compensaciones de consistencia eventual en algunos modos.
Elige PostgreSQL cuando:
- Te importan la corrección de consultas y las características SQL ricas (funciones de ventana reales, CTE, mejores constraints y tipos de datos) y realmente las vas a usar.
- Quieres planes de consulta predecibles, buena observabilidad y valores por defecto sensatos para muchos patrones de apps modernas.
- Puedes comprometerte con el pooling de conexiones (pgBouncer) porque el modelo process-per-connection de PostgreSQL castiga “abrir más conexiones” en máquinas pequeñas.
Si esto es mayormente tráfico de CMS con plugins que no controlas, suelo ser conservador: quédate con MySQL a menos que la app ya sea Postgres-first. Si estás construyendo algo nuevo con un equipo que escribe SQL intencionalmente, PostgreSQL suele ser la mejor apuesta a largo plazo. Pero en 4GB, la victoria a corto plazo es la simplicidad operativa, no la pureza filosófica.
Regla práctica: si no puedes describir tus 5 consultas principales y sus índices, no estás “eligiendo una base de datos”, estás eligiendo qué modos de fallo quieres experimentar primero.
Hechos interesantes y contexto histórico (que realmente cambia decisiones)
- El dominio temprano de MySQL en la web vino por la ubicuidad del stack LAMP y la velocidad “suficientemente buena” para sitios principalmente de lectura. Por eso tantas apps de sitio aún asumen particularidades del dialecto MySQL.
- InnoDB se convirtió en el motor por defecto en MySQL 5.5 (era 2010). Si aún piensas en términos de MyISAM (bloqueos de tabla, sin recuperación tras crash), llevas un fósil en el bolsillo.
- El modelo MVCC de PostgreSQL es una de las razones por las que se mantiene consistente bajo concurrencia, pero genera la necesidad constante de vacuum. Ignorar vacuum no hará que la base de datos grite; irá empeorando lentamente.
- PostgreSQL ha evolucionado hacia un modelo de ejecución más amigable con el paralelismo (consultas paralelas, mejores capacidades del planner). En un VPS pequeño esto importa menos que en hierro grande, pero forma parte de por qué Postgres “se siente moderno” para consultas de analítica.
- El query cache de MySQL fue eliminado en MySQL 8.0 porque escalaba mal bajo concurrencia. Si alguien te dice “habilita query_cache_size”, encontraste a un viajero en el tiempo.
- Postgres recibe crédito por estándares y corrección porque históricamente priorizó funciones e integridad sobre velocidad cruda temprana. Hoy también es rápido, pero el ADN cultural sigue en los defaults y las herramientas.
- Ambos motores son conservadores respecto a durabilidad por defecto (fsync, WAL/redo). Desactivar ajustes de durabilidad hace que los benchmarks luzcan heroicos y los postmortems parezcan escenas de crimen.
- MariaDB se desvió de MySQL de maneras significativas. El consejo de “tuning MySQL” a veces no aplica bien a versiones de MariaDB y a sus motores de almacenamiento. Verifica qué estás ejecutando.
- RDS y servicios gestionados influyeron en la folclore de tuning: la gente copia defaults de la nube a VPS, y luego se pregunta por qué una caja de 4GB se comporta como si estuviera bajo el agua.
Arquitectura base para un VPS de 4GB (y por qué importa)
En un VPS de 4GB no tienes “memoria extra”. Tienes un presupuesto. Gástalo en cachés que reduzcan I/O y en margen que prevenga swapping. La caché de página del SO también importa porque tanto MySQL como PostgreSQL al final necesitan lecturas respaldadas por el sistema de ficheros, y el kernel no es tu enemigo; es tu última línea de defensa.
Presupuesto de memoria basado en la realidad
- SO + SSH + daemons básicos: 300–600MB
- Servidor web + PHP-FPM: variable. Unos cientos de MB hasta varios GB según el número de procesos y el comportamiento de la app.
- Base de datos: lo que queda, pero no todo. Si le das al DB todo, la capa web hará OOM o swapeará cuando suba el tráfico.
Para “sitios web en un único VPS”, la base de datos no está aislada. Esta es una de las pocas veces en que “configurar y olvidar” no es pereza; es supervivencia.
Opinión: Si ejecutas web y DB en el mismo VPS de 4GB, planea asignar aproximadamente máximo 1.5–2.5GB a la capa de caché de la base de datos, a menos que hayas medido el uso de memoria de PHP bajo carga y sea realmente pequeño. Tu objetivo es latencia estable, no un buffer pool heroico.
Chiste #1: Un VPS de 4GB es como un estudio — técnicamente puedes meter una caminadora, pero odiarás tu vida y también tus vecinos.
Guion de diagnóstico rápido: encuentra el cuello de botella en 10 minutos
Este es el orden en que verifico las cosas cuando “el sitio va lento” y la base de datos es la principal sospechosa. Cada paso te dice si mirar CPU, memoria, conexiones, bloqueos o almacenamiento.
Primero: ¿la máquina está hambrienta (CPU, RAM, swap)?
- Revisa la carga vs número de CPUs.
- Revisa actividad de swap y fallos de página mayores.
- Revisa el historial del OOM killer.
Segundo: ¿es latencia de almacenamiento (IOPS/fsync/WAL/redo)?
- Alto iowait, fsync lentos, tiempos de commit largos o checkpoints estancados.
- Busca profundidad de cola y tiempos medios de espera.
Tercero: ¿presión de conexiones?
- Demasiadas conexiones o hilos en la BD.
- Tormentas de conexiones desde workers PHP.
- Conteos de hilos/procesos alcanzando la memoria.
Cuarto: ¿bloqueos o transacciones largas?
- MySQL: metadata locks, locks de InnoDB, transacciones de larga duración.
- Postgres: consultas bloqueadas, sesiones idle-in-transaction, vacuum bloqueado por snapshots antiguos.
Quinto: ¿consultas malas + índices faltantes?
- Los slow query logs / pg_stat_statements muestran a los mayores ofensores.
- Busca scans de tablas completas y “filesort”/tablas temporales o scans secuenciales con grandes conteos de filas.
Eso es todo. No empieces cambiando controles al azar. No copies un “my.cnf de alto rendimiento” de un servidor de 64GB. Mide, luego elige un cambio que puedas explicar.
Cita (idea parafraseada): La idea de confiabilidad de John Allspaw: la producción es donde las suposiciones van a morir, así que diseña y opera para el aprendizaje, no para la certeza.
Tareas prácticas: comandos, salidas y qué hacer después
Estas son tareas reales que puedes ejecutar en un VPS Linux. Cada una incluye: el comando, qué significa la salida típica y la decisión que tomas. Ejecútalas en orden cuando estés triageando o estableciendo bases.
Tarea 1: Confirma la presión básica del sistema (CPU, RAM, swap)
cr0x@server:~$ uptime
14:22:19 up 36 days, 3:18, 1 user, load average: 5.84, 5.12, 3.90
Qué significa: En un VPS de 2 vCPU, cargas medias por encima de ~2–3 durante períodos sostenidos suelen indicar colas de procesos listos (CPU) o esperas I/O ininterrumpibles.
Decisión: Si la carga es alta, verifica inmediatamente iowait y memoria/swap antes de tocar configuraciones de la base de datos.
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 3.8Gi 3.3Gi 120Mi 90Mi 420Mi 220Mi
Swap: 1.0Gi 860Mi 164Mi
Qué significa: Uso de swap con “available” bajo es una señal roja para cargas web sensibles a latencia.
Decisión: Si el swap se usa durante el pico, reduce la huella de memoria del DB y/o reduce los workers de PHP-FPM. Considera habilitar una pequeña cantidad de swap solo como colchón contra crashes, no como capa de rendimiento.
Tarea 2: Verifica swapping activo e 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
2 1 880000 120000 28000 380000 12 45 210 480 520 900 18 8 40 34 0
3 0 880000 118000 28000 381000 0 60 160 510 600 1100 20 9 35 36 0
1 2 880000 115000 28000 379000 20 0 400 200 480 850 15 7 38 40 0
4 0 880000 119000 28000 382000 0 8 90 520 610 1200 22 10 32 36 0
2 1 880000 117000 28000 381500 0 35 200 600 590 1050 19 9 34 38 0
Qué significa: Valores no nulos en si/so indican swapping. Alto wa indica espera por I/O.
Decisión: Si el swapping está activo, deja de tunear consultas y arregla conteos de memoria/conexiones. Si el iowait es alto, enfócate en latencia de almacenamiento, comportamiento de checkpoints/flush y en reducir la amplificación de escrituras.
Tarea 3: Identificar latencia de disco y colas rápidamente
cr0x@server:~$ iostat -xz 1 3
avg-cpu: %user %nice %system %iowait %steal %idle
18.20 0.00 8.10 34.50 0.00 39.20
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %util await aqu-sz
vda 12.0 48.0 900.0 6200.0 0.2 3.1 92.0 21.5 1.8
Qué significa: %util cerca de 100% y await > 10–20ms en un disco de VPS suele significar que tu almacenamiento es el cuello de botella.
Decisión: Reduce escrituras (batching, ajuste de autovacuum, tuning de flush de InnoDB), mueve la BD a mejor almacenamiento o separa DB y web. En algunos planes de VPS, el único “tunear” que importa es pagar por más IOPS.
Tarea 4: Confirma qué BD estás ejecutando y versión
cr0x@server:~$ mysql --version
mysql Ver 8.0.36-0ubuntu0.22.04.1 for Linux on x86_64 ((Ubuntu))
Qué significa: Las versiones mayores cambian defaults y eliminan knobs (como query cache). La versión te dice qué consejos son aplicables.
Decisión: No apliques recetas de tuning a menos que coincidan con tu versión mayor y tu sabor (MySQL vs MariaDB).
cr0x@server:~$ psql --version
psql (PostgreSQL) 16.1 (Ubuntu 16.1-1.pgdg22.04+1)
Qué significa: Versiones más nuevas de Postgres mejoran vacuum, WAL y planner. Eso cambia “qué duele” en cajas pequeñas.
Decisión: En Postgres antiguo quizás necesites más cuidado manual. En Postgres nuevo enfócate más en pooling de conexiones y umbrales de autovacuum.
Tarea 5: Cuenta conexiones DB (MySQL)
cr0x@server:~$ mysql -e "SHOW STATUS LIKE 'Threads_connected';"
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_connected | 185 |
+-------------------+-------+
Qué significa: 185 conexiones en un VPS de 4GB con PHP suele ser problema, incluso antes de que las consultas se pongan lentas.
Decisión: Limita la concurrencia de la app, habilita conexiones persistentes con cautela o mueve a un patrón que limite la concurrencia DB (colas en la app, caché o separar lecturas). Si no puedes controlar la app, reduce max_connections y acepta fallos controlados en lugar de un colapso total.
Tarea 6: Cuenta conexiones DB (PostgreSQL)
cr0x@server:~$ sudo -u postgres psql -c "SELECT count(*) AS connections FROM pg_stat_activity;"
connections
-------------
142
(1 row)
Qué significa: 142 sesiones de Postgres equivalen a 142 procesos backend. En un VPS de 4GB, eso es un impuesto de memoria y de cambio de contexto.
Decisión: Instala pgBouncer y baja max_connections. En cajas pequeñas, Postgres sin pooling es una broma de mal gusto que te haces a ti mismo.
Tarea 7: Encuentra consultas de larga duración y bloqueos (PostgreSQL)
cr0x@server:~$ sudo -u postgres psql -c "SELECT pid, now()-query_start AS age, state, wait_event_type, wait_event, left(query,80) AS q FROM pg_stat_activity WHERE state <> 'idle' ORDER BY age DESC LIMIT 5;"
pid | age | state | wait_event_type | wait_event | q
------+----------+--------+-----------------+------------+--------------------------------------------------------------------------------
9123 | 00:02:18 | active | Lock | relation | UPDATE orders SET status='paid' WHERE id=$1
9051 | 00:01:44 | active | IO | DataFileRead | SELECT * FROM products WHERE slug=$1
(2 rows)
Qué significa: Esperas por locks apuntan a contención; esperas por IO apuntan a almacenamiento lento o misses de caché.
Decisión: Si las esperas por Lock dominan, arregla el alcance de las transacciones y el indexado. Si las esperas por IO dominan, aumenta el caching efectivo (con cautela) y reduce lecturas aleatorias mediante índices y modelado de consultas.
Tarea 8: Encuentra esperas por locks (MySQL)
cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST;"
Id User Host db Command Time State Info
210 app 10.0.0.12:50344 shop Query 75 Waiting for table metadata lock ALTER TABLE orders ADD COLUMN foo INT
238 app 10.0.0.15:38822 shop Query 12 Sending data SELECT * FROM orders WHERE created_at > NOW() - INTERVAL 1 DAY
Qué significa: Los metadata locks pueden congelar escrituras y lecturas detrás de cambios de esquema, dependiendo de la operación y la versión.
Decisión: Deja de hacer cambios de esquema en línea a la ligera en un VPS pequeño. Programa mantenimiento o usa herramientas de migración de esquema online diseñadas para reducir bloqueos.
Tarea 9: Revisa la tasa de aciertos del InnoDB buffer pool y presión de lectura
cr0x@server:~$ mysql -e "SHOW STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+---------+
| Variable_name | Value |
+---------------------------------------+---------+
| Innodb_buffer_pool_read_requests | 9823412 |
| Innodb_buffer_pool_reads | 412390 |
+---------------------------------------+---------+
Qué significa: reads son lecturas físicas; read_requests son lógicas. Si las lecturas físicas son altas respecto a requests, estás perdiendo caché.
Decisión: Si el working set cabe en RAM, incrementa innodb_buffer_pool_size con cautela. Si no cabe, prioriza índices y reducir el working set (menos columnas, menos scans).
Tarea 10: Revisa caché de Postgres y derrames a archivos temporales
cr0x@server:~$ sudo -u postgres psql -c "SELECT datname, blks_hit, blks_read, temp_files, temp_bytes FROM pg_stat_database ORDER BY temp_bytes DESC LIMIT 5;"
datname | blks_hit | blks_read | temp_files | temp_bytes
-----------+----------+-----------+------------+--------------
appdb | 9201123 | 612332 | 1832 | 2147483648
(1 row)
Qué significa: Mucho temp_bytes sugiere que ordenamientos/hashes están desbordando a disco porque work_mem es demasiado pequeño para esas operaciones—o las consultas están pidiendo demasiado.
Decisión: No subas work_mem globalmente en un VPS pequeño. Arregla consultas e índices primero; luego aumenta work_mem por rol o por sesión para cargas específicas.
Tarea 11: Ver las consultas top (Postgres, si pg_stat_statements está habilitado)
cr0x@server:~$ sudo -u postgres psql -c "SELECT calls, mean_exec_time, rows, left(query,80) AS q FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 5;"
calls | mean_exec_time | rows | q
-------+----------------+------+--------------------------------------------------------------------------------
82021 | 12.45 | 1 | SELECT id FROM sessions WHERE token=$1
1220 | 210.12 | 300 | SELECT * FROM orders WHERE user_id=$1 ORDER BY created_at DESC LIMIT 50
(2 rows)
Qué significa: Las consultas con alto tiempo total son las que consumen tu presupuesto. Las de alto conteo son “muerte por mil cortes”.
Decisión: Indexa las rutas calientes y reduce consultas chatty. Si una consulta se ejecuta 80k veces y tarda 12ms, es la razón de haber desperdiciado un core.
Tarea 12: Habilita y lee el slow query log de MySQL rápidamente
cr0x@server:~$ mysql -e "SET GLOBAL slow_query_log=ON; SET GLOBAL long_query_time=0.5; SET GLOBAL log_queries_not_using_indexes=ON;"
...output omitted...
Qué significa: Estás activando la recolección de evidencia. Mantén umbrales razonables para no hacerte DOS con logs.
Decisión: Recoge durante 15–60 minutos en pico, luego usa los datos para arreglar los peores ofensores. Desactiva log_queries_not_using_indexes si es demasiado ruidoso para tu app.
cr0x@server:~$ sudo tail -n 5 /var/log/mysql/mysql-slow.log
# Query_time: 1.204 Lock_time: 0.000 Rows_sent: 50 Rows_examined: 84512
SELECT * FROM orders WHERE user_id=123 ORDER BY created_at DESC LIMIT 50;
Qué significa: Rows examined es enorme respecto a rows sent: clásico índice faltante o índice en orden incorrecto.
Decisión: Añade/ajusta índices compuestos para coincidir con el patrón filter + sort (por ejemplo, (user_id, created_at)), y verifica con EXPLAIN.
Tarea 13: Revisa espacio en sistema de ficheros y presión de inodos
cr0x@server:~$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 80G 74G 2.1G 98% /
Qué significa: Discos casi llenos destruyen rendimiento y pueden romper escrituras de BD. Postgres puede negarse a checkpoint; MySQL puede fallar o quedarse en solo-lectura según circunstancias.
Decisión: Libera espacio ahora. Luego configura rotación de logs y una alerta de monitorización en 80–85%.
Tarea 14: Revisa memoria de kernel y procesos DB
cr0x@server:~$ ps -eo pid,comm,rss,pmem --sort=-rss | head
2311 mysqld 1854320 46.2
1822 php-fpm8.1 412000 10.2
1825 php-fpm8.1 405000 10.1
911 postgres 220000 5.4
Qué significa: RSS muestra memoria residente real. Unos pocos workers grandes de PHP más un caché grande de BD pueden empujar la máquina al swap.
Decisión: Si DB + PHP ya consumen la mayor parte de la RAM, deja de aumentar buffers del DB. Reduce la concurrencia y limita los consumidores de memoria.
Si eliges MySQL: qué configurar primero en un VPS de 4GB
MySQL en hardware de VPS pequeño suele funcionar si no lo tratas como un pozo sin fondo para conexiones y memoria. InnoDB es tu motor por defecto; ajusta para InnoDB, no por nostalgia.
1) Define innodb_buffer_pool_size como una persona adulta
Objetivo: Cachear datos/índices calientes, reducir lecturas aleatorias, evitar dejar sin memoria al resto.
- Si la BD está en la misma máquina que el web: comienza alrededor de 1.0–1.5GB.
- Si la BD está mayormente sola: hasta 2.0–2.5GB puede funcionar.
Modo de fallo: Sobredimensionar el buffer pool no “usa memoria libre”. Compite con la caché de página del SO y la capa web. Luego haces swap. Entonces cada consulta se vuelve un benchmark de almacenamiento.
2) Establece max_connections más bajo de lo que piensas
Los hilos de MySQL consumen memoria. A las apps PHP les encanta abrir conexiones como si fueran gratis. No lo son.
- Empieza alrededor de 100–200 dependiendo de la app y latencia de consultas.
- Si ves 300–800 conexiones, no tienes un “problema de rendimiento de BD”. Tienes un problema de control de concurrencia.
3) Mantén el redo log y el comportamiento de flush sensatos
En un VPS pequeño con latencia de almacenamiento incierta, los flushs agresivos pueden causar picos. Pero convertir la durabilidad en sugerencia es como actualizar tu currículum.
innodb_flush_log_at_trx_commit=1para durabilidad real (por defecto).- Si absolutamente debes reducir presión de fsync y puedes aceptar perder hasta 1 segundo de transacciones en un crash: considera
=2. Documenta esto. Ponlo en runbooks de incidentes. No finjas que es gratis.
4) Deshabilita lo que no necesitas, pero no te quedes ciego
Performance Schema es útil; también tiene coste. En un VPS tiny puedes reducir instrumentación en lugar de eliminarla por completo.
- Si estás constantemente CPU-bound con latencia de consultas baja, considera recortar consumidores de Performance Schema.
- Pero mantén visibilidad suficiente para detectar regresiones. Depurar sin métricas es solo escritura creativa.
5) Ajusta límites de tablas temporales con cuidado
Las apps web adoran ORDER BY y GROUP BY, a menudo con conjuntos de resultados demasiado amplios.
tmp_table_sizeymax_heap_table_sizepueden reducir tablas temporales en disco, pero ponerlos demasiado altos y reventarás memoria bajo concurrencia.
Boceto de configuración inicial para MySQL (no una religión copy-paste)
Este es el espíritu para un VPS mixto web+DB de 4GB. Ajusta según las mediciones anteriores.
cr0x@server:~$ sudo cat /etc/mysql/mysql.conf.d/99-vps-tuning.cnf
[mysqld]
innodb_buffer_pool_size = 1G
innodb_buffer_pool_instances = 1
max_connections = 150
innodb_flush_log_at_trx_commit = 1
innodb_flush_method = O_DIRECT
slow_query_log = ON
long_query_time = 0.5
Qué significa: Buffer pool más pequeño para preservar margen, conexiones limitadas, I/O directo para reducir doble caching (depende de tu sistema de ficheros y carga), y logging de consultas lentas para evidencias.
Decisión: Aplica, reinicia en una ventana tranquila y vuelve a revisar swap/iowait y slow logs. Si la latencia mejora y el swap desaparece, vas por buen camino.
Si eliges PostgreSQL: qué configurar primero en un VPS de 4GB
Postgres es excelente para sitios web, pero te obliga a prestar atención a tres cosas desde el principio: conteo de conexiones, vacuum y WAL/checkpoints. Ignorar cualquiera de ellas y tendrás ralentizaciones “aleatorias” que en realidad no lo son.
1) Instala pooling de conexiones (pgBouncer) antes de “necesitarlo”
En 4GB, los backends de Postgres no son descartables. Un pico de tráfico que abre cientos de conexiones puede convertirse en presión de memoria y sobrecarga de cambio de contexto.
Haz: ejecuta pgBouncer en modo transaction pooling para cargas web típicas.
No hagas: subir max_connections a 500 y llamarlo escalado.
2) Ajusta shared_buffers con prudencia
La regla de folklore dice “25% de la RAM”. En un VPS mixto web+DB, empezaría alrededor de:
- 512MB a 1GB para
shared_buffers.
Postgres también se beneficia de la caché de página del SO. Darle todo a shared_buffers puede dejar sin recursos al SO y a otros procesos.
3) Mantén work_mem bajo globalmente; súbelo quirúrgicamente
work_mem es por operación de sort/hash, por consulta, por backend. No tienes RAM suficiente para valentías aquí.
- Comienza en 4–16MB globalmente dependiendo de la concurrencia.
- Aumenta para un rol o sesión específica si tienes una consulta pesada conocida.
4) Mantén autovacuum saludable
Autovacuum no es limpieza opcional. Es cómo Postgres evita bloat de tablas e índices y mantiene posibles los index-only scans.
- Monitorea tuples muertas y retraso de vacuum.
- Ajusta umbrales de autovacuum por tabla caliente si es necesario.
5) Haz checkpoints menos puntuales
En almacenamiento lento de VPS, los picos de checkpoint aparecen como acantilados de latencia aleatorios. Checkpoints más suaves reducen el dolor.
- Aumenta
checkpoint_timeout(con moderación). - Fija
checkpoint_completion_targetalto para repartir escrituras.
Boceto de configuración inicial para Postgres
cr0x@server:~$ sudo cat /etc/postgresql/16/main/conf.d/99-vps-tuning.conf
shared_buffers = 768MB
effective_cache_size = 2304MB
work_mem = 8MB
maintenance_work_mem = 128MB
checkpoint_completion_target = 0.9
checkpoint_timeout = 10min
wal_compression = on
log_min_duration_statement = 500ms
Qué significa: shared_buffers conservador, hint realista de cache, work_mem modesto, checkpoints más suaves y logging de consultas lentas.
Decisión: Aplica y recarga/reinicia, luego observa crecimiento de archivos temporales y tiempos de checkpoint. Si tu disco es lento, el suavizado de checkpoints se verá como menos acantilados de latencia.
Conexiones: el asesino silencioso en máquinas pequeñas
Si ejecutas sitios web, la manera más fácil de arruinar una base de datos es dejar que la aplicación decida la concurrencia. PHP-FPM workers + “abrir una conexión DB por petición” se convierte en una manada atronadora. En 4GB no sobrevives siendo más rápido. Sobrevives siendo más tranquilo.
Cómo se ve “demasiadas conexiones”
- CPU de la BD alta pero sin trabajo útil (cambio de contexto, contención de mutex).
- Uso de memoria que crece con el tráfico hasta el swap.
- Latencia que aumenta incluso para consultas simples.
Qué hacer en su lugar
- Limita la concurrencia de la app: menos hijos PHP-FPM, o configura el process manager para evitar explosiones.
- Usa pooling: pgBouncer para Postgres; para MySQL considera pooling a nivel de aplicación o asegúrate de que las conexiones persistentes estén configuradas con sentido.
- Falla rápido: a veces reducir
max_connectionses lo correcto porque protege la máquina de un thrash total.
Chiste #2: Conexiones ilimitadas es como un buffet ilimitado de camarones—suena genial hasta que te das cuenta que eres quien cierra el restaurante.
Almacenamiento y realidades del sistema de ficheros: IOPS, fsync y por qué “SSD rápido” miente
En plataformas VPS, “almacenamiento SSD” puede significar cualquier cosa, desde NVMe respetable hasta un dispositivo de bloque en red compartido teniendo un mal día. A las bases de datos les importa más la latencia que el throughput. Unos pocos milisegundos extra por fsync por commit se hacen visibles en el sitio.
Cómo las escrituras te lastiman de forma distinta en MySQL vs PostgreSQL
- MySQL/InnoDB: redo logging + doublewrite buffer (según configuración/version) + flushing de páginas sucias. Flushs en ráfaga pueden amplificar la latencia.
- PostgreSQL: WAL writes + checkpoints + background writer. Vacuum también genera I/O, y el bloat aumenta el I/O futuro.
Mejor práctica en VPS pequeño: reduce primero la amplificación de escrituras
- Arregla apps chatty (demasiadas transacciones pequeñas).
- Batch escrituras donde la consistencia lo permita.
- Evita actualizar constantemente columnas “last_seen” en cada petición si no lo necesitas.
- Mantén índices ligeros; cada índice es un impuesto de escritura.
Trampas del sistema de ficheros
- No pongas bases de datos en sistemas de ficheros de red inestables a menos que conozcas las garantías de durabilidad de la plataforma.
- Atento a condiciones de disco lleno: Postgres y MySQL se comportan mal de diferentes formas, pero ninguna es “agradable”.
Tres mini-historias corporativas desde las trincheras
1) El incidente causado por una suposición errónea: “La caché lo cubrirá”
Un equipo pequeño ejecutaba varios sitios de marketing y un servicio de checkout en un único VPS de 4GB. Tenía MySQL, Nginx y PHP-FPM. El tráfico era “principalmente estático”, lo que era cierto hasta que lanzaron una campaña y el servicio de checkout empezó a recibir ráfagas de peticiones autenticadas.
La suposición fue que la caché de páginas y la caché de la app cubrirían las lecturas, así que subieron innodb_buffer_pool_size cerca de 3GB para “hacer la base de datos rápida”. Se veía bien en una hora tranquila. Luego llegó la campaña.
PHP-FPM lanzó más procesos para manejar el tráfico. Cada worker usó más memoria de la que recuerdaba nadie. El SO empezó a swapear. El buffer pool de la base de datos era enorme, así que el kernel tenía menos espacio para todo lo demás. La latencia no aumentó gradualmente; se desplomó. El endpoint de checkout empezó a agotar tiempos, los reintentos aumentaron el tráfico, y la tormenta de reintentos convirtió un problema de recursos en un denial-of-service auto-hospedado.
La solución no fue exótica. Redujeron el buffer pool para dejar margen, limitaron PHP-FPM children, bajaron max_connections de MySQL para que el sistema fallara rápido en lugar de hacer thrash, y pusieron una cola explícita delante del checkout. También aprendieron la diferencia operacional entre “memoria libre” y “memoria disponible bajo ráfaga”.
2) La optimización que salió mal: “Solo sube work_mem, está bien”
Una app interna corría en PostgreSQL. Los usuarios se quejaban de reportes lentos, así que alguien aumentó significativamente work_mem porque un post decía que reduciría I/O a archivos temporales. Lo hizo. Para un usuario. En una sesión.
Luego vino un lunes. Varios usuarios ejecutaron reportes concurrentes. Esos reportes hicieron múltiples sorts y hash joins. Postgres asignó correctamente work_mem por operación. El uso de memoria se disparó. El VPS no murió de inmediato; se volvió cada vez más lento a medida que el swap entraba en acción. La BD parecía “viva” pero cada consulta esperaba detrás de la tormenta de I/O causada por el swapping.
El equipo revertió work_mem a un valor conservador y en su lugar arregló la consulta del reporte. Añadieron un índice faltante, redujeron columnas seleccionadas y crearon una tabla resumen que se actualizaba periódicamente. Para la consulta realmente pesada usaron un rol con mayor work_mem y la ejecutaron por una ruta de reporting controlada. La lección no fue “nunca tunear”. Fue “no tunear globalmente por un problema local en una máquina pequeña”.
3) La práctica aburrida pero correcta que salvó el día: “Limita conexiones y registra consultas lentas”
Una organización diferente alojaba varios sitios pequeños de clientes en un VPS compartido de 4GB. Nada sofisticado. No buscaban microsegundos. Hicieron tres cosas aburridas desde el día uno: limitaron conexiones de BD, activaron slow query logging con un umbral sensato y monitorizaron el uso de disco con una alerta mucho antes del 90%.
Una tarde una actualización de plugin introdujo una regresión en una consulta. El sitio no se cayó de inmediato porque los límites de conexión impidieron que la carga ilimitada se amontonara en la BD. En su lugar, algunas peticiones fallaron rápido, lo que hizo visible el problema sin fundir la máquina.
El slow query log tenía la evidencia: una consulta que empezó a escanear una tabla grande sin índice útil. Añadieron el índice, resolvieron la regresión y el incidente quedó confinado a una ventana corta. Sin misterio. Sin “se arregló solo”. Sin arqueología de fin de semana.
Esto es lo que parece la fiabilidad aburrida: fallos controlados, recolección de evidencia y suficiente margen para que un deploy malo no se convierta en catástrofe del sistema.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: paradas repentinas de 10–60s en todo el sitio
Causa raíz: picos de latencia de almacenamiento durante checkpoints/flushes o tormentas de swap.
Solución: confirma con iostat y vmstat; reduce presión de memoria (buffers DB más pequeños, menos app workers), suaviza checkpoints (Postgres) y reduce amplificación de escrituras (ambos).
2) Síntoma: CPU de la base de datos alta, consultas “no tan lentas” individualmente
Causa raíz: demasiadas conexiones concurrentes; la sobrecarga de contención domina.
Solución: limita conexiones; añade pooling (pgBouncer); reduce concurrencia de PHP-FPM; cachea en la app o en el reverse proxy; falla rápido en lugar de thrash.
3) Síntoma: Postgres crece y crece; el rendimiento se degrada lentamente
Causa raíz: retraso de vacuum y bloat de tablas/índices por autovacuum insuficiente o transacciones largas.
Solución: identifica sesiones idle-in-transaction, ajusta autovacuum por tabla caliente y deja de mantener transacciones abiertas entre peticiones.
4) Síntoma: MySQL “Waiting for table metadata lock” en processlist
Causa raíz: cambio de esquema o DDL bloqueado por transacciones largas; consultas se encolan detrás de metadata locks.
Solución: programa DDL en ventanas de mantenimiento; mantén transacciones cortas; usa enfoques de cambio de esquema online si es necesario.
5) Síntoma: muchas archivos temporales o “Using temporary; Using filesort” en MySQL
Causa raíz: índices faltantes para patrones ORDER BY/GROUP BY; consultas ordenando datasets enormes.
Solución: añade índices compuestos que coincidan con filter+sort; reduce columnas seleccionadas; pagina correctamente; evita paginación con OFFSET para páginas profundas.
6) Síntoma: errores frecuentes de “too many connections”
Causa raíz: fugas de conexiones en la app, sin pooling o picos en conteo de workers web.
Solución: pool de conexiones; establece timeouts sensatos; limita concurrencia de la app; fija max_connections a un número que puedas permitirte.
7) Síntoma: después de “tunear”, el rendimiento empeoró
Causa raíz: un ajuste global (como work_mem o un buffer pool demasiado grande) aumentó la memoria por conexión y disparó el swap bajo concurrencia.
Solución: revierte; aplica tuning por usuario/consulta; mide memoria y concurrencia explícitamente.
Listas de verificación / plan paso a paso
Paso 0: Decide qué significa “bueno”
- Elige un objetivo estilo SLO: por ejemplo, homepage p95 < 500ms, checkout p95 < 800ms.
- Elige una ventana de medición y captura la línea base (CPU, RAM, swap, iowait, conexiones DB, consultas lentas).
Paso 1: Estabiliza el host
- Asegura que el disco tenga al menos 15–20% de espacio libre.
- Asegura que no estás swappeando bajo tráfico pico normal.
- Configura límites de servicio conservadores (systemd limits si es necesario) para evitar procesos desbocados.
Paso 2: Limita la concurrencia deliberadamente
- Fija PHP-FPM max children a un número que puedas permitir en RAM.
- Fija
max_connectionsde la BD para proteger la máquina. - En Postgres: despliega pgBouncer y reduce conexiones backend.
Paso 3: Ajusta los primeros knobs de memoria
- MySQL: ajusta
innodb_buffer_pool_sizepara que el working set quepa sin dejar sin recursos al SO. - Postgres: ajusta
shared_bufferscon prudencia; mantienework_membajo globalmente.
Paso 4: Activa la recolección de evidencia
- MySQL: slow query log a 0.5–1s durante pico, luego analiza y arregla.
- Postgres:
log_min_duration_statementy idealmentepg_stat_statements.
Paso 5: Arregla los 3 patrones de consulta más importantes
- Añade los índices faltantes que reduzcan scans de filas.
- Elimina N+1 queries en la app.
- Deja de hacer consultas caras por petición; precomputa o cachea.
Paso 6: Re-prueba y establece guardarraíles
- Vuelve a ejecutar tus tareas de triage en pico.
- Añade alertas sobre actividad de swap, utilización de disco, conteos de conexiones y tasa de consultas lentas.
- Documenta tus ajustes “seguros” y la razón para que el tú del futuro no los revierta.
Preguntas frecuentes
1) En un VPS de 4GB, ¿debo priorizar caché de BD o caché de página del SO?
Prioriza estabilidad. Para un single-box web+DB, no dejes al SO sin memoria. Una caché moderada en la BD más margen supera a una caché gigantesca que provoca swap en ráfagas.
2) ¿Es PostgreSQL “más lento” que MySQL para sitios web?
No en general. Para muchas cargas web, cualquiera es suficientemente rápido si está bien indexado. El mayor diferenciador en 4GB es la gestión de conexiones y patrones de escritura, no la velocidad bruta del motor.
3) ¿Cuál es el primer ajuste de MySQL que debería cambiar?
innodb_buffer_pool_size, dimensionado a tu realidad. Luego limita max_connections. Luego habilita slow query logging y arregla lo que muestre.
4) ¿Cuál es el primer ajuste de PostgreSQL que debería cambiar?
Estrategia de pooling de conexiones (pgBouncer) y max_connections. Luego shared_buffers conservador y logging/pg_stat_statements para identificar las consultas principales.
5) ¿Puedo simplemente aumentar swap para resolver problemas de memoria?
Puedes aumentar swap para evitar crashes abruptos por OOM, pero swap no es RAM de rendimiento. Si tu base de datos o workers PHP golpean regularmente el swap, la latencia será impredecible.
6) ¿Debería desactivar fsync para ganar velocidad?
No en producción si te importa la integridad de datos. Si desactivas durabilidad y el host cae, puedes perder datos. A los benchmarks les encanta; a los clientes no.
7) ¿Cómo sé si estoy limitado por I/O?
Alto iowait en vmstat, alto await y %util en iostat, y sesiones DB esperando en eventos de IO (Postgres) son señales fuertes.
8) ¿Cuándo debería separar web y BD en servidores distintos?
Cuando tus cambios de tuning se convierten en compensaciones entre la capa web y la BD, o cuando la latencia de almacenamiento hace impredecibles las escrituras de BD. Separar te da aislamiento y planificación de capacidad más clara.
9) ¿Los valores por defecto son suficientes hoy en día?
Los defaults son mejores que antes, pero no están adaptados a tu situación de 4GB “todo en una caja”. Los topes de conexiones y la presupuestación de memoria siguen siendo tu responsabilidad.
10) ¿Cuál es la mejora de rendimiento más segura que puedo hacer sin mucha experiencia en BD?
Habilita slow query logging (o pg_stat_statements), identifica los 3 consumidores de tiempo principales y añade los índices correctos. También limita conexiones para que el servidor permanezca estable bajo carga.
Siguientes pasos que no te avergonzarán después
En un VPS de 4GB no estás optimizando una base de datos. Estás gestionando la contención entre web, base de datos y almacenamiento tratando de mantener la latencia aburrida.
- Ejecuta el guion de diagnóstico rápido durante pico y anota qué está pasando realmente: swap, iowait, conexiones, locks, consultas top.
- Limita la concurrencia primero: workers PHP-FPM y conexiones DB. Añade pgBouncer si usas Postgres.
- Fija el primer knob de memoria (InnoDB buffer pool o shared_buffers de Postgres) a un valor conservador que deje margen.
- Activa evidencia (slow query logs / pg_stat_statements) y arregla los mayores offenders con índices y cambios de consulta.
- Revisa disco y comportamiento de escrituras; suaviza checkpoints, reduce derrames a temp y deja de hacer escrituras ruidosas innecesarias.
- Decide si la solución real es arquitectónica: mover la BD a un VPS separado, mejorar el tier de almacenamiento o usar una BD gestionada. A veces el parámetro de tuning más efectivo es tu factura.
Si haces solo una cosa hoy: limita conexiones y deja de swapear. Todo lo demás es adorno.