Puedes ejecutar mucho negocio en un solo VPS de 16GB. Hasta que no puedes. El modo de fallo rara vez es “se bloqueó instantáneamente”. Es peor: cojea, luego tu app acumula más conexiones y la base de datos se convierte en un restaurante lleno con un único cocinero y 200 personas con tarjetas.
Aquí es donde “MySQL vs MariaDB” deja de ser un debate de poca monta y se convierte en una decisión operativa: cuál te da valores por defecto más seguros, observabilidad más clara y menos sorpresas al añadir replicación y pooling — porque los añadirás si te importa la disponibilidad.
La realidad del VPS de 16GB: no tienes gran capacidad
Un VPS de 16GB es el “sedán mediano” del hosting de bases de datos. Es suficiente para sentirte confiado. También es lo bastante pequeño como para que un ajuste erróneo se convierta en un incidente ligado a CPU, con I/O hambriento, swap activo y una guarnición de “¿por qué el load average es 40?”
Esto es lo que realmente compras con 16GB:
- Presupuesto de memoria: no son 16GB reales. El kernel, la caché del sistema de archivos, tus agentes y la propia base de datos quieren su porción. Asume 12–13GB utilizables para procesos MySQL/MariaDB antes de arriesgar swap y jitter.
- Techo de I/O: “NVMe” en VPS a menudo significa “NVMe compartido”. Los picos de latencia no son un bug; son un vecino.
- Contención de CPU: incluso con vCPUs dedicadas, los vecinos ruidosos aparecen como %steal, retrasos del planificador y latencias cola impredecibles.
- Radio de explosión de un único host: kernel panic, corrupción del sistema de archivos, mantenimiento del proveedor, rm accidental y te quedas sin servicio.
En esta máquina, la replicación y el pooling no son “mejoras” de rendimiento. Son la diferencia entre un mal día y un día que limita tu carrera.
Hechos e historia que aún importan en producción
Algunos puntos de contexto son trivia hasta que te pegan durante una ventana de actualización.
- MariaDB se bifurcó de MySQL en 2009 tras la adquisición de Sun por Oracle. Esa bifurcación no fue solo ideológica; moldeó el ritmo de lanzamientos y los valores por defecto.
- MySQL 5.6/5.7 consagró a InnoDB; los días de MyISAM son principalmente historia, pero muchos esquemas legacy aún traen esas suposiciones.
- MariaDB mantuvo más motores de almacenamiento vivos por más tiempo (y añadió otros). Eso es potencia y riesgo: más opciones significa más formas de elegir mal.
- MySQL 8.0 eliminó por completo el query cache (era un festival de mutex globales). MariaDB mantuvo una versión más tiempo, pero sigue siendo rara vez una buena idea bajo concurrencia.
- Las implementaciones de GTID difieren entre MySQL y MariaDB. “GTID activado” no zanja la conversación; inicia preguntas de compatibilidad.
- El plugin de autenticación por defecto cambió en MySQL (caching_sha2_password se volvió común). Clientes antiguos y algunos proxies tuvieron problemas con eso.
- MariaDB y MySQL divergieron en el optimizador con el tiempo. El mismo SQL puede generar planes diferentes. En un VPS, las regresiones de plan son caras porque no tienes margen para ocultarlas.
- La replicación evolucionó de “mejor esfuerzo” a columna vertebral operativa: metadatos de replicación crash-safe, appliers multi-hilo, opciones semi-sync. Pero aún debes probar el failover como si te fuera la vida en ello.
MySQL vs MariaDB: cómo elegir en un VPS
Marco de decisión: garantías operativas, no lista de características
En un VPS de 16GB estás limitado por dos cosas: latencia y varianza. El pico de throughput importa menos que la consistencia del tiempo de respuesta bajo carga y la recuperación segura cuando las cosas salen mal.
Así que elige el motor según:
- Seguridad en las actualizaciones: ¿qué tan predecibles son las actualizaciones mayores/menores y cuánto las entiende tu equipo?
- Compatibilidad de tooling: ¿tu proxy, herramienta de backups, ORM y stack de monitorización se comportan de forma sensata?
- Ergonomía de replicación: ¿obtienes un estado de replicación claro, re-seed sencillo y menos sorpresas en casos límite?
- Estabilidad de rendimiento: ¿evita atascos patológicos (metadata locks, stalls de flushing, thrash del buffer pool) con tu carga?
MySQL: el “predeterminado aburrido” que es aburrido por una razón
Si ejecutas una carga web típica (tablas InnoDB, consultas OLTP, algo de reporting), MySQL 8.0 suele ser la opción segura. Está ampliamente probada en ecosistemas, proxies, drivers y servicios gestionados. Operativamente, eso importa más que una característica brillante que nunca desplegarás.
Fortalezas de MySQL en un VPS:
- Soporte de ecosistema predecible: drivers, conectores y herramientas en la nube tienden a asumir el comportamiento de MySQL primero.
- Madurez de Performance Schema: excelente para diagnosticar esperas, locks y SQL más costoso sin adivinar.
- Normalidad en replicación: muchas herramientas de failover y runbooks están escritos con semánticas de MySQL.
MariaDB: una opción sólida, pero trata la compatibilidad como un proyecto
MariaDB puede ser una gran elección cuando necesitas sus capacidades específicas o tu organización la estandariza. Pero no la trates como “MySQL con otro logo”. La divergencia es real y en una máquina pequeña el coste operacional de las sorpresas se amplifica.
Fortalezas de MariaDB en un VPS:
- Opciones diferentes de replicación y clustering: MariaDB + Galera es un patrón común (con salvedades), y las características de replicación de MariaDB pueden ser atractivas según la versión.
- Velocidad comunitaria y empaquetado: algunas distribuciones y proveedores tienen valores por defecto y backports sólidos para MariaDB.
Opinión práctica: Si empiezas desde cero y tu grafo de dependencias es típico (app web + proxies + conectores estándar), elige MySQL 8.0. Si ya estás en MariaDB y estable, no migres por aburrimiento: migra porque tienes un punto de dolor definido y una ruta probada.
Una cita de fiabilidad para anotar
Werner Vogels: “Todo falla, todo el tiempo.” (idea parafraseada)
Cuándo el pooling de conexiones se vuelve obligatorio (no “agradable”)
En un VPS de 16GB, tu servidor de base de datos no solo ejecuta consultas. También supervisa conexiones. Hilos, memoria por conexión, cambios de contexto, handshakes de autenticación, TLS y administración interna. Esta sobrecarga es invisible hasta que deja de serlo.
El pooling se vuelve obligatorio cuando ves cualquiera de estos patrones
- Tormentas de conexiones: escalado automático de instancias app, picos de cron, despliegues o reintentos crean miles de nuevas conexiones en segundos.
- Muchas consultas cortas: típico OLTP web donde las consultas son rápidas pero numerosas; la sobrecarga de conexión se vuelve el cuello de botella.
- Alta concurrencia con límites de máquina pequeños: aunque cada consulta sea barata, la BD pasa tiempo planificando en vez de trabajar.
- Sensibilidad a latencias cola: te importan p95/p99, no la media. El establecimiento de nuevas conexiones y el scheduling de hilos empeoran las colas.
Aquí va la verdad directa: si tu app abre y cierra conexiones por petición, el pooling no es opcional. La base de datos tarde o temprano te limitará con CPU, contención de mutex o presión de memoria. Y ocurrirá en el peor momento.
Broma #1: Una base de datos sin pooling es como una discoteca con un portero que vuelve a revisar tu DNI cada vez que parpadeas.
Qué significa “pooling” en producción
Hay dos capas:
- Pooling a nivel de aplicación (línea base preferida): tu app mantiene un pool estable de conexiones por instancia. Esto es bueno, pero escala de forma lineal con el número de instancias.
- Pooling en red/proxy (obligatorio al crecer): un pooler/proxy como ProxySQL multiplexa muchas sesiones cliente sobre menos conexiones al servidor, aplica reglas de enrutamiento y protege la BD de tormentas.
Tamaño del pool en un VPS de 16GB: deja de pensar “max_connections = 2000”
Un max_connections alto no es un trofeo. A menudo es una confesión de que no has hecho pooling y estás compensando con negación.
En 16GB, normalmente quieres:
- Conexiones de servidor razonables: a menudo en las centenas bajas o menos, según la carga.
- Time-outs estrictos: elimina clientes inactivos antes de que se conviertan en fugas de recursos.
- Contrapresión: poolers que encolan y degradan carga en vez de dejar morir la BD.
Cuándo la replicación se vuelve obligatoria (aunque odies la complejidad)
La replicación no es solo “para escalar lecturas”. En un VPS único, es principalmente para sobrevivir: backups, tiempo de recuperación, mantenimiento y pruebas. La primera vez que tu único host falla y la restauración lleva horas, aprenderás a querer réplicas.
La replicación se vuelve obligatoria cuando necesitas cualquiera de lo siguiente
- RPO < 1 hora sin apostar por backups de binlog y procedimientos de restauración perfectos.
- Ventanas de mantenimiento sin downtime: parches del SO, actualizaciones DB, migración de almacenamiento.
- Backups que no destrozan producción: hacer backups lógicos o ejecutar lecturas pesadas contra una réplica.
- Experimentos seguros: probar cambios de esquema, planes de consulta, creación de índices.
- Sanidad en respuesta a incidentes: necesitas un lugar limpio para hacer consultas forenses sin pelear con tráfico en vivo.
Réplica en otro VPS: la arquitectura mínima viable
Para un primario de 16GB, un movimiento “adulto” común es:
- Primario (16GB): acepta escrituras
- Réplica (8–16GB): sirve lecturas, ejecuta backups, puede ser promovida
- Pooler/proxy (VM pequeña o en hosts de app): controla concurrencia y enrutamiento
No es glamuroso. Es fiable. Como un extintor: aburrido hasta que es todo.
Replicación asíncrona vs semi-sync en un VPS
Asíncrona es el valor por defecto: el primario devuelve éxito antes de que la réplica confirme. Es rápida y puede perder las últimas transacciones si el primario falla.
Semi-sync puede reducir ese riesgo esperando la confirmación de al menos una réplica antes de devolver el commit. En redes de VPS con jitter, semi-sync también puede amplificar la latencia cola. Úsala cuando toleres la latencia y tengas un enlace de réplica estable; de lo contrario, céntrate en buenos backups y failover rápido.
Broma #2: La replicación es como usar hilo dental: todo el mundo jura que empezará después de la primera emergencia.
Guion de diagnóstico rápido: encuentra el cuello de botella rápido
Cuando la latencia sube o el throughput colapsa, no empieces editando configuraciones. Empieza por averiguar qué está haciendo la máquina ahora. Ese es el camino más corto del pánico a una decisión correcta.
Primero: ¿es CPU, memoria o I/O?
- Saturación de CPU / cola de ejecución: si la CPU está al máximo y el load average es alto, busca consultas caras, esperas por mutex o demasiados hilos.
- Presión de memoria: si hay swapping o reclaim, no estás afinando; te estás ahogando. Reduce el footprint de memoria y conexiones, luego revisa el dimensionamiento del buffer.
- Latencia de I/O: si el tiempo de espera de disco sube, puedes tener mucha CPU y aun así estar “caído”. El comportamiento de flush de InnoDB, doublewrite, frecuencia de fsync y ruido de vecinos importan.
Segundo: ¿demasiadas conexiones o pocos recursos por conexión?
- Comprueba el conteo de hilos, conexiones activas y conexiones abortadas.
- Comprueba si las consultas están bloqueadas por locks o esperando I/O.
- Busca tormentas de conexiones después de despliegues y eventos de autoscaling.
Tercero: ¿regresión de plan de consulta o deuda de esquema/índices?
- Identifica las consultas top por tiempo total y por latencia p95.
- Confirma el uso de índices y las estimaciones de filas.
- Revisa índices compuestos faltantes y escaneos completos accidentales.
Cuarto: salud de replicación (si la tienes)
- Lag de réplica: segundos detrás de la fuente, crecimiento de relay log.
- Hilos applier atascados en una transacción, esperas por locks.
- Disco lleno en réplica debido a binlogs/relay logs.
Tareas prácticas: comandos, salidas y decisiones
Estos son los chequeos reales que hago en un VPS de 16GB cuando algo se siente raro. Cada uno incluye un comando, salida típica, qué significa y qué decisión tomo.
Tarea 1: Confirmar presión de memoria y actividad de swap
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 15Gi 12Gi 520Mi 310Mi 2.6Gi 1.8Gi
Swap: 2.0Gi 1.2Gi 820Mi
Qué significa: El swap se está usando activamente. “Available” es pequeño. Espera stalls, especialmente bajo carga de escritura.
Decisión: Reduce inmediatamente el número de conexiones y la memoria por conexión. Si usas buffers grandes para sorts/joins, deténlo. Luego ajusta el tamaño del buffer pool de InnoDB y aplica pooling.
Tarea 2: Comprobar saturación de CPU y %steal
cr0x@server:~$ mpstat -P ALL 1 5
Linux 6.1.0 (server) 12/31/2025 _x86_64_ (4 CPU)
12:00:01 AM CPU %usr %sys %iowait %steal %idle
12:00:02 AM all 72.4 10.8 1.2 8.6 7.0
12:00:03 AM all 70.1 11.2 0.9 9.3 8.5
Qué significa: La CPU está saturada y el %steal es alto. Estás compitiendo por CPU del host.
Decisión: Reduce la concurrencia (pooling), corta max_connections y prioriza la eficiencia de las consultas. Considera mover la BD a un plan con menos contención o con CPUs dedicadas.
Tarea 3: Comprobar latencia de disco rápidamente
cr0x@server:~$ iostat -x 1 3
Device r/s w/s rkB/s wkB/s await svctm %util
nvme0n1 35.2 210.7 2200.1 9800.4 18.40 0.52 96.8
Qué significa: El disco está cerca de saturación y el await promedio es alto. InnoDB se detendrá en fsync y flushing.
Decisión: Investiga la amplificación de escrituras (binlogs, doublewrite, redo), revisa el flushing de páginas sucias y reduce picos (batching, tamaño de transacciones). Si es jitter de almacenamiento compartido, la replicación no lo arreglará; necesitarás mejor almacenamiento.
Tarea 4: Confirmar versión y sabor de MySQL/MariaDB
cr0x@server:~$ mysql -e "SELECT VERSION() AS version, @@version_comment AS comment\G"
*************************** 1. row ***************************
version: 8.0.36
comment: MySQL Community Server - GPL
Qué significa: Conocer el sabor y la versión exacta determina qué contadores de rendimiento, campos de estado de replicación y valores por defecto aplican.
Decisión: Alinea los pasos de troubleshooting a la versión. No apliques consejos de MariaDB a MySQL 8.0 sin verificar (y viceversa).
Tarea 5: Comprobar presión de conexiones y uso de hilos
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_%'; SHOW VARIABLES LIKE 'max_connections';"
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_cached | 12 |
| Threads_connected | 385 |
| Threads_created | 98123 |
| Threads_running | 42 |
+-------------------+-------+
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 800 |
+-----------------+-------+
Qué significa: Muchas threads creadas sugieren churn. Un alto número de hilos conectados en una máquina pequeña es a menudo sobrecarga más que trabajo útil.
Decisión: Implementa pooling. Reduce max_connections. Establece timeouts sensatos. Si hay mucho churn de hilos, ajusta thread_cache_size y revisa la reutilización de conexiones.
Tarea 6: Identificar esperas principales (Performance Schema)
cr0x@server:~$ mysql -e "SELECT event_name, COUNT_STAR, SUM_TIMER_WAIT/1000000000000 AS total_s
FROM performance_schema.events_waits_summary_global_by_event_name
WHERE event_name LIKE 'wait/io/file/innodb/%'
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 5;"
+----------------------------------------+------------+---------+
| event_name | COUNT_STAR | total_s |
+----------------------------------------+------------+---------+
| wait/io/file/innodb/innodb_log_file | 8421132 | 912.21 |
| wait/io/file/innodb/innodb_data_file | 20122341 | 311.45 |
| wait/io/file/innodb/innodb_temp_file | 1023311 | 88.17 |
+----------------------------------------+------------+---------+
Qué significa: Esperas pesadas en el archivo de logs implican presión de fsync/log. Esperas en archivos de datos sugieren I/O-bound en lecturas/escrituras.
Decisión: Si dominan las esperas del log, revisa tamaño del redo log, tamaño de transacciones y ajustes de durabilidad. Si dominan las esperas de datos, mira la tasa de aciertos del buffer pool y planes de consulta.
Tarea 7: Comprobar eficiencia del buffer pool de InnoDB
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+-----------+
| Variable_name | Value |
+---------------------------------------+-----------+
| Innodb_buffer_pool_read_requests | 987654321 |
| Innodb_buffer_pool_reads | 12345678 |
+---------------------------------------+-----------+
Qué significa: Lecturas vs solicitudes de lectura da una idea aproximada de la ratio de miss en caché. Los misses aquí son caros en almacenamiento VPS.
Decisión: Si los misses son altos y hay memoria disponible, aumenta el buffer pool. Si no hay memoria disponible, optimiza consultas/índices y reduce el working set.
Tarea 8: Encontrar las peores consultas por tiempo total
cr0x@server:~$ mysql -e "SELECT DIGEST_TEXT, COUNT_STAR, ROUND(SUM_TIMER_WAIT/1e12,2) AS total_s, ROUND(AVG_TIMER_WAIT/1e9,2) AS avg_ms
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 3\G"
*************************** 1. row ***************************
DIGEST_TEXT: SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT ?
COUNT_STAR: 1243321
total_s: 812.44
avg_ms: 0.65
Qué significa: La consulta es barata por llamada pero domina el tiempo por volumen. Eso es una historia de pooling e índices, no de “más servidor”.
Decisión: Asegura un índice compuesto correcto (user_id, created_at). Considera cachear en la capa de app. Añade pooling para evitar la sobrecarga de conexiones en alto QPS.
Tarea 9: Detectar contención de locks rápidamente
cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G"
TRANSACTIONS
------------
Trx id counter 123456789
Purge done for trx's n:o < 123456700 undo n:o < 0 state: running
History list length 9821
LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION:
... WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 123 page no 456 n bits 72 index PRIMARY of table `app`.`accounts` trx id 123456780 lock_mode X locks rec but not gap waiting
Qué significa: Deadlocks y esperas indican escrituras en conflicto o transacciones largas manteniendo locks.
Decisión: Acorta transacciones, añade índices adecuados y haz las actualizaciones determinísticas. Si un ORM hace patrones “select luego update”, corrígelo.
Tarea 10: Comprobar estado de replicación (MySQL)
cr0x@replica:~$ mysql -e "SHOW REPLICA STATUS\G"
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 4
Last_SQL_Error:
Qué significa: Replicación sana con lag pequeño.
Decisión: Puedes descargar lecturas y backups. Si el lag crece, deja de enviar lecturas pesadas y revisa cuellos de botella en applier/I/O.
Tarea 11: Confirmar retención de binlogs y presión de disco
cr0x@server:~$ mysql -e "SHOW BINARY LOGS;"
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| binlog.000231 | 1073741824|
| binlog.000232 | 1073741824|
| binlog.000233 | 1073741824|
+------------------+-----------+
Qué significa: Los binlogs son grandes y numerosos. En un disco de VPS, los binlogs pueden comerse el filesystem root y convertir una buena semana en un bucle de reinicios.
Decisión: Establece retención explícita (expire settings), monitoriza uso de filesystem y asegura que las réplicas no impidan purgas por estar offline.
Tarea 12: Comprobar capacidad del filesystem y agotamiento de inodos
cr0x@server:~$ df -h /var/lib/mysql
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 200G 186G 14G 94% /
Qué significa: Estás cerca del lleno. MySQL se comportará mal mucho antes del 100% porque necesita espacio temporal para temp tables, logs y recuperación de crash.
Decisión: Libera espacio ahora. Purga logs antiguos, mueve backups fuera del host, aumenta disco o reduce retención. Si estás al 94% en horario laboral, ya llegaste tarde.
Tarea 13: Comprobar presión de tablas temporales (memoria vs disco)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%tables';"
+-------------------------+----------+
| Variable_name | Value |
+-------------------------+----------+
| Created_tmp_disk_tables | 842113 |
| Created_tmp_tables | 902331 |
+-------------------------+----------+
Qué significa: Una alta proporción de tmp tables en disco significa que las consultas vierten a disco. En un VPS, eso es un impuesto de latencia.
Decisión: Añade índices, reduce el tamaño de resultados, corrige patrones GROUP BY/ORDER BY y evita grandes sorts. No “soluciones” inflando tmp_table_size hasta que te encuentres con OOM.
Tarea 14: Comprobar tamaños de tablas/índices para entender el working set
cr0x@server:~$ mysql -e "SELECT table_schema, table_name,
ROUND((data_length+index_length)/1024/1024,1) AS mb
FROM information_schema.tables
WHERE table_schema='app'
ORDER BY (data_length+index_length) DESC
LIMIT 5;"
+-------------+----------------+------+
| table_schema| table_name | mb |
+-------------+----------------+------+
| app | events | 8420 |
| app | orders | 3120 |
| app | users | 980 |
+-------------+----------------+------+
Qué significa: Tus tablas más grandes definen las necesidades del buffer pool. Si el hot set no cabe, pagarás I/O por siempre.
Decisión: Particiona/archiva datos fríos, añade tablas resumen o mueve analítica a otro lado. Escalar “hacia arriba” no arreglará un working set que crece perpetuamente en un solo VPS.
Tres micro-historias corporativas desde el frente
Micro-historia 1: El incidente causado por una suposición equivocada
Ejecutaban una app SaaS en un único VM de 16GB. Funcionó durante meses. Salió una nueva integración y el equipo asumió “la base de datos puede manejar más conexiones; es solo un valor de config”. Aumentaron max_connections y siguieron adelante.
El lunes siguiente el tráfico subió. La app abría conexiones por petición porque el pool de conexiones estaba mal configurado y efectivamente deshabilitado. La base de datos aceptó las conexiones, claro que sí. También creó threads, asignó buffers por conexión y empezó a hacer cambios de contexto como un colibrí con cafeína.
La latencia subió. La app reintentó. Eso creó más conexiones. El SO empezó a hacer swap. Ahora cada consulta tenía ese sabor especial de page faults de memoria. Los ingenieros miraban gráficos de CPU y se preguntaban por qué “añadir más conexiones” no aumentó el throughput.
Lo que lo arregló no fue tuning heroico. Hicieron cumplir el pooling, bajaron max_connections a un valor que la máquina realmente podía soportar y añadieron una pequeña capa proxy para absorber picos. La replicación vino después — no para lecturas, sino para poder hacer backups sin arrastrar producción por la melaza.
Micro-historia 2: La optimización que salió mal
Otra empresa tenía consultas de report lentas. Alguien decidió que la mejor solución era mayores configuraciones para tablas temporales y más memoria para sorts. Subieron buffers de sesión y globales agresivamente: sort buffers, join buffers, tamaños de temp table. Los reports mejoraron en staging. Aplaudieron y lo deployaron.
Producción no falló de inmediato. Falló con educación. Bajo concurrencia, cada conexión podía asignar grandes bloques de memoria. Cientos de conexiones por “razonables” buffers por conexión se convirtieron en “sorpresa, te quedaste sin RAM”. La máquina empezó a hacer swap. Luego la base de datos chocó contra un muro de stalls: I/O en disco subió, fsyncs se encolaron y el tiempo de respuesta se volvió asintótico.
El equipo persiguió fantasmas: ¿red? ¿proveedor? ¿kernel? En realidad habían convertido un presupuesto de memoria controlado en una ruleta por conexión.
El rollback fue humillante: reducir buffers por conexión, usar índices correctos, reescribir las consultas de report y mover reporting pesado a una réplica. También aprendieron una lección valiosa: en hosts pequeños, límites predictibles de recursos vencen a “rápido cuando está solo” siempre.
Micro-historia 3: La práctica aburrida pero correcta que salvó el día
Un tercer equipo ejecutaba MySQL con una réplica en otra región. Nada sofisticado: replicación asíncrona, backups lógicos diarios y una prueba de restauración semanal. Nadie lo presumía en reuniones porque era demasiado aburrido para impresionar.
Entonces su VPS primario tuvo un incidente de almacenamiento. El sistema de archivos se volvió de solo lectura a mitad del día. La base de datos hizo lo que hacen las bases de datos en esa condición: protestó, dejó de avanzar y dejó al equipo con la elección entre downtime y malas decisiones.
No hicieron tuning en pánico. Promovieron la réplica con su runbook practicado, apuntaron la aplicación al nuevo primario mediante el pooler y sacaron el primario viejo de rotación. El servicio degradó brevemente; no colapsó. Más tarde reconstruyeron el nodo viejo desde cero y lo reseedearon limpio.
El detalle clave: habían ensayado. La prueba de restauración aburrida significó que sabían que la réplica era utilizable, las credenciales funcionaban y la app podía apuntar a otro sitio sin una expedición arqueológica de tres horas en la configuración.
Errores comunes: síntoma → causa raíz → solución
Aquí es donde mueren la mayoría de los despliegues de bases de datos en VPS de 16GB: no por un gran bug, sino por un puñado de malentendidos operativos comunes.
1) Síntoma: pico repentino de latencia tras un deploy
Causa raíz: tormenta de conexiones (nuevos pods app arrancan, cada uno abre un pool; o pooling deshabilitado y conexiones por petición).
Solución: aplica pooling, limita la concurrencia en el proxy, ajusta wait_timeout/interactive_timeout apropiadamente y mantén max_connections realista. Si usas ProxySQL, configura multiplexing con cuidado y prueba con tus patrones de transacción.
2) Síntoma: CPU alta, QPS plano, “Threads_running” alto
Causa raíz: demasiadas consultas concurrentes, contención por locks o regresión de plan causando scans costosos.
Solución: identifica digests top, añade índices, reduce concurrencia vía pooling y arregla transacciones largas. No “soluciones” subiendo CPU antes de saber por qué hay hilos corriendo.
3) Síntoma: load average enorme, pero CPU no está totalmente usada
Causa raíz: espera de I/O o hilos bloqueados (presión de fsync, jitter de disco, metadata locks).
Solución: usa iostat -x, revisa esperas en Performance Schema y busca DDL largos o backups en el primario. Mueve backups y lecturas pesadas a la réplica.
4) Síntoma: el lag de réplica aumenta constantemente durante picos
Causa raíz: cuello de botella de I/O en réplica, applier single-thread, o lecturas pesadas que compiten con la replicación.
Solución: aumenta recursos de la réplica o reduce su carga de lecturas; ajusta replicación paralela donde aplique; asegura que relay logs y disco no estén saturados; evita ejecutar reportes masivos en la réplica usada para failover.
5) Síntoma: errores “Too many connections” aunque el servidor parezca inactivo
Causa raíz: fugas de conexión o transacciones inactivas de larga duración que ocupan conexiones; agotamiento del pool; max_connections demasiado bajo para el patrón real de concurrencia.
Solución: encuentra el cliente que filtra conexiones, aplica timeouts y pon un proxy delante para encolar. Incrementa max_connections solo después de haber acotado memoria por conexión y estabilizado el pooling.
6) Síntoma: stalls periódicos cada pocos minutos
Causa raíz: checkpointing o stalls de flushing, spills a tablas temporales, o jobs en background que hacen picos.
Solución: suaviza la carga de escrituras, revisa el comportamiento de páginas sucias, evita transacciones gigantes y programa jobs por lotes. En almacenamiento VPS, los picos grandes se castigan con saltos de latencia.
Listas de verificación / plan paso a paso
Paso a paso: hacer real el pooling en un VPS de 16GB
- Inventario de clientes: lista todos los servicios/trabajos que se conectan a la BD (app, workers, cron, herramientas BI).
- Arregla el pooling en la app primero: establece tamaño de pool explícito, vida máxima y timeout de inactividad. Evita “pools sin límite”.
- Pon un proxy/pooler delante cuando tengas múltiples nodos app o picos impredecibles.
- Fija server max_connections a un número que el host pueda permitirse. Esto es un fusible de seguridad, no una perilla de throughput.
- Establece timeouts para matar conexiones inactivas y sesiones atascadas.
- Monitoriza Threads_created, Threads_connected, aborted connects. Quieres gráficos estables y aburridos.
Paso a paso: añadir replicación sin convertirlo en hobby
- Elige topología: un primario + una réplica es la línea base. Manténlo simple.
- Provisiona la réplica en otro dominio de fallo: host VPS distinto, idealmente otra zona del proveedor.
- Habilita binary logging y fija la retención según tus necesidades de recuperación y tamaño de disco.
- Seedea la réplica correctamente: usa un método de snapshot consistente (herramienta de backup físico o dump lógico con locking/semántica GTID adecuada).
- Verifica la salud de replicación y alerta por lag y errores.
- Escribe y practica pasos de promoción. Ensaya de nuevo cuando actualices versiones.
- Mueve backups y lecturas pesadas a la réplica. Mantén la réplica de failover limpia y no sobrecargada.
Paso a paso: tunear InnoDB para un VPS de 16GB sin autolesionarte
- Dimensiona bien el buffer pool según la memoria disponible y el working set. No dejes sin caché al OS por completo.
- Mantén los buffers por conexión modestos. Los buffers globales son más seguros que sorpresas por conexión.
- Vigila spills a temp tables y arregla consultas antes de inflar límites de memoria.
- Confirma que las opciones de durabilidad coinciden con los requisitos del negocio. No cargo-cultes ajustes inseguros solo para ganar benchmarks.
- Mide, cambia una cosa, mide otra vez. “Tunear” editando 30 parámetros es la forma de crear misterios.
Preguntas frecuentes
1) ¿Debo elegir MySQL o MariaDB para un solo VPS de 16GB?
Si partes desde cero y quieres el soporte de ecosistema más fluido, elige MySQL 8.0. Si ya estás estable en MariaDB, quédate a menos que tengas una razón concreta de compatibilidad u operativa para migrar.
2) ¿Cuándo es realmente obligatorio el pooling de conexiones?
Cuando tienes picos, muchas instancias app o consultas cortas a alto QPS. Si ves churn de hilos (Threads_created subiendo rápido) o tormentas de conexiones durante despliegues, ya es obligatorio.
3) ¿Puedo simplemente aumentar max_connections en lugar de hacer pooling?
Puedes, como puedes quitar un detector de humo porque suena. Un max_connections más alto suele aumentar uso de memoria y cambios de contexto, empeorando los fallos.
4) ¿La replicación es solo para escalar lecturas?
No. En un VPS, la replicación es sobre todo para recuperación, backups y mantenimiento. Escalar lecturas es un efecto secundario agradable, no la justificación principal.
5) La replicación asíncrona es arriesgada. ¿Debo usar semi-sync?
Quizá. Semi-sync puede reducir pérdida de datos en fallo del primario, pero en redes VPS puede añadir latencia cola. Decide según tolerancia RPO y estabilidad de la red, y prueba bajo carga.
6) ¿Por qué mi réplica se atrasa cuando ejecuto reportes?
Porque las lecturas compiten con el applier de replicación por CPU e I/O. Si la réplica es target de failover, evita reporting pesado en ella o aprovisiona una réplica separada para analítica.
7) ¿Cuál es la primera métrica a vigilar en un host DB pequeño?
Latencia de disco (iostat await / utilización) y presión de memoria (actividad de swap). Los gráficos de CPU mienten cuando el cuello de botella real es I/O wait o swapping.
8) ¿Galera (MariaDB) es buena idea en instancias VPS?
Puedes usarla, pero no es gratis. La coordinación de escritura casi síncrona puede castigar la latencia y los riesgos de split-brain aumentan si no diseñas el quórum bien. Si no estás preparado para operar un clúster, prefiere primario+réplica primero.
9) ¿Necesito un proxy como ProxySQL si mi app ya hace pooling?
No siempre, pero a menudo sí. El pooling en la app por sí solo sigue escalando conexiones de servidor con el número de instancias app. Un proxy añade contrapresión, enrutamiento, multiplexado y protección contra tormentas.
10) ¿Cuál es la mejora segura y más rápida que puedo hacer este mes?
Añade una réplica para backups y failover practicado, e implementa pooling de conexiones con límites estrictos. Esos dos cambios previenen los incidentes más comunes de “un solo VPS fundido”.
Conclusión: qué hacer la próxima semana
Si estás en un VPS de 16GB, no tienes el lujo de la concurrencia descuidada. Tampoco tienes el lujo de “añadiremos replicación más tarde”. “Más tarde” es cuando restauras desde backups mientras tu CEO aprende vocabulario nuevo.
Pasos prácticos:
- Implementa pooling (a nivel de app de inmediato; añade un proxy si tienes múltiples nodos app o tráfico con picos).
- Fija límites de conexiones realistas y timeouts; trata max_connections como un disyuntor.
- Añade una réplica en un VPS separado y mueve backups/reporting allí.
- Escribe un runbook de failover y ensáyalo en horario laboral con salvaguardas.
- Ejecuta el guion de diagnóstico rápido en tu próximo pico de latencia y documenta lo aprendido.
MySQL vs MariaDB no te salvará por sí solo. La replicación y el pooling sí. Elige la base de datos que tu equipo pueda operar con calma a las 2 a.m., y luego diseña el sistema para que las 2 a.m. sigan siendo aburridas.