No te «falta base de datos». Te falta algo aburrido: RAM, IOPS, descriptores de archivos, tiempo de CPU o paciencia.
En un VPS de 8GB, el margen entre «funciona» y «¿por qué el pago caduca?» es más delgado que el sarcasmo del canal de incidentes.
Esta es una guía práctica para escalar clientes de forma segura en MariaDB y PostgreSQL cuando no puedes simplemente tirar hardware al problema.
Hablaremos de límites de conexiones, matemáticas de memoria, la realidad del I/O, las trampas que solo aparecen bajo carga y las prácticas que te mantienen fuera de problemas.
Enmarcar el problema: qué rompe realmente “escalar clientes”
“Escalar clientes” generalmente significa “aumentar el número de conexiones concurrentes o solicitudes”. Suena como un problema de red.
No lo es. Es un problema de contabilidad de recursos.
El patrón típico de fallo en un VPS de 8GB es así:
- El tráfico aumenta, los servidores de la aplicación abren más conexiones a la BD “por si acaso”.
- La BD las acepta (porque los valores por defecto son generosos o subiste
max_connectionsa las apuradas). - La memoria sube por conexión/sesión. El cambio de contexto aumenta. Las cachés se ven comprimidas.
- El I/O se dispara porque el working set ya no cabe en RAM.
- La latencia sube; los timeouts de la app provocan reintentos; los reintentos generan más conexiones; te inventas un ataque de denegación de servicio contra ti mismo.
En un VPS hay otra vuelta de tuerca: steal de CPU, vecinos ruidosos, créditos de burst y almacenamiento que “parece SSD” pero se comporta como una triste memoria USB en el peor momento.
Una cita que vale pegar cerca de tu panel: La esperanza no es una estrategia.
— General Gordon R. Sullivan.
No es una cita de bases de datos, pero es la mejor frase de ingeniería para el escalado de bases de datos.
La pregunta central no es “¿MariaDB o PostgreSQL es más rápido?” Es:
¿cuál te permitirá imponer límites sensatos y comportamiento predecible al aumentar clientes, con RAM limitada y discos del mundo real?
Hechos e historia que importan en producción
Esto no son datos para un trivial. Explican por qué los valores por defecto, comportamientos y herramientas operativas son como son.
-
El modelo de procesos de PostgreSQL es históricamente “un backend por conexión”.
Ese diseño facilita la aislamiento y la depuración, pero te empuja hacia el uso de poolers conforme sube la concurrencia. -
El enfoque de hilo por conexión de MySQL marcó la historia de concurrencia de MariaDB.
Puede manejar muchas conexiones, pero la memoria por sesión aún puede aplastar una máquina de 8GB si lo dejas. -
MariaDB nació tras la adquisición de MySQL por Oracle (era 2009–2010).
Ese fork creó dos ecosistemas y dos conjuntos de supuestos operativos que aún difieren hoy. -
InnoDB se convirtió en el motor por defecto mucho después de la era MyISAM.
Muchos posts de «tuning de MySQL» son fósiles de la era MyISAM y son activamente dañinos en cargas modernas con InnoDB. -
MVCC de PostgreSQL tiene “bloat” como realidad operativa.
Vacuuming no es opcional si actualizas/eliminás mucho; es la recolección de basura para tus tablas. -
InnoDB de MariaDB también tiene MVCC y purge, pero las palancas operativas son distintas.
La historia importa: las perillas no son una a una y los síntomas confunden a quienes migran entre ellas. -
El WAL de PostgreSQL y los redo/binlogs de MariaDB convierten escrituras aleatorias en I/O secuencial-ish.
Pero el comportamiento de fsync, los checkpoints y las «tormentas de flush» varían por motor y configuración. -
PostgreSQL introdujo grandes mejoras de consultas paralelas en años recientes.
Eso ayuda en analítica, pero puede sorprenderte en cajas pequeñas si los workers paralelos comen CPU y memoria durante picos OLTP. -
El thread pool de MariaDB existe precisamente porque hilo-por-conexión tiene límites de escalado.
Es una de las pocas perillas que “actívala y valida” que puede cambiar genuinamente el comportamiento de concurrencia.
Chiste #1: Una base de datos siempre aceptará tu cambio max_connections=2000. Es como un niño aceptando un tercer espresso.
Cómo MariaDB y PostgreSQL gastan tus 8GB
El presupuesto de 8GB: qué tienes realmente
“8GB RAM” es marketing. En producción reservas espacio para el kernel, caché del sistema de archivos, servicios en segundo plano y el hecho de que el uso de memoria aumenta bajo carga.
Si ejecutas solo la BD, podrías asignar de forma segura ~5–6GB a los ajustes de memoria de la base de datos.
Si ejecutas app + BD en el mismo VPS (no lo hagas si puedes evitarlo), córtalo drásticamente.
También estás comprando I/O, no solo RAM. La mayoría de los errores de latencia atribuidos a “rendimiento de la BD” son en realidad rendimiento de almacenamiento y encolamiento.
PostgreSQL: predecible, pero con muchas conexiones
La memoria de PostgreSQL se divide ampliamente en:
- Memoria compartida: principalmente
shared_buffers. - Memoria por sesión / por operación:
work_mem,maintenance_work_mem, ordenamientos, hashes, buffers temporales. - Caché del sistema operativo: PostgreSQL depende fuertemente de ella para el rendimiento real.
El problema clave en cajas pequeñas no es shared_buffers. Es la multiplicación del overhead por conexión más las asignaciones de memoria por consulta.
Si permites cientos de conexiones directas desde las apps, apuestas tu disponibilidad a “no harán consultas costosas al mismo tiempo.”
Lo harán. Un lunes. Durante la nómina. O cuando alguien despliegue un nuevo informe.
MariaDB (InnoDB): buffer pool y sorpresas por hilo
Para MariaDB (cargas InnoDB), la memoria está típicamente dominada por:
- InnoDB buffer pool: tu caché principal (
innodb_buffer_pool_size). - Buffers por conexión: sort buffer, join buffer, tmp tables, net buffers.
- Caches de hilos e internas: especialmente visibles cuando la rotación de conexiones es alta.
MariaDB puede verse “bien” hasta que una consulta obliga a grandes buffers de sort/join en muchos hilos y tomas un desvío repentino hacia swapping.
El swap en un disco de VPS es un generador de outages en cámara lenta.
Escalar clientes de forma segura es, en su mayoría, controlar la concurrencia
Ambas bases de datos pueden manejar un throughput serio en 8GB si haces una cosa: limitar la concurrencia y suavizar la carga.
Pooling de conexiones, límites sensatos y disciplina de consultas vencen al tuning heroico cada vez.
Guion de diagnóstico rápido (primero/segundo/tercero)
Cuando la latencia se dispara, necesitas un bucle de triaje rápido que te diga dónde está el límite: CPU, memoria, locks o I/O.
No “tunées”. Diagnostica.
Primero: ¿está enfermo el host?
- Load average vs número de CPUs: si la carga es alta y la CPU está ociosa, estás esperando I/O.
- Actividad de swap: cualquier swap-in/out sostenido durante el pico significa que estás perdiendo.
- Latencia del disco: si
awaites alto, la BD es inocente; el almacenamiento es la escena del crimen.
Segundo: ¿la BD está encolada o bloqueada?
- PostgreSQL: revisa
pg_stat_activitypor eventos de espera y consultas bloqueadas. - MariaDB: revisa processlist y el estado de InnoDB por esperas de bloqueo y deadlocks.
- Tormentas de conexiones: timeouts + reintentos producen carga autoinfligida. Busca picos repentinos en el conteo de conexiones.
Tercero: ¿es una consulta mala o tuning sistémico?
- Slow query logs (ambos) muestran si pocas consultas dominan el tiempo.
- Tasas de aciertos de caché te dicen si estás limitado por I/O por falta de caché o índices pobres.
- Autovacuum / purge aparecen como tablas/índices crecientes, I/O en aumento y “lentitud aleatoria”.
Este orden importa porque la solución más rápida a menudo está fuera de la base de datos: reduce la concurrencia de clientes, detén reintentos o limita jobs en background.
12+ tareas prácticas con comandos, qué significa la salida, decisiones
Estos son los chequeos que ejecuto en un VPS de 8GB antes de tocar parámetros de tuning. Cada uno incluye: comando, qué significa la salida y qué decisión impulsa.
Task 1: Confirmar memoria real y presión de swap
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 7.7Gi 5.9Gi 220Mi 120Mi 1.6Gi 1.3Gi
Swap: 2.0Gi 1.1Gi 900Mi
Significado: “available” es lo que aún puedes usar sin hacer swap. Swap ya en uso es una advertencia; swap moviéndose activamente es la alarma real.
Si el uso de swap crece durante la carga, la BD se detendrá.
Decisión: Si el swap es no trivial bajo carga, reduce el número de conexiones, baja ajustes de memoria por sesión o añade RAM antes de “optimizar consultas”.
Task 2: Revisar swap activo (la olla a presión de outages)
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 118000 210000 65000 980000 120 240 3100 4200 900 2100 18 6 32 44 0
3 2 119200 190000 64000 970000 300 500 2800 3900 1000 2500 20 7 25 48 0
Significado: si/so por encima de ~0 sostenido significa que estás haciendo swap. wa alto significa que la CPU espera I/O.
Decisión: Detén la hemorragia: limita la concurrencia (pool), baja multiplicadores de memoria DB, desactiva informes costosos en picos o muévete a almacenamiento más rápido.
Task 3: Medir latencia de disco y encolamiento
cr0x@server:~$ iostat -xz 1 3
Linux 6.8.0 (server) 12/31/2025 _x86_64_ (2 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
14.20 0.00 6.50 28.10 0.80 50.40
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %util await
vda 85.0 120.0 4200.0 9800.0 0.0 2.0 96.0 18.5
Significado: %util cerca de 100% con await en dos dígitos significa que el disco está saturado.
En muchas plataformas VPS, ese es tu cuello de botella ahí mismo.
Decisión: Arregla el I/O: reduce la amplificación de escrituras (índices, tuning de autovacuum/purge), mueve logs a almacenamiento más rápido o mejora el tier de disco. No aumentes caches a ciegas.
Task 4: Revisar espacio en filesystem y presión de inodos
cr0x@server:~$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 80G 62G 15G 81% /
tmpfs 3.9G 0 3.9G 0% /dev/shm
cr0x@server:~$ df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/vda1 5242880 220000 5022880 5% /
Significado: Discos casi llenos perjudican el rendimiento de la BD y pueden detener escrituras (WAL/redo/binlog no pueden crecer).
Decisión: Mantén margen libre (me gusta 20%+ en sistemas ocupados). Si estás ajustado, estás a un incidente de sorpresa «solo lectura».
Task 5: Revisar límites de descriptores de archivo (asesino silencioso de conexiones)
cr0x@server:~$ ulimit -n
1024
Significado: 1024 es bajo para una base de datos que puede abrir muchos ficheros (tablas, índices, segmentos WAL) además de sockets.
Decisión: Eleva límites vía unidad systemd o limits.conf. Si la BD alcanza límites de FD, los clientes verán fallos de conexión que parecen “aleatorios”.
Task 6 (PostgreSQL): Contar conexiones y ver quién las acapara
cr0x@server:~$ sudo -u postgres psql -X -c "SELECT state, count(*) FROM pg_stat_activity GROUP BY 1 ORDER BY 2 DESC;"
state | count
---------+-------
idle | 140
active | 12
| 3
(3 rows)
Significado: Muchas conexiones idle suelen indicar que la app las acapara. Eso es overhead de memoria y procesos haciendo nada.
Decisión: Usa un pooler (PgBouncer) y reduce conexiones directas. Fija tamaños de pool sensatos en la app; no dejes que cada pod abra 50 sesiones “porque son los valores por defecto”.
Task 7 (PostgreSQL): Encontrar consultas bloqueadas y cadenas de locks
cr0x@server:~$ sudo -u postgres psql -X -c "SELECT pid, wait_event_type, wait_event, state, left(query,80) AS q FROM pg_stat_activity WHERE wait_event IS NOT NULL ORDER BY pid;"
pid | wait_event_type | wait_event | state | q
------+-----------------+---------------+---------+----------------------------------------------
2314 | Lock | relation | active | UPDATE orders SET status='paid' WHERE id=$1
2451 | Lock | transactionid | active | DELETE FROM carts WHERE user_id=$1
(2 rows)
Significado: Esperar por locks significa que tu concurrencia se está peleando a sí misma. Añadir conexiones lo empeora.
Decisión: Identifica la transacción bloqueante (a menudo de larga duración o idle-in-transaction). Arregla el alcance de las transacciones en la app; agrega índices para reducir tiempo de lock; programa migraciones de forma segura.
Task 8 (PostgreSQL): Comprobar efectividad de caché (¿estás limitado por I/O?)
cr0x@server:~$ sudo -u postgres psql -X -c "SELECT datname, blks_hit, blks_read, round(100.0*blks_hit/(blks_hit+blks_read+1),2) AS hit_pct FROM pg_stat_database ORDER BY hit_pct ASC;"
datname | blks_hit | blks_read | hit_pct
-----------+----------+-----------+---------
appdb | 89213321 | 5120032 | 94.55
postgres | 1200032 | 12033 | 98.99
(2 rows)
Significado: 94–95% puede ser aceptable para algunas cargas, terrible para otras. Si hit% baja durante la carga y sube la latencia del disco, estás falto de caché o tu working set creció.
Decisión: Añade RAM o reduce el working set (mejores índices, menos full scans). No pongas shared_buffers a 6GB y dejes al OS sin caché.
Task 9 (PostgreSQL): Ver presión de vacuum (impuesto por bloat)
cr0x@server:~$ sudo -u postgres psql -X -c "SELECT relname, n_dead_tup, n_live_tup, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 5;"
relname | n_dead_tup | n_live_tup | last_autovacuum
-------------+------------+------------+----------------------------
events | 8200032 | 41000211 | 2025-12-31 10:22:14+00
sessions | 1200033 | 9000123 | 2025-12-31 10:18:01+00
carts | 450012 | 1500032 | 2025-12-31 09:59:44+00
(3 rows)
Significado: Tuplas muertas acumulándose significa más lecturas de disco, índices más grandes, consultas más lentas. Autovacuum puede estar subdimensionado o bloqueado por transacciones largas.
Decisión: Arregla transacciones largas, ajusta autovacuum por tabla y considera particionar tablas con mucho append.
Task 10 (MariaDB): Revisar conexiones actuales y uso máximo
cr0x@server:~$ sudo mariadb -e "SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Max_used_connections'; SHOW VARIABLES LIKE 'max_connections';"
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_connected | 210 |
+-------------------+-------+
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| Max_used_connections | 398 |
+----------------------+-------+
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 500 |
+-----------------+-------+
Significado: Ya alcanzaste 398 conexiones. Eso no es “capacidad”, es “estás coqueteando con el colapso por memoria”.
Decisión: Implementa pooling en la app o usa el thread pool de MariaDB; reduce max_connections a un número seguro; aplica timeouts y mata sleepers inactivos.
Task 11 (MariaDB): Detectar conexiones durmientes y procesos largos
cr0x@server:~$ sudo mariadb -e "SHOW PROCESSLIST;"
Id User Host db Command Time State Info
1203 app 10.0.1.12:44218 appdb Sleep 512 NULL
1210 app 10.0.1.12:44276 appdb Query 9 Sending data SELECT * FROM events WHERE user_id=?
1311 app 10.0.1.13:51022 appdb Sleep 611 NULL
Significado: Muchas conexiones en Sleep con Time alto son acaparamiento de conexiones. El estado Sending data a menudo significa escaneo y retorno de filas (o espera por disco).
Decisión: Reduce tamaños de pool de la app, añade límites de vida útil de conexiones, habilita slow query log y arregla índices faltantes.
Task 12 (MariaDB): Revisar tamaño y presión del InnoDB buffer pool
cr0x@server:~$ sudo mariadb -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';"
+-------------------------+------------+
| Variable_name | Value |
+-------------------------+------------+
| innodb_buffer_pool_size | 4294967296 |
+-------------------------+------------+
+--------------------------+---------+
| Variable_name | Value |
+--------------------------+---------+
| Innodb_buffer_pool_reads | 8200332 |
+--------------------------+---------+
+----------------------------------+------------+
| Variable_name | Value |
+----------------------------------+------------+
| Innodb_buffer_pool_read_requests | 932003312 |
+----------------------------------+------------+
Significado: La proporción de lecturas desde disco vs solicitudes de lectura sugiere misses en caché. En un VPS de 8GB, 4GB de buffer pool puede ser razonable, pero no si los buffers por hilo son enormes.
Decisión: Mantén el buffer pool sustancial (a menudo 50–70% de la RAM si solo corre la BD), pero audita primero los buffers por conexión y el conteo de conexiones.
Task 13 (MariaDB): Capturar spills de tablas temporales (martillo silencioso de disco)
cr0x@server:~$ sudo mariadb -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%tables';"
+-------------------------+----------+
| Variable_name | Value |
+-------------------------+----------+
| Created_tmp_disk_tables | 2200033 |
| Created_tmp_tables | 3100044 |
+-------------------------+----------+
Significado: Una alta proporción de tmp tables en disco indica consultas que ordenan/agrupar en datasets grandes sin índices adecuados, o límites de tmp table demasiado pequeños.
Decisión: Arregla consultas/índices primero. Solo después considera ajustar tmp_table_size/max_heap_table_size, porque aumentarlos eleva el riesgo de memoria por sesión.
Task 14: Confirmar backlog de sockets de red y falsos SYN flood
cr0x@server:~$ ss -s
Total: 1180
TCP: 942 (estab 410, closed 410, orphaned 0, timewait 110)
Transport Total IP IPv6
RAW 0 0 0
UDP 8 6 2
TCP 532 410 122
INET 540 416 124
FRAG 0 0 0
Significado: Si las conexiones establecidas se disparan y timewait crece rápido, tu app podría estar reconectando agresivamente o un balanceador está fallando.
Decisión: Arregla el comportamiento del cliente (pool, keepalive, timeouts). Las bases de datos odian la churn de conexiones casi tanto como a los humanos les disgusta una llamada inesperada.
Task 15: Confirmar tiempo de CPU steal (realidad VPS)
cr0x@server:~$ mpstat 1 3
Linux 6.8.0 (server) 12/31/2025 _x86_64_ (2 CPU)
12:00:01 AM all %usr %nice %sys %iowait %irq %soft %steal %idle
12:00:02 AM all 22.00 0.00 10.00 18.00 0.00 1.00 6.00 43.00
12:00:03 AM all 20.00 0.00 9.00 19.00 0.00 1.00 7.00 44.00
Significado: %steal indica que el hipervisor te está quitando CPU. Si steal sube durante picos, la “lentitud” de tu BD es literalmente carga de otra persona.
Decisión: Considera una clase de VPS mejor o CPU dedicada. El tuning no arregla ciclos robados.
Patrones seguros para escalar más clientes
Patrón 1: El pooling de conexiones no es opcional en 8GB
Si ejecutas PostgreSQL y esperas muchos clientes concurrentes, pon un pooler delante. Punto.
Incluso si “solo” tienes 100 instancias de app. Especialmente entonces.
Para PostgreSQL, el pooling externo (PgBouncer) es la jugada estándar porque reduce procesos backend mientras mantiene concurrencia cliente.
Para MariaDB, muchos stacks usan pooling a nivel de aplicación; el thread pool de MariaDB puede ayudar, pero no es gratuito.
El modelo mental seguro: los clientes pueden ser numerosos, la concurrencia del servidor debe ser pequeña y controlada.
Quieres una cola que controlas, no una estampida de recursos que controle el kernel.
Patrón 2: Fija topes duros y hazlos cumplir
“Ilimitado” es la forma de provocar una falla en cascada. Limita:
- Conexiones de base de datos (
max_connections/max_connections). - Conexiones por usuario o por app (roles, usuarios, reglas de firewall, ajustes de pooler).
- Tiempo de sentencia (PostgreSQL
statement_timeout, MariaDBmax_execution_timepara SELECT, además de timeouts de aplicación). - Idle-in-transaction (PostgreSQL
idle_in_transaction_session_timeoutes salvavidas).
Patrón 3: Protege la caché manteniendo pequeños los multiplicadores de memoria
El error de tuning más común en 8GB es gastar toda la RAM en “memoria de la base de datos” olvidando que:
los buffers por conexión se multiplican, y la caché del SO no es “memoria desperdiciada”.
PostgreSQL: si pones work_mem alto y permites muchas sesiones activas, puedes asignar mucha más memoria de la que posees.
MariaDB: si inflas sort/join buffers y permites muchos hilos, sucede lo mismo, solo con una contabilidad menos obvia.
Patrón 4: Haz las escrituras aburridas
En discos pequeños de VPS, las escrituras causan la mayor parte del dolor:
fsyncs de WAL/redo, checkpoints, flush en background, tablas temporales, vacuum, mantenimiento de índices.
Tu objetivo es comportamiento de escritura estable, no “rápido hasta que se detiene”.
- Mantén transacciones cortas.
- Agrupa jobs en batch fuera de pico o regúlalos.
- No crees tres índices nuevos al mediodía en una tabla ocupada.
- Usa ajustes de durabilidad sensatos; no desactives fsync a menos que realmente quieras practicar pérdida de datos.
Patrón 5: Escala lecturas y escrituras de forma diferente
En un único VPS de 8GB, el escalado vertical es limitado. Así que escalas la forma:
- Lecturas: caching, réplicas de lectura, optimización de consultas, indexación, modelos de lectura desnormalizados donde sea necesario.
- Escrituras: reduce la amplificación de escrituras, particiona tablas con mucha rotación, archiva datos antiguos, evita “actualizar cada fila”.
PostgreSQL y MariaDB ambos pueden replicar. Los compromisos operativos difieren, pero el principio se mantiene: no esperes que una sola caja haga trabajo infinito.
Patrón 6: Observa la base de datos desde afuera
Las métricas internas son buenas. Los síntomas externos son mejores para el triaje:
- p95/p99 de latencia de consultas
- await y utilización del disco
- actividad de swap
- tasa de nuevas conexiones (conexiones nuevas/seg)
- comportamiento de checkpoints/flush
Si no puedes verlo, “resolverás” el problema equivocado. Así es como se repiten outages con mejores dashboards.
Chiste #2: Apagar la durabilidad para “mejorar el rendimiento” es como quitarte los frenos para ganar una carrera. Llegarás más rápido, por un momento.
Tres microhistorias desde la vida corporativa
Microhistoria 1: El incidente causado por una suposición errónea (conexiones PostgreSQL)
Un equipo SaaS de tamaño medio migró de una base administrada a una instancia self-hosted de PostgreSQL en un VPS de 8GB para ahorrar costos.
La migración fue fluida. La primera semana estuvo tranquila. Todos se felicitaron y volvieron a lanzar funcionalidades.
Luego incorporaron un cliente grande. El tráfico se duplicó. La latencia fue subiendo, pero solo durante horas laborales. El equipo supuso que eran “consultas más pesadas”
y empezaron a perseguir índices. Ayudó un poco. Entonces vino el incidente real: la app empezó a lanzar 500 intermitentes y el gráfico de CPU de la BD se veía extrañamente tranquilo.
La suposición errónea fue sutil: pensaron “PostgreSQL puede manejar muchas conexiones como nuestro sistema anterior”.
Su capa de apps usaba un pool por defecto de 50 por instancia. Con unas pocas instancias ya era alto. Con el autoescalado explotó.
La máquina corrió cientos de procesos backend, consumió memoria, forzó al SO a reclamar caché y convirtió cada miss en caché en una fiesta de espera en I/O.
La solución no fue un índice ingenioso. Fue aburrida: reducir tamaños de pool de la app, desplegar PgBouncer, limitar las conexiones máximas del servidor y establecer timeouts para que las sesiones inactivas no persistieran.
También ajustaron el comportamiento de reintentos para evitar un bucle de retroalimentación.
Después de eso, el tuning de consultas realmente funcionó—porque el sistema ya no se autodestruía.
La lección quedó: en máquinas pequeñas, la gestión de conexiones es gestión de rendimiento.
Microhistoria 2: La optimización que salió mal (perillas de memoria en MariaDB)
Otra compañía ejecutaba MariaDB para un dashboard interno de analítica en un VM de 8GB. Era principalmente lecturas con cargas por lotes periódicas.
Alguien leyó una guía de tuning antigua y decidió que “buffers más grandes siempre son mejores”. Aumentaron buffers de sort y join por hilo y los límites de tablas temporales.
En un sistema calmado, se vio genial. Consultas que antes hacían spill a disco se volvieron más rápidas. El dashboard se sintió ágil.
El cambio se promovió a producción sin pruebas de carga porque el sistema “no era crítico”. Esa frase envejece mal.
Cuando la siguiente carga por lotes corrió, desencadenó varios reports concurrentes más jobs ETL. Cada job abrió múltiples conexiones.
Cada conexión ahora podía asignar buffers grandes. El uso de memoria se disparó rápido, el kernel empezó a hacer swap y la máquina entró en el estado clásico:
CPU mayormente ociosa, load average alto, requests con timeout y todos culpando “la base de datos lenta”.
Revertir los cambios de memoria estabilizó el sistema. La solución a largo plazo fue más disciplinada:
poner tope a jobs concurrentes pesados, arreglar las peores consultas e índices y mantener memoria por conexión pequeña para que la concurrencia no se multiplicara en caos.
Conclusión: las perillas de memoria pueden ser herramientas de rendimiento o botones de autodestrucción. En 8GB, suelen ser ambas cosas.
Microhistoria 3: La práctica aburrida pero correcta que salvó el día (headroom de disco e higiene de WAL/binlog)
Un equipo ejecutaba una carga mixta—transacciones más jobs background—en un solo VPS mientras esperaban aprobación de presupuesto.
No es ideal, pero la realidad tiene opiniones. Hicieron una cosa de forma consistente: chequeos rutinarios de capacidad y margen conservador de disco.
Monitorizaron uso de disco, patrones de crecimiento de WAL/binlog y se aseguraron de que la rotación de logs fuera sensata.
También vigilaron la lag de replicación (cuando estaba activada) y mantuvieron una alerta estricta en umbrales de uso de filesystem.
Nada de esto fue glamuroso. Nadie fue ascendido por ello.
Un día, un despliegue introdujo un bug que generó mucha más actividad de escritura de lo normal. WAL/binlogs empezaron a crecer.
En muchos equipos, aquí aprendes que tu disco está lleno justo cuando la base de datos deja de aceptar escrituras.
Ellos no. Una alerta saltó temprano, antes de la zona de peligro.
El on-call reguló el job ofensor, pausó el rollout y recortó el radio del impacto mientras la BD se mantenía sana.
El postmortem fue corto. La solución fue sencilla. El impacto al cliente fue mínimo.
La práctica aburrida no evita todos los bugs. Evita que los bugs se conviertan en incidentes.
Errores comunes: síntoma → causa raíz → solución
1) “La BD está lenta” pero la CPU es baja y el load average es alto
Síntoma: p95 de latencia se dispara; CPU ociosa; load alto; usuarios se quejan.
Causa raíz: Espera en I/O (saturación de disco) o swapping.
Solución: Revisa iostat/vmstat. Reduce concurrencia, detén swap, muévete a disco mejor y reduce amplificación de escrituras (índices, tuning de vacuum/purge).
2) Errores de conexión aleatorios durante picos de tráfico
Síntoma: “too many connections”, “could not connect”, fallos intermitentes.
Causa raíz: Pools de conexión de app sin límites + falta de pooling + límites bajos de FD.
Solución: Impon tamaños de pool, usa PgBouncer para PostgreSQL, fija max_connections realista, sube ulimit -n y añade backpressure.
3) PostgreSQL se vuelve más lento días/semanas y luego “mágicamente” mejora tras mantenimiento
Síntoma: Lentitud gradual; disco crece; más lecturas; aparecen logs de autovacuum.
Causa raíz: Bloat de tablas/índices por MVCC y vacuum insuficiente, a menudo agravado por transacciones largas.
Solución: Encuentra y elimina sesiones largas/idle-in-transaction. Ajusta autovacuum por tabla. Programa vacuum/analyze donde haga falta.
4) MariaDB está bien hasta que corre un informe y todo se detiene
Síntoma: Un solo dashboard/report causa lentitud global; suben temp tables.
Causa raíz: Mal plan de consulta + índice faltante + tablas temporales a disco + concurrencia.
Solución: Usa slow query log, EXPLAIN, añade índices, limita jobs concurrentes de reporting y evita inflar buffers por hilo como parche.
5) La lag de replicación salta en horas pico
Síntoma: Réplica de lectura atrasada; aumenta riesgo de failover.
Causa raíz: Picos de escrituras, fsync lento del disco, transacciones grandes o presión de vacuum/checkpoint.
Solución: Reduce picos de escritura, divide transacciones grandes, ajusta checkpoint/flush con cuidado y asegura que la replicación coincida con la capacidad del disco.
6) La latencia sube cada pocos minutos como un latido
Síntoma: Paradas regulares, ralentizaciones periódicas y luego recuperación.
Causa raíz: Checkpoints/tormentas de flush (checkpoints de PostgreSQL; flushing de InnoDB) o jobs programados que colisionan.
Solución: Suaviza la carga de escrituras; ajusta intervalo de checkpoints y completion target (PostgreSQL); valida flushing de InnoDB; programa batch fuera de picos.
7) “Aumentamos caches y empeoró”
Síntoma: Ajustes de memoria mayores generan más latencia, no menos.
Causa raíz: Dejar al SO sin caché, aumentar riesgo de swap o incrementar margen de asignación por sesión.
Solución: Retrocede. Mantén margen. Haz que los ajustes de memoria sean proporcionales a la concurrencia y la carga, no a la esperanza.
Listas de verificación / plan paso a paso
Plan paso a paso: escalar clientes de forma segura en un VPS de 8GB (cualquiera de las dos bases)
- Separa responsabilidades: si es posible, mantén la BD en su propio VPS. Si no puedes, presupuestá memoria y CPU para la BD explícitamente.
- Mide la línea base: disk await, actividad de swap, conteo de conexiones, p95 de latencia. Anótalas. Si no lo haces, discutirás después.
- Fija límites duros de conexiones: limita conexiones del servidor y tamaños de pool en la app. Evita “autoscaling = conexiones infinitas”.
- Añade pooling: PgBouncer para PostgreSQL; para MariaDB usa pooling robusto en la app y considera thread pool si procede.
- Configura timeouts: mata sesiones idle-in-transaction; establece timeouts de sentencia; usa timeouts de app que no provoquen tormentas de reintentos.
- Protege la RAM: mantén pequeña la memoria por sesión; reserva caché del SO; evita swap. Añade margen con intención.
-
Activa visibilidad de consultas lentas: slow query log (MariaDB) o
pg_stat_statements(PostgreSQL) y registra consultas lentas. - Arregla los mayores culpables primero: añade índices faltantes, elimina N+1, elimina scans grandes sin control.
- Estabiliza las escrituras: evita transacciones enormes; agrupa jobs por lotes; ajusta checkpoint/flush con cuidado y valida con métricas.
- Planifica el crecimiento: si el conteo de clientes sigue subiendo, planea réplicas de lectura, particionado o pasar a una máquina más grande antes de que un incidente te obligue.
Checklist específico para PostgreSQL (VPS 8GB)
- Usa un pooler si la concurrencia de clientes es más que unas pocas docenas.
- Mantén
shared_buffersmoderado (a menudo alrededor de 1–2GB en 8GB DB-only); deja espacio para la caché del SO. - Fija
work_memcon prudencia y recuerda que puede usarse múltiples veces por nodo de consulta. - Vigila autovacuum: tuplas muertas, transacciones largas, riesgo de freeze de vacuum.
- Revisa wait events antes de cambiar ajustes. Locks y esperas por I/O requieren soluciones distintas.
Checklist específico para MariaDB (VPS 8GB)
- Dimensiona
innodb_buffer_pool_sizede forma realista (a menudo 4–6GB en DB-only 8GB, dependiendo de conteo de conexiones y buffers por hilo). - Mantén buffers por hilo sensatos; evita “aumentar sort buffer” sin cálculo de concurrencia.
- Habilita y revisa slow query log regularmente; arregla las peores consultas, no las medias.
- Valida comportamiento de tablas temporales y reduce tmp tables en disco indexando y reescribiendo consultas.
- Considera thread pool cuando tengas muchas conexiones pero CPU limitada.
Regla práctica de planificación de capacidad (práctica, no perfecta)
En 8GB, apunta a un número pequeño y estable de consultas ejecutándose activamente (decenas, no cientos).
Deja que los clientes hagan cola en un pooler o en la capa de app en vez de dejar que el kernel y la base de datos peleen por memoria e I/O.
Preguntas frecuentes
1) ¿Cuál es “mejor” en un VPS de 8GB: MariaDB o PostgreSQL?
Si necesitas semánticas estrictas de corrección, buena observabilidad de esperas/locks y herramientas de planificación de consultas predecibles, PostgreSQL suele dar mejor experiencia operativa.
Si estás profundamente en el ecosistema MySQL y tu carga es OLTP sencilla con pooling disciplinado, MariaDB puede funcionar muy bien.
El predictor más importante es si controlas el conteo de conexiones y el comportamiento de escrituras.
2) ¿Cuántas conexiones debería permitir?
Menos de las que crees. En un VPS de 8GB, conexiones directas a la base de datos en centenas son un camino común hacia swap y colapso de I/O.
Prefiere un límite pequeño en el servidor (a menudo decenas a principios de cientos según la carga) y haz cola en el pooler/capa de app.
Mide memoria y latencia en pico, luego ajusta.
3) ¿Realmente necesito PgBouncer para PostgreSQL?
Si tienes muchas instancias de app, conexiones de corta vida o tráfico con picos: sí.
El modelo de conexiones de PostgreSQL hace que el pooling sea la manera limpia de escalar el número de clientes sin escalar procesos backend.
Si tienes pocas conexiones de larga duración y sin picos, puedes prescindir—hasta que no puedas.
4) ¿Puede MariaDB manejar más conexiones que PostgreSQL sin pooling?
Puede manejar más hilos que PostgreSQL maneja procesos, pero “manejar” no es lo mismo que “mantenerse rápido”.
La memoria por conexión y los buffers por consulta aún se multiplican y la contención aumenta.
Pooling y comportamiento sensato de la app importan igual.
5) ¿Debería aumentar shared_buffers (PostgreSQL) para usar la mayor parte de la RAM?
No. PostgreSQL se beneficia mucho de la caché del SO. En 8GB, un shared_buffers moderado suele rendir mejor que uno agresivo
porque deja espacio para la caché del filesystem y evita presión de memoria. Valida con tasas de acierto en caché y disk await.
6) ¿Debería fijar innodb_buffer_pool_size al 75–80% de la RAM (MariaDB)?
Solo si la BD es el servicio principal y tus conteos de conexión y buffers por hilo están controlados.
Si tienes alta concurrencia o grandes buffers por sesión, 80% puede empujarte al swap en picos.
Empieza conservador, mide y luego aumenta.
7) ¿Por qué empeora el rendimiento cuando aumento memoria por consulta (work_mem, sort/join buffers)?
Porque la concurrencia convierte “por consulta” en “por todos”.
Más memoria por consulta puede ayudar consultas individuales, pero bajo carga aumenta la huella total de memoria, comprime la caché y dispara swap.
En RAM pequeña quieres comportamiento predecible más que velocidad máxima de una sola consulta.
8) ¿Cuál es la primera señal de que estoy limitado por I/O?
Latencia creciente con CPU baja y iowait alto es el clásico. iostat mostrará %util alto y await elevado.
Los wait events de PostgreSQL suelen mostrar esperas relacionadas con I/O; MariaDB puede mostrar “Sending data” mientras espera lecturas.
9) ¿Es seguro desactivar fsync o relajar durabilidad para sobrevivir picos?
Es una decisión de negocio disfrazada de perilla de tuning. Si desactivas fsync, aceptas pérdida de datos en un crash.
Si necesitas throughput, arregla concurrencia, consultas y almacenamiento primero. Solo relaja durabilidad si puedes explicar el radio de impacto a stakeholders sin pestañear.
10) ¿Cuándo dejo de tunear y actualizo el VPS?
Cuando el disco está saturado en estado estable, cuando no puedes evitar swap en picos pese a límites disciplinados, o cuando el steal de CPU domina.
El tuning no puede crear IOPS ni recuperar CPU robada. Subir de tier a veces es la optimización más fiable.
Próximos pasos que puedes hacer esta semana
- Elige una estrategia de conexiones: pooler para PostgreSQL, pooling estricto en la app para MariaDB. Anota la concurrencia máxima objetivo del servidor.
- Ejecuta las comprobaciones de diagnóstico rápido durante el pico:
vmstat,iostat, conteos de conexiones, esperas por locks. - Activa visibilidad de consultas lentas y arregla los 3 principales culpables. No 30. Tres.
- Establece timeouts que prevengan sesiones descontroladas y tormentas de reintentos.
- Crea un presupuesto de capacidad para RAM: caché compartida + overhead por conexión + margen de caché del SO. Luego hazlo cumplir con límites.
- Planifica la salida: si el crecimiento continúa, decide pronto si añadirás una réplica, pasarás a una máquina más grande o dividirás cargas.
En un VPS de 8GB no ganas por ser ingenioso. Ganas por ser disciplinado: menos conexiones, escrituras más estables y negarte a dejar que los valores por defecto dicten la arquitectura.