Compraste el VPS de 2GB porque parecía “suficiente para una pequeña app”. Luego la base de datos empezó a comportarse como una planta de interior frágil: unas cuantas conexiones más, una consulta gorda y se desploma—a veces llevándose todo el nodo. No necesitas heroísmos. Necesitas presupuestos de memoria sensatos, E/S predecible y ajustes que fallen de forma controlada en lugar de explosiva.
Esta es una guía de campo para ejecutar MySQL o MariaDB en una máquina pequeña sin convertir al OOM killer del kernel en tu DBA principal. Soy opinionado porque la producción es opinionada: no le importa lo que dijera tu benchmark si tu servidor se reinicia a las 3 a.m.
Elegir MySQL o MariaDB en 2GB: los puntos reales de decisión
En un VPS de 2GB, “MySQL vs MariaDB” tiene menos que ver con la ideología y más con la ergonomía operacional: los valores por defecto, el empaquetado y qué minas prefieres gestionar.
Mi recomendación (con opinión y salvedades)
- Si necesitas comportamiento predecible entre nubes y servicios gestionados: elige MySQL 8.0. Coincide con lo que la mayoría de proveedores alojados ejecutan y la experiencia operativa es amplia.
- Si te autohospedas y quieres perillas que ayuden a cajas pequeñas: MariaDB puede ser más amigable, especialmente con thread pool (en algunas compilaciones) y empaquetado sensato en muchas distribuciones.
- Si dependes de características de MySQL 8 (cierta JSON y comportamientos de replicación, trabajo del optimizador más nuevo, expectativas de herramientas oficiales): elige MySQL, ajústalo con precaución y sigue con tu vida.
- Si quieres minimizar sorpresas en actualizaciones: escoge lo que tu distribución soporte mejor y mantenlo fijado. En 2GB, la sorpresa sale cara.
En rendimiento, cualquiera de los dos motores saturará felizmente tu CPU pequeño y tu disco lento antes de ganar premios. El verdadero riesgo es la memoria: la base de datos es profesional gastando todo lo que le das, más lo que olvidaste que podía gastar.
Chiste #1: Un VPS de 2GB ejecutando una base de datos sin ajustar es como una maleta de mano: puedes meterlo todo, hasta que la cremallera decide que la has ofendido.
Hechos interesantes y contexto que realmente importan
No son datos para una noche de trivial. Son los que explican por qué las opciones difieren, por qué existen valores por defecto y por qué “solo copia este my.cnf” es una trampa.
- MariaDB se bifurcó de MySQL tras la compra de Sun por Oracle (2010): esa bifurcación no fue solo política—llevó a valores por defecto distintos, características diferentes y, a veces, interpretaciones distintas de “compatible”.
- MySQL 5.6 hizo de InnoDB el predeterminado claro para cargas serias: el cambio desde la era MyISAM al tuning centrado en InnoDB cambió lo que significa “seguro”.
- MySQL 8.0 eliminó completamente la query cache: las guías antiguas aún recomiendan ajustes de query cache; en MySQL 8 esos mandos no existen, y en MariaDB aún pueden hacer daño.
- El buffer pool de InnoDB se convirtió en la palanca principal de rendimiento: la regla “dale el 80% de la RAM” se popularizó en hosts dedicados—en un VPS de 2GB con otros procesos, es temerario.
- La agrupación de hilos divergió: MariaDB tiene implementaciones de thread pool muy usadas; la historia de MySQL varía según edición y versión. En CPUs pequeñas, la gestión de hilos afecta la latencia cola más de lo que piensas.
- El logging redo de InnoDB evolucionó: ajustar la capacidad del redo y el comportamiento de flush importa porque los discos pequeños del VPS suelen ser de red y con ráfagas.
- Los valores por defecto en Ubuntu/Debian cambiaron con el tiempo: verás configuraciones base distintas entre versiones mayores, lo que significa que “MySQL stock” no es una sola cosa.
- El OOM killer de Linux no cuida tu SLA: mata algo. A menudo mysqld. A veces tu app. En cualquier caso, tu canal de incidentes se ilumina.
El modelo mental para VPS de 2GB: a dónde va la memoria
En hosts pequeños, la pregunta no es “¿cuál es el ajuste más rápido?” Es “¿cuánto daño puede hacer este ajuste en concurrencia pico?” Ese es el cambio mental que evita caídas.
Presupuesta tu RAM como presupuestas tu sueño en guardia
Con 2GB RAM, no tienes 2GB para MySQL/MariaDB. Tienes:
- Kernel + page cache + slab: típicamente unos cientos de MB, más bajo carga I/O.
- Servicios del sistema (sshd, journald, cron, agente de monitorización): 50–200MB.
- Tu runtime de aplicación (PHP-FPM, Node, Java, trabajadores Python): desde “manejable” hasta “¿por qué usa 900MB?”
- Base de datos: lo que quede, y tratará de tomarlo todo.
En un VPS dedicado solo a BD, puedes empujar más la base de datos. En una caja compartida app+BD (común en stacks baratos), debes reservar espacio para que el kernel no entre en pánico y empiece a matar procesos.
Los dos tipos de memoria en bases de datos: global y por conexión
La mayoría de guías hablan de buffers globales (buffer pool, log buffers). El asesino oculto es la memoria por conexión:
- sort buffers
- join buffers
- read buffers
- tmp tables (en memoria hasta que desbordan)
- thread stacks
Cada uno puede parecer pequeño. Multiplícalo por 200 conexiones y has construido una hoguera de RAM. Por eso “solo aumentar max_connections” es uno de los clics más caros que puedes hacer.
El disco es el otro eje: el almacenamiento de VPS suele ser “teatro I/O”
Tu disco de VPS puede ser adjunto por red, con ráfagas, limitado o respaldado por almacenamiento sobrecomprometido. Las bases de datos exponen brutalmente las mentiras del almacenamiento. Si afinas la memoria pero ignoras los patrones de E/S (tasa de fsync, flushing de páginas sucias), tendrás síntomas clásicos:
- rápido hasta que deja de serlo
- picos aleatorios de latencia
- bloqueos durante checkpoints
- replicación con lag que aparece “sin razón”
Una cita de fiabilidad (idea parafraseada)
“La esperanza no es una estrategia.” — General H. Norman Schwarzkopf (idea parafraseada, usada a menudo en ingeniería y operaciones)
Aplica aquí: no puedes esperar que el VPS no llegue a concurrencia pico; debes poner límites para que el pico no te mate.
Perfiles de ajuste seguros (copiar/pegar) para 2GB RAM
Estos perfiles priorizan “no caer” primero, “suficientemente rápido” segundo. Asumen InnoDB (deberías usar InnoDB a menos que tengas una razón muy específica para no hacerlo).
Antes de copiar/pegar: decide tu tipo de despliegue
- VPS solo BD: la base de datos puede usar la mayor parte de la memoria, pero todavía deja margen para el SO.
- App + BD en el mismo VPS: la BD debe ser modesta y los límites de conexión estrictos.
También decide características de almacenamiento:
- SSD-ish con fsync decente: puedes ejecutar flushing más agresivo.
- Almacenamiento en red/barato: espera dolor por fsync; configura para evitar paradas por ráfagas.
Perfil A: “App + BD juntas” (stack barato más común)
Objetivo: MySQL 8.0 o MariaDB 10.6+ en un VPS de 2GB que también ejecuta servidor de aplicación. Este es el perfil que usas cuando prefieres degradar rendimiento a que todo el equipo se despierte.
cr0x@server:~$ sudo tee /etc/mysql/conf.d/99-vps-2gb-safe.cnf >/dev/null <<'EOF'
[mysqld]
# Core safety limits
max_connections = 60
skip_name_resolve = ON
wait_timeout = 60
interactive_timeout = 300
# InnoDB: keep it small and stable
innodb_buffer_pool_size = 512M
innodb_buffer_pool_instances = 1
innodb_log_buffer_size = 16M
innodb_redo_log_capacity = 256M
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 1
innodb_io_capacity = 200
innodb_io_capacity_max = 400
# Reduce per-connection memory blowups
tmp_table_size = 32M
max_heap_table_size = 32M
sort_buffer_size = 2M
join_buffer_size = 2M
read_buffer_size = 1M
read_rnd_buffer_size = 1M
thread_stack = 256K
# Keep table cache reasonable
table_open_cache = 800
open_files_limit = 65535
# Observability that pays rent
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = OFF
EOF
Por qué funciona: 512M de buffer pool no ganará benchmarks, pero no dejará sin memoria a tu app. Timeouts ajustados reducen el acaparamiento de conexiones inactivas. Buffers tmp/sort/join modestos limitan explosiones de memoria por conexión. Estás eligiendo daño acotado.
Perfil B: “VPS solo BD de 2GB” (más margen, pero no es un juguete)
Si la caja solo ejecuta la base de datos (más monitorización), puedes dar más a InnoDB.
cr0x@server:~$ sudo tee /etc/mysql/conf.d/99-vps-2gb-dbonly.cnf >/dev/null <<'EOF'
[mysqld]
max_connections = 120
skip_name_resolve = ON
wait_timeout = 120
innodb_buffer_pool_size = 1G
innodb_buffer_pool_instances = 1
innodb_log_buffer_size = 32M
innodb_redo_log_capacity = 512M
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 1
innodb_io_capacity = 300
innodb_io_capacity_max = 600
tmp_table_size = 64M
max_heap_table_size = 64M
sort_buffer_size = 2M
join_buffer_size = 2M
read_buffer_size = 1M
read_rnd_buffer_size = 1M
thread_stack = 256K
table_open_cache = 1200
open_files_limit = 65535
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.5
EOF
Regla dura: no pongas un buffer pool de 1.4G en un VPS de 2GB y luego te sorprendas cuando haya swapping. Estarás al borde, y el borde es donde viven los incidentes.
Perillas específicas de MariaDB que valen la pena en cajas pequeñas
MariaDB a menudo te da palancas operativas extra. Úsalas con cuidado; “más perillas” no es automáticamente “mejor”.
- Thread pool puede reducir el thrash de hilos bajo altos recuentos de conexión, mejorando la latencia cola. Si lo activas, mantén max_connections realista.
- Aria y motores legacy existen; no los uses accidentalmente para tablas con muchas escrituras a menos que conozcas los modos de fallo.
Realidades específicas de MySQL en cajas pequeñas
- Los valores por defecto de MySQL 8 suelen ser sensatos, pero “sensato” asume que no estás ejecutando 200 conexiones en 2GB.
- No persigas funciones eliminadas como query cache. Si encuentras una guía que te dice que la ajustes en MySQL 8, cierra la pestaña.
Sobre ajustes de durabilidad (el argumento fsync)
innodb_flush_log_at_trx_commit=1 es el valor predeterminado duradero. En almacenamiento malo, puede ser doloroso. Ponerlo en 2 reduce la frecuencia de fsync y mejora el rendimiento, pero aceptas perder hasta ~1 segundo de transacciones ante un crash. Si estás en un VPS único sin replicación y te importa la integridad de datos, no “optimices” la durabilidad porque un blog te lo dijo. Si tu carga lo tolera (sesiones, caches, datos derivados), eso es una decisión de negocio. Escríbelo.
Chiste #2: Desactivar la durabilidad para “arreglar el rendimiento” es como quitar el detector de humo porque el pitido molesta.
Tareas prácticas: comandos, salidas y qué decides
Estas tareas están diseñadas para el momento exacto en que miras un VPS de 2GB y te preguntas por qué está lento, bloqueado o reiniciando. Cada tarea incluye: un comando, cómo se ve una salida realista, qué significa y la decisión que tomas.
Tarea 1: Confirma lo que realmente instalaste (y evita ajustar el demonio equivocado)
cr0x@server:~$ mysql --version
mysql Ver 8.0.36-0ubuntu0.22.04.1 for Linux on x86_64 ((Ubuntu))
Significado: La versión te dice qué características existen y qué mitos de ajuste ignorar. MySQL 8 no tiene query cache; MariaDB tiene valores por defecto distintos en varias áreas.
Decisión: Ajusta tu consejo según tu versión. Si estás en MariaDB, también comprueba la versión menor exacta porque los valores por defecto cambian.
Tarea 2: Verifica el sabor del servidor desde dentro de SQL
cr0x@server:~$ mysql -e "SELECT VERSION(), @@version_comment;"
+-----------+------------------------------+
| VERSION() | @@version_comment |
+-----------+------------------------------+
| 8.0.36 | (Ubuntu) |
+-----------+------------------------------+
Significado: Esto confirma que estás conectado al servidor que crees. Suena obvio. Deja de ser obvio durante migraciones, failovers y contenedores “temporales” de depuración.
Decisión: Si version_comment no coincide con lo esperado, para y localiza el endpoint real. Ajustar la instancia equivocada es un desperdicio especial de día.
Tarea 3: Comprueba RAM, swap y si ya estás viviendo peligrosamente
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 1.9Gi 1.2Gi 140Mi 42Mi 620Mi 420Mi
Swap: 1.0Gi 180Mi 820Mi
Significado: Poca memoria “available” más uso de swap significa que la presión es real. Hacer swap en un host de BD a menudo se correlaciona con picos de latencia y bloqueos.
Decisión: Si swap es distinto de cero y la latencia de BD es mala, reduce la huella de memoria de la BD (buffer pool, recuento de conexiones, buffers por conexión) antes de “optimizar consultas”.
Tarea 4: Prueba que el OOM killer está involucrado (o descártalo)
cr0x@server:~$ sudo dmesg -T | tail -n 20
[Mon Dec 30 09:11:02 2025] Out of memory: Killed process 1421 (mysqld) total-vm:3074824kB, anon-rss:1320440kB, file-rss:0kB, shmem-rss:0kB, UID:110 pgtables:3140kB oom_score_adj:0
[Mon Dec 30 09:11:02 2025] oom_reaper: reaped process 1421 (mysqld), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
Significado: Eso no es “MySQL se estrelló”. Es “Linux lo mató”. La solución es capacidad o límites de memoria, no reindexar.
Decisión: Establece límites duros: baja max_connections, baja buffers por conexión y ajusta innodb_buffer_pool_size. Considera añadir swap solo como cojín de último recurso, no como plan.
Tarea 5: Identifica quién está acaparando memoria ahora mismo
cr0x@server:~$ ps -eo pid,comm,rss,pmem --sort=-rss | head
PID COMMAND RSS %MEM
1421 mysqld 1184500 60.2
911 php-fpm8.1 210300 10.7
947 php-fpm8.1 185120 9.4
612 node 121800 6.1
Significado: RSS muestra memoria residente actual. Es una instantánea, pero te dice si la BD es la fuente de presión.
Decisión: Si mysqld es el mayor por mucho, ajusta la memoria de BD. Si la app es mayor, arregla el modelo de procesos de la app (recuento de workers, fuga, caching).
Tarea 6: Confirma el tamaño del buffer pool y variables de memoria clave
cr0x@server:~$ mysql -e "SHOW VARIABLES WHERE Variable_name IN ('innodb_buffer_pool_size','max_connections','tmp_table_size','max_heap_table_size','sort_buffer_size','join_buffer_size');"
+-------------------------+-----------+
| Variable_name | Value |
+-------------------------+-----------+
| innodb_buffer_pool_size | 536870912 |
| join_buffer_size | 2097152 |
| max_connections | 60 |
| max_heap_table_size | 33554432 |
| sort_buffer_size | 2097152 |
| tmp_table_size | 33554432 |
+-------------------------+-----------+
Significado: Los valores son bytes para muchas variables. Buffer pool en 512MB coincide con el Perfil A.
Decisión: Si estos no coinciden con tu perfil previsto, estás depurando ficción. Arregla el orden de carga de config, elimina fragmentos conflictivos y reinicia limpio.
Tarea 7: Estima la concurrencia real de conexiones y si max_connections es fantasía
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Max_used_connections';"
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_connected | 18 |
+-------------------+-------+
+---------------------+-------+
| Variable_name | Value |
+---------------------+-------+
| Max_used_connections| 54 |
+---------------------+-------+
Significado: Has llegado a 54 conexiones concurrentes históricamente. Con max_connections=60 estás cerca del techo.
Decisión: Si max_used está cerca del límite, o (a) súbelo ligeramente y reduce buffers por conexión, o (b) arregla el pooling en la app para no necesitar 60 conexiones reales.
Tarea 8: Comprueba derrames a tablas temporales en disco (clásico asesino silencioso)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Created_tmp%tables';"
+-------------------------+-------+
| Variable_name | Value |
+-------------------------+-------+
| Created_tmp_disk_tables | 1842 |
| Created_tmp_files | 211 |
| Created_tmp_tables | 9321 |
+-------------------------+-------+
Significado: Se están creando tablas temporales en disco. Algo de eso es normal; mucho indica sorts/group-bys que no caben en memoria o falta de índices.
Decisión: Si las tmp en disco son altas respecto a tmp tables, no subas tmp_table_size en 2GB. Arregla la consulta, añade índices o acepta derrames y ajusta I/O en su lugar.
Tarea 9: Comprueba si el buffer pool es demasiado pequeño (o justo)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+---------+
| Variable_name | Value |
+---------------------------------------+---------+
| Innodb_buffer_pool_read_requests | 9284432 |
| Innodb_buffer_pool_reads | 188322 |
+---------------------------------------+---------+
Significado: Reads vs read_requests te da una idea de misses en cache. Algunos misses son esperados. Una ratio muy alta sugiere buffer pool pequeño o una carga que no cachea bien.
Decisión: Si los misses son enormes y tienes margen de memoria (y no estás haciendo swap), aumenta el buffer pool con precaución. Si estás limitado por memoria, arregla consultas e índices en su lugar.
Tarea 10: Encuentra rápido las clases de espera principales (snapshot de InnoDB status)
cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2025-12-30 09:22:11 0x7f2d2c1ff700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 6 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 2199 srv_active, 0 srv_shutdown, 12195 srv_idle
srv_master_thread log flush and writes: 14394
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 31212
OS WAIT ARRAY INFO: signal count 30009
RW-shared spins 0, rounds 0, OS waits 12
RW-excl spins 0, rounds 0, OS waits 48
------------
TRANSACTIONS
------------
Trx id counter 1149091
History list length 188
Significado: Esto es un control de la realidad rápido. Altos OS waits en semáforos pueden indicar contención; lista de historial grande sugiere purge lag (a menudo por transacciones largas).
Decisión: Si la history list length crece y se mantiene alta, busca transacciones de larga duración. Si los semaphore waits suben bajo carga, reduce concurrencia, ajusta consultas y considera thread pooling (especialmente en MariaDB).
Tarea 11: Detecta transacciones largas que mantienen undo/purge ocupados
cr0x@server:~$ mysql -e "SELECT trx_id, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx ORDER BY trx_started;"
+--------+---------------------+---------------------+----------------------------------+
| trx_id | trx_started | trx_mysql_thread_id | trx_query |
+--------+---------------------+---------------------+----------------------------------+
| 1148802| 2025-12-30 09:04:01 | 312 | SELECT * FROM orders WHERE ... |
+--------+---------------------+---------------------+----------------------------------+
Significado: Una transacción ejecutándose desde las 09:04 puede mantener segmentos undo calientes y retrasar el purge, afectando escrituras y generando bloat.
Decisión: Arregla el comportamiento de la app (evita transacciones interactivas), añade índices y establece timeouts sensatos. A veces matar la transacción es necesario, pero prevenir es más barato.
Tarea 12: Verifica que el logging de consultas lentas esté activado y sea útil
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'long_query_time';"
+----------------+-------+
| Variable_name | Value |
+----------------+-------+
| slow_query_log | ON |
+----------------+-------+
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| long_query_time | 1.000 |
+-----------------+-------+
Significado: Los slow logs son tu perfilador de bajo presupuesto. En un VPS de 2GB no puedes adivinar.
Decisión: Si el slow log está apagado, enciéndelo. Si es demasiado ruidoso, sube temporalmente long_query_time—no deshabilites el log.
Tarea 13: Lee el slow log como operador, no como turista
cr0x@server:~$ sudo tail -n 25 /var/log/mysql/slow.log
# Time: 2025-12-30T09:27:14.123456Z
# User@Host: app[app] @ 10.0.0.15 []
# Query_time: 3.214 Lock_time: 0.002 Rows_sent: 50 Rows_examined: 982341
SET timestamp=1735550834;
SELECT * FROM events WHERE user_id=123 ORDER BY created_at DESC LIMIT 50;
Significado: Rows_examined ~ 1M para un LIMIT 50 es un clásico índice faltante/pobre. Lock_time es pequeño; esto no es “bloqueo”, es “trabajo”.
Decisión: Añade/ajusta un índice (probablemente (user_id, created_at)), o reescribe la consulta para evitar escaneos. No toques tamaños de buffer para “arreglar” esto.
Tarea 14: Confirma que no estás accidentalmente en doble cacheo (malo con desajustes O_DIRECT)
cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'innodb_flush_method';"
+---------------------+----------+
| Variable_name | Value |
+---------------------+----------+
| innodb_flush_method | O_DIRECT |
+---------------------+----------+
Significado: O_DIRECT reduce el doble buffering entre el buffer pool de InnoDB y el page cache del SO. En RAM pequeña, eso ayuda a mantener la memoria predecible.
Decisión: Si no es O_DIRECT y tienes presión de memoria, considera cambiarlo (prueba primero; algunos stacks de almacenamiento se comportan de forma extraña).
Tarea 15: Inspecciona tablas abiertas y presión de descriptores de archivos
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Open_tables'; SHOW VARIABLES LIKE 'table_open_cache';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Open_tables | 790 |
+---------------+-------+
+------------------+------+
| Variable_name | Value|
+------------------+------+
| table_open_cache | 800 |
+------------------+------+
Significado: Si Open_tables sigue alcanzando el límite de cache, MySQL abrirá/cerrará tablas con frecuencia, causando sobrecarga y contención de metadatos.
Decisión: Si tienes margen de FD y memoria, sube table_open_cache moderadamente. En 2GB, no lo pongas a 10.000 como si fuera gratis.
Tarea 16: Comprueba síntomas de latencia de I/O desde el lado del SO
cr0x@server:~$ iostat -xz 1 3
Linux 5.15.0-91-generic (server) 12/30/2025 _x86_64_ (2 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
18.21 0.00 6.44 22.12 0.00 53.23
Device r/s w/s rkB/s wkB/s await %util
vda 42.0 58.0 912.0 2310.0 48.30 92.10
Significado: await ~48ms y %util ~92% indica que el disco es el cuello de botella. Alto iowait confirma eso.
Decisión: Si el disco está saturado, deja de afinar knobs de CPU. Reduce la amplificación de escritura (índices, tamaño de transacción), ajusta la capacidad redo sensatamente y considera mejorar el almacenamiento o mover la BD fuera de la caja.
Guion de diagnóstico rápido
Este es el flujo “tienes 15 minutos para parecer competente”. Está ordenado para encontrar el cuello de botella rápido en un VPS de 2GB.
Primero: ¿está muriendo (OOM, reinicios o swap)?
- Revisa
dmesgpor kills de OOM. - Revisa
free -hpor actividad de swap y baja memoria disponible. - Comprueba si mysqld está reiniciando (logs de systemd).
cr0x@server:~$ sudo journalctl -u mysql --since "1 hour ago" | tail -n 30
Dec 30 09:11:04 server systemd[1]: mysql.service: Main process exited, code=killed, status=9/KILL
Dec 30 09:11:04 server systemd[1]: mysql.service: Failed with result 'signal'.
Dec 30 09:11:06 server systemd[1]: mysql.service: Scheduled restart job, restart counter is at 3.
Decisión: Si hay OOM/reinicios, prioriza límites de memoria y tope de conexiones antes de cualquier micro-optimización.
Segundo: ¿es disco (iowait, stalls por fsync, derrames a tmp)?
iostat -xzpara await/util.- Comprueba derrames a disco en tablas temporales.
- Revisa presión de redo/log flush.
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_os_log_fsyncs'; SHOW GLOBAL STATUS LIKE 'Innodb_os_log_written';"
+----------------------+--------+
| Variable_name | Value |
+----------------------+--------+
| Innodb_os_log_fsyncs | 192113 |
+----------------------+--------+
+-----------------------+-----------+
| Variable_name | Value |
+-----------------------+-----------+
| Innodb_os_log_written | 987654321 |
+-----------------------+-----------+
Decisión: Si el disco es cuello de botella, céntrate en arreglar consultas/índices y reducir escrituras. No subas el buffer pool hasta que no haya swapping; cambiarás un outage por otro.
Tercero: ¿es CPU/locking (demasiados hilos, contención)?
top/pidstatpara ver saturación CPU.- InnoDB status para semaphore waits y contención de locks de filas.
- Processlist para muchas consultas pesadas simultáneas.
cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST;" | head -n 15
Id User Host db Command Time State Info
311 app 10.0.0.15:41012 prod Query 12 Sending data SELECT * FROM events WHERE user_id=...
312 app 10.0.0.15:41014 prod Query 520 Updating UPDATE inventory SET ...
313 app 10.0.0.15:41015 prod Sleep 58 NULL
Decisión: Si ves updates de larga duración y muchos lectores “Sending data”, probablemente tengas un problema de índices o diseño de transacciones, no un problema mágico de configuración.
Tres mini-historias corporativas (porque el fallo enseña)
1) Incidente causado por una suposición errónea: “max_connections es solo un tope, no una configuración de memoria”
Ejecutaban un portal de clientes en un VPS de 2GB: web, base de datos, todo en un mismo lugar. Durante un pico estacional, el portal no solo se ralentizó. Empezó a reiniciarse. El equipo asumió que era un kernel malo o un “vecino ruidoso” en el host del VPS.
La primera solución que intentaron fue clásica: subir max_connections de 100 a 400 porque los clientes recibían “demasiadas conexiones”. El outage empeoró. Ahora el sitio no solo devolvía errores; entró en un bucle de reinicios de la base de datos y despliegues a medias.
En el post-mortem repasamos la contabilidad de memoria. Cada conexión podía asignar varios buffers. En un pico, las conexiones se expandían, algunas ejecutando sorts y joins, cada una cogiendo megabytes. Multiplica por cientos y tienes un precipicio de memoria. El kernel hizo lo que hace: mató mysqld.
La solución aburrida fue la correcta: limitar conexiones, imponer pooling en la app, reducir buffers por conexión y mantener el buffer pool de InnoDB modesto. Luego, porque la gente olvida, añadimos alertas sobre Max_used_connections y uso de swap.
El resultado no fue “más rápido” en sentido benchmark. Fue estable. El portal se degradó con elegancia en picos en vez de detonar. En producción, la degradación controlada es una característica.
2) Optimización que salió mal: “Hagamos tmp_table_size enorme para evitar derrames a disco”
Un servicio con muchas consultas de reporting sufría por group-bys lentos. Alguien vio muchos Created_tmp_disk_tables y decidió que la solución era subir tmp_table_size y max_heap_table_size dramáticamente. En papel, menos tablas temporales en disco significa menos I/O, lo que significa consultas más rápidas. Esa es la seducción.
El cambio funcionó en pruebas. Luego llegó a producción. El uso de memoria subió con concurrencia. No linealmente. De repente. El servicio no se ralentizó; se cayó. El OOM killer pasó a formar parte de las reuniones diarias.
Lo sutil: las tablas temporales en memoria consumen memoria por sesión, y consultas complejas pueden crear estructuras temporales grandes. Bajo concurrencia, esa “ayuda” de memoria se convierte en una responsabilidad sin límites. En un VPS de 2GB, sin límites es otra palabra para “pronto”.
La solución correcta fue múltiple y un poco molesta: añadir índices adecuados para reducir la necesidad de tablas temporales, reescribir los peores reports y aceptar que algunos derrames a disco son normales. Mantuvimos tmp_table_size modesto para que el modo de fallo fuera “reporte lento”, no “base de datos muerta”.
También movieron los workloads de reporting a horas con menos carga e introdujeron un réplica más tarde. La gran victoria no fue la perilla. Fue decidir que la carga no pertenecía al primario en horario de negocio.
3) Práctica aburrida pero correcta que salvó el día: “Slow query log + un índice por incidente”
Otro equipo corría un SaaS pequeño en MariaDB con un presupuesto de 2GB. Nada sofisticado. Tenían una práctica que parecía casi demasiado básica: slow query logging siempre activado, rotado y revisado tras cualquier incidente.
Cuando lanzaron una nueva función, la latencia p95 se duplicó. No empezaron cambiando buffer pools o flushing. Sacaron el slow log y encontraron una consulta que examinaba demasiadas filas para un simple panel de usuario. No era maliciosa. Simplemente faltaba el índice compuesto correcto.
Añadieron el índice, desplegaron y la latencia volvió a la normalidad. Sin cambios arriesgados en configuración. Sin compromisos temporales de durabilidad. Sin proyectos de rendimiento de semana. Solo bucles de feedback disciplinados.
Más tarde, cuando ajustaron configuración, fue informada por cuellos de botella observados—picos de conexiones, ratios de tablas temporales, await de disco. El secreto del equipo no fue brillantez. Fue negarse a operar a ciegas.
Si ejecutas en hardware pequeño, la observabilidad aburrida no es sobrecarga. Es renta.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: mysqld es asesinado, reinicia o desaparece
Causa raíz: OOM killer por memoria por conexión sin límites, buffer pool sobredimensionado o app y BD compitiendo por RAM.
Solución: Reduce max_connections, reduce buffers por conexión (sort/join/read), ajusta innodb_buffer_pool_size y asegúrate de que la app use pooling. Confírmalo con dmesg.
2) Síntoma: errores de “Too many connections” durante picos
Causa raíz: Rotación de conexiones y falta de pooling; o max_connections demasiado bajo para la concurrencia pico.
Solución: Implementa pooling (a nivel de app o proxy), acorta wait_timeout para limpiar conexiones inactivas y sube max_connections solo si limitas memoria por conexión.
3) Síntoma: picos aleatorios de latencia, especialmente en escrituras
Causa raíz: Stalls de disco (fsync, checkpointing, almacenamiento barato), o flushing en background compitiendo con consultas en primer plano.
Solución: Verifica con iostat. Ajusta capacidad redo modestamente, mantén durabilidad a menos que aceptes explícitamente pérdida de datos y reduce la amplificación de escritura (índices, tamaño de lotes).
4) Síntoma: CPU alta, load average sube, pero las consultas no son “tan grandes”
Causa raíz: Demasiados hilos en ejecución por concurrencia excesiva; consultas ineficientes; índices faltantes provocando escaneos.
Solución: Baja concurrencia (conexiones), usa slow logs, añade índices y considera thread pool (especialmente en MariaDB) para reducir thrash del scheduler.
5) Síntoma: replicación con lag (si tienes réplica)
Causa raíz: La I/O de la réplica no da abasto (disco), o el hilo SQL está bloqueado por transacciones largas o consultas pesadas.
Solución: Revisa el estado de la réplica, reduce carga de escritura y optimiza las consultas más lentas. En 2GB, correr replicación además de app y BD es ambicioso; sé honesto con los recursos.
6) Síntoma: tablas temporales “misteriosamente” llenan disco
Causa raíz: Grandes sorts/group-bys que desbordan a disco; tmpdir en un filesystem pequeño; índices insuficientes.
Solución: Coloca tmpdir en un filesystem con espacio, pero principalmente arregla consultas/índices. No aumentes tmp_table_size en 2GB sin pensar.
7) Síntoma: el estado “Sending data” domina el processlist
Causa raíz: Scans de tabla/índice y evaluación pesada de filas; a veces mal orden de joins.
Solución: Usa slow log + EXPLAIN, añade índices adecuados, reduce columnas seleccionadas y paginación correcta. La configuración no te salvará de escanear millones de filas por diversión.
Listas de verificación / plan paso a paso
Paso a paso: estabiliza primero (evita caídas), luego optimiza
- Confirma la versión y el motor: MySQL vs MariaDB, versión exacta y que las tablas sean InnoDB.
- Establece límites de conexión: elige un
max_connectionsrealista (60–120 en 2GB según app y costo de cada consulta). - Limita la memoria por conexión: mantiene sort/join buffers modestos; no “ayudes” a cada consulta con buffers enormes.
- Dimensiona correctamente el buffer pool: 512M para app+BD, ~1G para BD-only como punto de partida.
- Activa slow query logging: siempre, con rotación de logs configurada.
- Verifica que no estás haciendo swap: swap no es caché de base de datos, es un impuesto de rendimiento.
- Mide latencia de disco: usa
iostat; si await es malo, acepta que estás limitado por almacenamiento. - Arregla las 3 consultas principales: añade índices, reescribe, reduce escaneos. No empieces cambiando 40 perillas.
- Configura alertas: uso de swap, Max_used_connections cerca del límite, util disco y cambios en volumen de queries lentas.
- Vuelve a probar bajo carga: prueba en staging o un ramp controlado en producción. Observa memoria y disco.
Límites de cordura para un VPS de 2GB (reglas prácticas)
- max_connections: 40–80 si app+BD; 80–150 si BD-only y la carga por conexión es ligera.
- innodb_buffer_pool_size: 512M (compartido) a 1G (BD-only). Rara vez más.
- tmp_table_size / max_heap_table_size: 32M–64M. Más grande es trampa bajo concurrencia.
- sort/join buffers: 1M–4M máximo en la mayoría de casos con poca RAM. Valores grandes son para cargas especializadas con baja concurrencia.
- Durabilidad: mantén
innodb_flush_log_at_trx_commit=1a menos que tengas tolerancia explícita a pérdida de datos.
Higiene operacional que previene “lentitud misteriosa”
- Rota slow logs y error logs para que el disco no se llene.
- Mantén tablas e índices recortados; evita índices sin uso que amplifiquen escrituras.
- Ejecuta
ANALYZE TABLEcuando los planes cambien tras grandes cambios de datos (con moderación, fuera de pico). - Planifica las actualizaciones; saltos de versión mayores de última hora en un VPS de 2GB son cómo conoces las peores partes del optimizador.
Preguntas frecuentes
1) ¿MariaDB es más rápida que MySQL en un VPS de 2GB?
A veces, para cargas específicas, pero el factor dominante en 2GB suele ser la presión de memoria y la latencia de disco. Elige el motor que puedas operar con limpieza y luego arregla las consultas.
2) ¿Cuál es la perilla más importante para evitar caídas?
max_connections, porque limita indirectamente la explosión de memoria por conexión. Justo detrás: mantener modestos los buffers por conexión.
3) ¿Qué tamaño debería tener innodb_buffer_pool_size en 2GB?
Empieza en 512M si la caja también ejecuta tu app. Si es solo BD, empieza alrededor de 1G. Aumenta solo si no haces swap y los misses de cache realmente perjudican rendimiento.
4) ¿Debería habilitar swap en un VPS de base de datos?
Swap puede prevenir una muerte inmediata por OOM, pero también puede crear largos stalls de latencia. Si usas swap, trátalo como un cojín de emergencia y mantén la memoria de BD conservadora. Si el swap es rutinario, estás subdimensionado o mal configurado.
5) ¿Es aceptable innodb_flush_log_at_trx_commit=2?
Es aceptable solo si el negocio acepta perder hasta aproximadamente un segundo de transacciones comprometidas en un crash. En un VPS único que alberga datos primarios, la durabilidad por defecto suele ser la opción correcta.
6) ¿Por qué no simplemente hacer tmp_table_size enorme para evitar tablas temporales en disco?
Porque en un servidor pequeño eso es una granada de concurrencia. Unas pocas consultas concurrentes pueden consumir cientos de MB cada una en el peor caso. Arregla la consulta y los índices primero.
7) ¿Debería ejecutar app y base de datos juntos en 2GB?
Puedes, pero debes ser estricto: limita memoria de BD, fija conexiones y reduce conteos de workers en la app. Si la app crece, sepáralos temprano—escalar verticalmente en una sola máquina pequeña tiene aristas muy afiladas.
8) ¿Cuál es la forma más rápida de saber si estoy limitado por CPU o por disco?
Usa iostat -xz. Alto %iowait, alto await y alto %util apuntan a disco. Si iowait es bajo pero las CPU están saturadas, estás limitado por CPU/consultas/concurrencia.
9) ¿Necesito ajustar table_open_cache en 2GB?
Solo si ves churn (Open_tables cerca del límite de cache y problemas de rendimiento ligados a operaciones de metadatos). Manténlo moderado; caches enormes consumen memoria y descriptores de archivo.
10) Si solo puedo arreglar una cosa en la aplicación, ¿qué es?
Pooling de conexiones y timeouts sensatos. Reduce la necesidad de altos max_connections y baja dramáticamente la volatilidad de memoria.
Conclusión: pasos prácticos siguientes
Si quieres un VPS de 2GB cuya base de datos no se caiga, deja de pensar en “mejor rendimiento” y empieza a pensar en “fallo acotado”. Limita conexiones. Mantén pequeños los buffers por conexión. Dale a InnoDB un buffer pool que se ajuste a la realidad, no al ego. Luego usa el slow query log para ganar mejoras reales en rendimiento de la manera honesta: haciendo menos trabajo.
Haz esto luego, en orden
- Elige Perfil A (compartido) o Perfil B (solo BD) y aplícalo limpiamente; reinicia el servicio.
- Ejecuta las comprobaciones de diagnóstico rápido:
free,dmesg,iostat, Threads_connected/Max_used_connections. - Revisa el slow log por los mayores culpables; arregla la peor consulta con un índice o reescritura.
- Configura alertas para uso de swap y Max_used_connections cerca de tu tope.
- Si aún alcanzas límites: separa app y BD, o mejora RAM/almacenamiento. En 2GB, “aumentar” suele ser la optimización más barata que existe.
MySQL y MariaDB pueden comportarse bien en un VPS de 2GB. El truco no es encontrar la configuración perfecta. Es negarse a permitir que una sola configuración haga el fallo ilimitado.