MySQL/MariaDB en Docker: los valores por defecto que arruinan el rendimiento

¿Te fue útil?

Algunas bases de datos fallan de forma ruidosa. MySQL y MariaDB en Docker a menudo fallan con cortesía: siguen atendiendo tráfico mientras, en silencio, convierten tu SSD en un instrumento de percusión.

El acantilado de rendimiento normalmente no es tu esquema ni tu “consulta rara.” Son los valores por defecto: drivers de almacenamiento de Docker, semánticas del sistema de ficheros, límites de memoria, comportamiento de fsync y algunos ajustes de la base de datos que eran razonables en 2008 y son desastrosos dentro de un contenedor en 2026.

El verdadero enemigo: desajuste entre las expectativas de la base de datos y la realidad del contenedor

MySQL y MariaDB nacieron asumiendo un contrato bastante aburrido con el sistema operativo:

  • “Cuando llamo a fsync, mis datos están en almacenamiento estable.”
  • “Cuando asigno memoria, se queda siendo mía.”
  • “Cuando escribo en disco, la latencia es algo predecible.”
  • “Cuando uso tablas temporales, tengo suficiente disco local.”

Docker no rompe ese contrato a propósito. Solo añade capas: sistemas de ficheros en unión, copy-on-write, límites de cgroups, sistemas de ficheros virtualizados en macOS/Windows y drivers de volumen con características de durabilidad y latencia muy distintas. Tu base de datos sigue intentando ser correcta. Tu plataforma de contenedores intenta ser flexible. Correctitud y flexibilidad pueden coexistir—si dejas de confiar en los valores por defecto.

Aquí va la tesis: trata una base de datos en contenedor como un appliance de almacenamiento. El almacenamiento es una dependencia de primera clase. La CPU suele estar bien. La red rara vez es el primer problema. Casi siempre es la latencia I/O, el comportamiento de fsync o la presión de memoria disfrazada de “consultas lentas”.

Hechos interesantes y un poco de historia (porque los valores por defecto tienen trasfondo)

Estos son hechos pequeños, pero cada uno explica un momento de “¿por qué es así?” que te encontrarás al tunear MySQL/MariaDB en Docker:

  1. InnoDB se convirtió en el motor por defecto en MySQL 5.5, principalmente porque maneja mejor crashes y concurrencia que MyISAM. Eso también significa que heredaste las fuertes opiniones de InnoDB sobre fsync y redo logs.
  2. MariaDB se bifurcó de MySQL en 2009 tras la adquisición de Sun por Oracle. La bifurcación preservó compatibilidad, pero los valores operativos y las características de rendimiento han divergido de maneras sutiles con el tiempo.
  3. Los sistemas overlay de Docker fueron diseñados para imágenes, no para bases de datos. Copy-on-write es fantástico para capas de archivos de aplicación; es un impuesto para cargas que escriben bloques pequeños y aleatorios todo el día.
  4. El buffer de doble escritura de InnoDB existe porque ocurren escrituras parciales de página (corte de energía, comportamientos extraños del controlador, bugs del kernel). Intercambia amplificación de escritura por seguridad—un trade-off feo pero generalmente correcto.
  5. La query cache de MySQL se eliminó en MySQL 8.0 porque causaba contención y rendimiento impredecible. Si todavía “estás afinando query cache” en un contenedor, probablemente usas una versión antigua y tienes problemas mayores.
  6. Las semánticas de O_DIRECT y fsync en Linux varían según el sistema de ficheros y las opciones de montaje. La durabilidad de la base de datos no es una verdad universal; es un acuerdo negociado entre capas de software.
  7. Los cgroups hicieron que “memoria disponible” fuera una mentira dentro de contenedores durante años. MySQL moderno lee mejor los límites de cgroup, pero muchas imágenes y versiones antiguas siguen dimensionando buffers según la RAM del host.
  8. Los picos de latencia en SSD son reales: cuando saturas la cola del dispositivo o disparas la recolección de basura, la latencia sube primero y el rendimiento después. Tu base de datos hará time out mucho antes de que tu monitor muestre “disco 100% ocupado.”

Y una cita de fiabilidad—porque sigue siendo la única lección que importa:

“La esperanza no es una estrategia.” — General Gordon R. Sullivan

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

Si una instancia MySQL/MariaDB en contenedor está lenta, no empieces por SQL. Empieza por la física. La base de datos está esperando algo.

Primero: confirma qué significa “lento”

  • ¿Es mayor latencia por consulta, menor throughput o ambos?
  • ¿Son solo escrituras, solo lecturas o todo?
  • ¿Es periódico (picos) o constante?

Segundo: revisa la latencia de disco y la presión de fsync

  • Busca alto tiempo de fsync en los redo logs y stalls por páginas sucias.
  • Confirma que no estás escribiendo en overlay2 o en un volumen de red fino con latencia de sync terrible.

Tercero: revisa la memoria y los límites de cgroup

  • ¿Te está matando el kernel por OOM, hay swapping o la caché de páginas se está reduciendo hasta desaparecer?
  • ¿El buffer pool de InnoDB está dimensionado según la memoria del host en lugar del límite del contenedor?

Cuarto: revisa CPU steal y throttling

  • La inanición de CPU se hace pasar por “lentitud aleatoria” de la BD, especialmente en cargas con picos.

Quinto: solo ahora lee el slow query log

  • Si las consultas lentas esperan “waiting for handler commit” o I/O, vuelves a disco y fsync.
  • Si son bound por CPU con planes malos, entonces afina SQL e índices.

Ese es el orden. Cuando la gente lo salta, afinan lo equivocado durante semanas.

Los valores por defecto que arruinan el rendimiento (y qué hacer en su lugar)

1) Escribir tu datadir en overlay2 (o cualquier filesystem en unión)

El antipatrón clásico de Docker: ejecutas MySQL en un contenedor, olvidas montar un volumen real y el datadir vive en la capa escribible del contenedor. Funciona. Rinde terriblemente en benchmarks. Además complica upgrades y backups porque tu estado queda pegado a una capa efímera.

Por qué duele: overlay2 es copy-on-write. Las bases de datos escriben muchas actualizaciones pequeñas, e InnoDB escribe en patrones que disparan churn de metadatos. Incluso si “tienes SSD”, la capa del sistema de ficheros puede añadir latencia y sobrecarga de CPU.

Haz esto en su lugar: coloca /var/lib/mysql en un volumen nombrado o un bind mount en un sistema de ficheros real. En producción, prefiere volúmenes locales en XFS/ext4 con opciones de montaje sensatas, o un volumen CSI cuidadosamente elegido con latencia de sync conocida.

2) Asumir que “volumen Docker” significa “rápido” (no siempre)

Un “volumen” Docker es una abstracción. El almacenamiento subyacente puede ser un directorio local en ext4. O NFS. O un bloque cloud. O un sistema de ficheros distribuido. El perfil de latencia puede ser “aceptable” o “¿por qué el commit tarda 200ms?”

Haz esto en su lugar: mide la latencia de fsync en el driver de volumen exacto que usas. Trata el almacenamiento como una dependencia con un SLO.

3) Valores por defecto de InnoDB de durabilidad + almacenamiento lento = tristeza

Dos variables importan más para cargas con muchas escrituras:

  • innodb_flush_log_at_trx_commit (habitualmente 1 por defecto)
  • sync_binlog (a menudo 1 en setups más cautelosos, pero varía)

innodb_flush_log_at_trx_commit=1 significa que InnoDB vacía el redo a disco en cada commit. En almacenamiento con alta latencia de fsync, la latencia de commit se convierte en latencia de almacenamiento. Si también tienes binary logging y sync_binlog=1, pagas dos veces.

Qué hacer: decide tus requisitos de durabilidad explícitamente. Para muchos sistemas internos, innodb_flush_log_at_trx_commit=2 es un trade-off de riesgo aceptable. Para sistemas financieros, quizá no. Pero toma la decisión; no la heredes.

Chiste #1: Si pones innodb_flush_log_at_trx_commit=0 en producción, tu base de datos ahora funciona con vibras y optimismo.

4) Binary logs en almacenamiento lento (y sin plan)

Los binlogs no son opcionales si quieres replicación o recuperación punto en el tiempo. También son un flujo constante de escrituras que pueden convertirse en la I/O dominante. En Docker, la gente olvida rutinariamente:

  • Dónde se almacenan los binlogs (mismo datadir a menos que se configure)
  • Qué tan rápido crecen bajo carga de escrituras
  • Que purgarlos requiere una política

Arreglo: dimensiona el almacenamiento para retención de binlogs, habilita expiración automática y monitoriza.

5) Límites de memoria del contenedor + valores por defecto del buffer pool de InnoDB = thrash de caché

Dentro de un contenedor, la memoria es política. MySQL puede ver la memoria del host (según versión y configuración) y felizmente asignar un buffer pool grande. Luego el límite de cgroup entra en acción y el kernel empieza a matar procesos o reclamar agresivamente.

Síntomas: stalls periódicos, OOM kills, picos de latencia “aleatorios” en consultas y un SO que parece tranquilo mientras el contenedor arde.

Solución: configura innodb_buffer_pool_size según el límite del contenedor, no la RAM del host. Deja espacio para:

  • buffers de conexión (las asignaciones por conexión suman)
  • buffers de sort/join bajo carga
  • overhead interno de InnoDB
  • caché del sistema de ficheros (sí, sigue siendo relevante)

6) Demasiadas conexiones (porque los valores por defecto son generosos)

max_connections a menudo está alto “por si acaso.” En contenedores, ese “por si acaso” se vuelve “directo a swap.” Cada conexión puede asignar memoria por hilo. Bajo picos, la memoria se dispara y el kernel hace lo que los kernels hacen: te castiga por mentir sobre la capacidad.

Solución: limita max_connections, usa pooling y dimensiona buffers por hilo de forma sensata. Tu base de datos no es un local de conciertos; no necesita espacio infinito de pie.

7) tmpdir y tablas temporales en el disco equivocado

Los sorts grandes, operaciones ALTER TABLE y consultas complejas pueden volcar a disco. Dentro de contenedores, tmpdir puede estar en una pequeña raíz del filesystem, no en tu volumen de datos grande.

Solución: configura tmpdir a una ruta en el mismo volumen rápido (o en un volumen rápido dedicado) y monitoriza el espacio libre. En Kubernetes, aquí es donde los límites de almacenamiento efímero sorprenden a la gente.

8) Tamaño de redo log ausente o mal ajustado

Redo logs demasiado pequeños causan checkpoints frecuentes, lo que incrementa el flushing en background y amplifica la presión de escritura. Demasiado grandes pueden aumentar el tiempo de recuperación tras crash. En contenedores, el caso de “demasiado pequeño” es más común porque los valores por defecto son conservadores y el almacenamiento suele ser más lento de lo esperado.

Solución: ajusta la capacidad del redo log según tu tasa de escritura y objetivos de recuperación. En MySQL moderno pensarás en innodb_redo_log_capacity; en MariaDB y MySQL antiguos trabajarás con tamaño y número de archivos de log.

9) “Usaremos almacenamiento en red” (y entonces fsync es 20ms)

El almacenamiento remoto puede funcionar. También puede convertir tu camino de commit en un viaje de ida y vuelta por la red y tres capas de cache. Muchos sistemas de almacenamiento distribuido están optimizados para throughput, no para latencia de fsync.

Solución: valida la latencia de escritura sincronizada antes de confiarle la base de datos. Si debes usar volúmenes en red, prefiere los que tengan semánticas de durabilidad predecibles y baja latencia cola. Mide p99, no promedios.

10) No anclar CPU u observar throttling

El rendimiento de la base de datos ama la consistencia. El throttling y la contención de CPU pueden parecer esperas de I/O porque el hilo de la base de datos no recibe CPU para terminar trabajo. En contenedores, puedes ejecutar MySQL en CPU “best effort” mientras trabajos batch estampan el nodo.

Solución: establece requests/limits de CPU apropiados (o cuotas CPU de Docker) y vigila métricas de throttling. Si ves throttling bajo carga normal, estás insuficientemente aprovisionado o mal configurado.

11) Ejecutar en Docker Desktop para macOS/Windows y esperar I/O nativo de Linux

Los entornos de desarrollo son donde nacen mitos de rendimiento. Docker Desktop usa una VM. Los bind mounts pasan por capas de traducción. I/O de ficheros puede ser dramáticamente más lento, especialmente patrones con muchos sync como el redo de InnoDB.

Solución: para pruebas de rendimiento realistas, ejecuta en Linux con un volumen real. Para desarrollo local, acepta I/O más lento o cambia a volúmenes nombrados en lugar de bind mounts.

12) Tratar la configuración de MySQL como “dentro del contenedor” en vez de “parte del servicio”

Si tu configuración está baked en la imagen sin externalizar, eventualmente desplegarás un cambio que requiere reconstruir imágenes bajo presión. Así es como incidentes “simples” se vuelven largos.

Solución: monta la configuración, versiona y hazla observable (SHOW VARIABLES debería coincidir con tu intención).

Tareas prácticas: comandos, salidas y decisiones (12+)

Estas son comprobaciones de nivel producción. Cada una incluye: un comando, qué significa la salida y la decisión que tomas a partir de ello.

Task 1: Confirmar dónde está escribiendo MySQL realmente los datos (overlay vs volumen)

cr0x@server:~$ docker inspect -f '{{ .Mounts }}' mysql-prod
[{volume mysql-data /var/lib/docker/volumes/mysql-data/_data /var/lib/mysql local  true }]

Significado: /var/lib/mysql está en un volumen nombrado, no en la capa del contenedor.

Decisión: Si no ves un mount para /var/lib/mysql, detente y arregla eso antes de tunear cualquier otra cosa.

Task 2: Identificar el driver de almacenamiento de Docker (overlay2, devicemapper, etc.)

cr0x@server:~$ docker info --format '{{ .Driver }}'
overlay2

Significado: overlay2 está en uso. Bien para contenedores. No ideal para estado de bases de datos a menos que montes volúmenes correctamente.

Decisión: Si el datadir no es un volumen, espera problemas. Si lo es, en su mayoría evitaste las penalizaciones de overlay2.

Task 3: Comprobar tipo de filesystem y opciones de montaje que respaldan el volumen

cr0x@server:~$ docker run --rm -v mysql-data:/var/lib/mysql alpine sh -c "df -T /var/lib/mysql && mount | grep ' /var/lib/docker'"
Filesystem     Type  1K-blocks      Used Available Use% Mounted on
/dev/nvme0n1p2  xfs  488245288 124332456 363912832  26% /var/lib/docker
/dev/nvme0n1p2 on /var/lib/docker type xfs (rw,relatime,attr2,inode64,logbufs=8,logbsize=32k)

Significado: Este volumen vive en XFS con opciones típicas.

Decisión: Si ves NFS o un montaje remoto aquí, trata la latencia de fsync como tu sospechoso principal para rendimiento de escritura.

Task 4: Comprobar límite de memoria del contenedor vs lo que MySQL cree que tiene

cr0x@server:~$ docker inspect -f '{{ .HostConfig.Memory }}' mysql-prod
2147483648
cr0x@server:~$ docker exec -it mysql-prod bash -lc "cat /sys/fs/cgroup/memory.max 2>/dev/null || cat /sys/fs/cgroup/memory/memory.limit_in_bytes"
2147483648

Significado: El contenedor está limitado a 2 GiB.

Decisión: Ajusta innodb_buffer_pool_size a algo como 1.0–1.4 GiB dependiendo de la carga y el número de conexiones, no “80% del host”.

Task 5: Verificar tamaño real del buffer pool de InnoDB

cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -N -e \"SHOW VARIABLES LIKE 'innodb_buffer_pool_size';\""
innodb_buffer_pool_size	1073741824

Significado: El buffer pool es de 1 GiB.

Decisión: Si es pequeño (como 128 MiB) en una instancia ocupada, espera lecturas constantes desde disco y bajo rendimiento.

Task 6: Comprobar OOM kills y señales de presión de memoria

cr0x@server:~$ docker exec -it mysql-prod bash -lc "dmesg -T | tail -n 5"
[Fri Jan  3 09:28:41 2026] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=docker-3b1a...,mems_allowed=0,oom_memcg=/docker/3b1a...
[Fri Jan  3 09:28:41 2026] Killed process 1973 (mysqld) total-vm:3121044kB, anon-rss:1632100kB, file-rss:0kB, shmem-rss:0kB

Significado: El kernel mató mysqld debido a límites de cgroup de memoria.

Decisión: Reduce el uso de memoria de MySQL (buffers, conexiones) o aumenta el límite de memoria del contenedor. Además arregla bucles de reinicio y analiza riesgos de corrupción.

Task 7: Inspeccionar presión de fsync en redo log y stalls de commit

cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -e \"SHOW ENGINE INNODB STATUS\\G\" | sed -n '1,120p'"
...
Log sequence number          912345667890
Log flushed up to            912345660000
Last checkpoint at           912345100000
0 pending log flushes, 0 pending chkp writes
...

Significado: Si ves muchos pending log flushes o una gran brecha entre “Log sequence number” y “Log flushed up to”, tu almacenamiento no puede seguir el ritmo de fsync del redo.

Decisión: Mejora la latencia de almacenamiento, ajusta la capacidad de redo, considera trade-offs de durabilidad (innodb_flush_log_at_trx_commit) y reduce la amplificación de escritura.

Task 8: Confirmar variables relacionadas con durabilidad (las que la gente “asume”)

cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -e \"SHOW VARIABLES WHERE Variable_name IN ('innodb_flush_log_at_trx_commit','sync_binlog','innodb_doublewrite');\""
+------------------------------+-------+
| Variable_name                | Value |
+------------------------------+-------+
| innodb_doublewrite           | ON    |
| innodb_flush_log_at_trx_commit | 1   |
| sync_binlog                  | 1     |
+------------------------------+-------+

Significado: Estás pagando el coste de durabilidad completo (redo fsync por commit + binlog fsync por commit).

Decisión: Manténlo si lo necesitas. Si no, cámbialo intencionalmente y documenta el perfil de riesgo.

Task 9: Medir latencia real de disco desde dentro del contenedor (rápido y sucio)

cr0x@server:~$ docker exec -it mysql-prod bash -lc "dd if=/dev/zero of=/var/lib/mysql/dd-test.bin bs=4k count=25000 oflag=dsync 2>&1 | tail -n 2"
25000+0 records in
25000+0 records out

Significado: Con oflag=dsync, cada bloque se sincroniza. Si este comando tarda segundos más de lo esperado, tu latencia de write sync es mala. (La línea de tiempo del dd se omite aquí porque varía, pero la verás en tu sistema.)

Decisión: Si las escrituras sincronizadas son lentas, deja de culpar a los índices. Arregla el almacenamiento o los ajustes de durabilidad.

Task 10: Comprobar si usas por accidente un bind mount con semánticas pobres (Desktop, CIFS, etc.)

cr0x@server:~$ docker inspect -f '{{ range .Mounts }}{{ .Type }} {{ .Source }} -> {{ .Destination }}{{ "\n" }}{{ end }}' mysql-prod
volume /var/lib/docker/volumes/mysql-data/_data -> /var/lib/mysql

Significado: Es un volumen gestionado por Docker; buen comienzo.

Decisión: Si ves bind a una ruta que vive en un FS remoto/lento, has encontrado un cuello de botella probable.

Task 11: Comprobar comportamiento de tablas temporales y tmpdir (spills a disco)

cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -e \"SHOW VARIABLES LIKE 'tmpdir'; SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';\""
+---------------+----------+
| Variable_name | Value    |
+---------------+----------+
| tmpdir        | /tmp     |
+---------------+----------+
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| Created_tmp_disk_tables | 48291 |
+-------------------------+-------+

Significado: tmpdir es /tmp, y tienes muchas tablas temporales en disco.

Decisión: Pon tmpdir en un volumen rápido con suficiente espacio. También investiga las consultas que causan spills temporales.

Task 12: Detectar throttling de CPU dentro de contenedores

cr0x@server:~$ docker stats --no-stream mysql-prod
CONTAINER ID   NAME        CPU %     MEM USAGE / LIMIT     MEM %     NET I/O       BLOCK I/O     PIDS
3b1a1c2d3e4f   mysql-prod  398.23%   1.62GiB / 2GiB        81.0%     1.2GB / 900MB  40GB / 12GB  58

Significado: Alto uso de CPU puede ser normal. La clave es si estás alcanzando cuotas de CPU (no mostrado aquí) y viendo picos de latencia correlacionados con throttling.

Decisión: Si CPU está alta y latencia alta, revisa planes de consulta. Si CPU está baja pero latencia alta, revisa I/O y locks primero.

Task 13: Confirmar que slow query log está activado y dónde escribe

cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -e \"SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'slow_query_log_file';\""
+----------------+-------+
| Variable_name  | Value |
+----------------+-------+
| slow_query_log | ON    |
+----------------------+-------------------------------+
| Variable_name        | Value                         |
+----------------------+-------------------------------+
| slow_query_log_file  | /var/lib/mysql/mysql-slow.log |
+----------------------+-------------------------------+

Significado: El logging está activado y va al volumen de datos.

Decisión: Si cae en el filesystem del contenedor y rotas mal, puedes llenar la raíz y hacer que MySQL caiga. Pon los logs en almacenamiento persistente y rote.

Task 14: Comprobar cuántos threads y conexiones estás ejecutando

cr0x@server:~$ docker exec -it mysql-prod bash -lc "mysql -e \"SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW VARIABLES LIKE 'max_connections';\""
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Threads_connected | 412   |
+-------------------+-------+
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 800   |
+-----------------+-------+

Significado: Estás con muchas conexiones activas. Incluso si las consultas son rápidas, la memoria por hilo puede arruinar tu contenedor.

Decisión: Añade pooling, limita max_connections y mide el crecimiento de memoria durante picos.

Tres mini-historias corporativas (anonimizadas, dolorosamente familiares)

Mini-historia 1: El incidente causado por una suposición errónea

Migraron una app legacy de VMs a contenedores porque “es solo empaquetado.” La base de datos también fue migrada, porque sonaba moderno. El equipo usó una imagen MySQL popular, la corrió en Docker Swarm y usó un bind mount a un directorio del host que parecía suficientemente persistente.

En el tercer día, la latencia p95 de escritura se duplicó, y luego se duplicó de nuevo. La app no se cayó; simplemente se volvió más lenta hasta que la UI parecía hecha por un pasante muy paciente. Los ingenieros se metieron en planes de consulta, añadieron índices y discutieron sobre comportamiento del ORM. El slow query log mostraba INSERTs inocentes tardando cientos de milisegundos.

Eventualmente alguien revisó el host: la ruta del bind mount vivía en un share de red montado “por conveniencia”, porque Ops quería acceso fácil para backups. Nadie lo llamó NFS en el Docker compose; era simplemente un directorio. La latencia de fsync era brutal y las latencias cola eran peores.

La solución fue aburrida: mover el datadir a almacenamiento de bloque local con latencia de sync predecible, luego implementar backups adecuados vía dumps lógicos y binlogs (y más tarde backups físicos). La latencia bajó inmediatamente. Los índices que añadieron no eran dañinos, pero no eran el problema.

Mini-historia 2: La optimización que salió mal

Otra compañía tenía un servicio con muchas escrituras. Alguien leyó que innodb_flush_log_at_trx_commit=2 puede ser más rápido, y tenían razón. Lo cambiaron, vieron buenos benchmarks y lo desplegaron ampliamente.

Durante un mes todo parecía bien. El throughput mejoró y los gráficos de almacenamiento se calmaron. Luego un nodo sufrió un crash fuerte—kernel panic, apagado no limpio, el espectáculo completo. MySQL se reinició, recuperó y el servicio volvió. Salvo que algunas escrituras recientemente reconocidas habían desaparecido.

Nadie estaba feliz, pero nadie sorprendido. El servicio no tenía un contrato de durabilidad claro: reconocía transacciones antes de que fueran plenamente durables, y la capa de aplicación asumía “reconocido significa permanente.” La optimización era técnicamente correcta pero operativamente sin dueño.

Eventualmente restauraron durabilidad estricta en tablas críticas e implementaron idempotencia en paths de escritura donde toleraban replay. La solución real no fue solo la configuración; fue alinear la semántica de la aplicación con la realidad del almacenamiento.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Un equipo que corría MariaDB en contenedores tenía un hábito: cada queja de rendimiento empezaba con las mismas tres comprobaciones—tipo de volumen, latencia de fsync y margen de memoria del contenedor. No era glamouroso, pero hacía los incidentes cortos.

Durante un pico de tráfico vieron que el lag de replicación subía. Los ingenieros de app culparon inmediatamente a “una mala rollout de consulta.” El SRE revisó la latencia de disco en primario y réplicas. Las réplicas estaban bien; el primario tenía picos periódicos en latencia de escritura sincronizada.

La causa raíz no era MySQL en absoluto. Un escaneo antivirus programado (sí, en Linux) estaba rastreando el punto de montaje del volumen y golpeando metadata. Como tenían el hábito de revisar almacenamiento primero, lo encontraron en minutos, no horas.

Excluyeron los directorios de la base de datos del escaneo, lo documentaron y siguieron. Lo mejor: nadie tuvo que aprender un nuevo parámetro de base de datos a las 2 a.m.

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

Esta sección está pensada para usarse durante un incidente cuando alguien pregunta por qué “la BD está lenta” y la sala se está poniendo ruidosa.

1) Síntoma: picos de latencia en INSERT/UPDATE, lecturas mayormente bien

  • Causa raíz: latencia de fsync (flush de redo, sync de binlog) en almacenamiento lento o volumen de red.
  • Solución: mueve datadir/binlogs a almacenamiento de menor latencia; mide escrituras sync; ajusta durabilidad solo con firma de riesgo explícita.

2) Síntoma: MySQL se reinicia “aleatoriamente”, el contenedor sale con 137

  • Causa raíz: OOM kill por límite de memoria de cgroup; buffer pool + memoria por conexión excedieron el límite.
  • Solución: reduce buffer pool, limita conexiones, usa pooling, aumenta límite de memoria y verifica uso bajo carga.

3) Síntoma: consultas lentas muestran “Copying to tmp table” o aumentan tablas temporales en disco

  • Causa raíz: tmpdir en filesystem lento o pequeño; spills temporales por operaciones sort/group by.
  • Solución: mueve tmpdir a volumen rápido; aumenta umbrales de tmp table con cuidado; corrige consultas/índices que causan spills.

4) Síntoma: “Disco lleno” pero el volumen del datadir tiene espacio

  • Causa raíz: binlogs o slow logs creciendo sin rotación; tmpdir en la raíz del contenedor; filesystem root de Docker llenándose.
  • Solución: configura rotación de logs; pon expiración de binlog; mueve tmpdir y logs al volumen correcto; monitoriza tanto Docker root como volúmenes de BD.

5) Síntoma: throughput se colapsa durante backups

  • Causa raíz: método de backup saturando I/O o bloqueando tablas; snapshot en almacenamiento que penaliza copy-on-write; lectura desde el mismo volumen donde se escriben redo.
  • Solución: programa backups con límites de I/O; usa réplicas para backups; usa backups físicos si procede; mide impacto y fija un SLO.

6) Síntoma: lag de replicación crece tras mover a contenedores

  • Causa raíz: réplicas en otra clase de almacenamiento; throttling de CPU; coste de fsync en binlog; espacio de relay log demasiado pequeño o disco lento.
  • Solución: estandariza almacenamiento; revisa throttling; ajusta settings de replicación; asegura relay logs y datadir en volúmenes persistentes rápidos.

7) Síntoma: el rendimiento está bien hasta un pico de carga, luego todo hace time out

  • Causa raíz: tormenta de conexiones; max_connections muy alto; creación de hilos y explosión de memoria.
  • Solución: pon pooling; limita concurrencia en la capa de app; fija max_connections según capacidad; considera thread pool donde esté disponible.

8) Síntoma: CPU parece baja pero las consultas son lentas

  • Causa raíz: espera por I/O, contención de locks o throttling no visible en métricas naïve.
  • Solución: revisa InnoDB status para esperas; comprueba latencia de disco; revisa métricas de throttling del contenedor; inspecciona esperas de locks.

Chiste #2: Los contenedores no hacen que tu base de datos sea más rápida; solo la ayudan a viajar con sus malos hábitos.

Listas de verificación / plan paso a paso

Paso a paso: MySQL/MariaDB en Docker listo para producción

  1. Elige la clase de almacenamiento primero. Almacenamiento local con SSD y block storage supera al almacenamiento en red para commits de baja latencia, salvo que hayas probado lo contrario.
  2. Monta el datadir como volumen. Sin excepciones. Si no puedes montar un volumen, no tienes una base de datos; tienes una demo.
  3. Decide la durabilidad explícitamente. Documenta innodb_flush_log_at_trx_commit y sync_binlog con una declaración clara de riesgos.
  4. Dimensiona memoria según límites del contenedor. Buffer pool + overhead + conexiones pico deben caber con holgura.
  5. Limita conexiones y aplica pooling. Usa un max_connections sensato. No dejes que tu app “descubra” el límite mediante outages.
  6. Pon tmpdir en un sitio seguro. Rápido, grande, monitorizado. Lo mismo para logs.
  7. Habilita observabilidad. Slow query log, performance schema (cuando corresponda), métricas clave de InnoDB.
  8. Planifica backups y restores como un sistema. Prueba el tiempo de restore y la corrección. Si no puedes restaurar, no tienes backups.
  9. Haz pruebas de carga en la misma plataforma. Los benchmarks en Docker Desktop son para sensaciones, no para capacity planning.
  10. Ensaya fallos. Kill -9 al contenedor en staging y verifica comportamiento de recuperación y supuestos de integridad de datos.

Decisiones mínimas de “día 1” (escríbelas)

  • ¿Dónde se almacena /var/lib/mysql y qué lo respalda?
  • ¿Cuál es la latencia p95 y p99 esperada de fsync?
  • ¿Cuál es el límite de memoria del contenedor y cómo se dimensiona el buffer pool?
  • ¿Cuál es el máximo de conexiones concurrentes permitido?
  • ¿Dónde viven binlogs y slow logs y cómo se rotan?
  • ¿Cuál es tu procedimiento de restore y cuándo lo probaste por última vez?

FAQ

1) ¿Debería ejecutar MySQL/MariaDB en Docker en producción?

Sí, si lo tratas como un servicio stateful con ingeniería de almacenamiento real. No, si tu plan es “funcionó en mi portátil.” Los contenedores no eliminan la necesidad de disciplina operacional; la hacen más urgente.

2) ¿Volumen nombrado o bind mount para /var/lib/mysql?

En servidores Linux, ambos pueden funcionar. Los volúmenes nombrados suelen ser más simples operativamente. Los bind mounts están bien si controlas el filesystem, las opciones de montaje y los backups. En Docker Desktop, los volúmenes nombrados suelen rendir mejor que los bind mounts para bases de datos.

3) overlay2 es mi driver de Docker—¿estoy condenado?

No. Solo estás condenado si tus archivos de base de datos viven en la capa escribible de overlay2. Usa un volumen real y en su mayoría evitas lo peor.

4) ¿Es innodb_flush_log_at_trx_commit=2 “seguro”?

Es una decisión de negocio disfrazada de perilla de configuración. Puede perder hasta ~1 segundo de transacciones en un crash (según el timing). Si la aplicación puede reintentar con seguridad (idempotencia) o tolerar pérdida menor, puede ser aceptable. Si no, mantenlo en 1.

5) ¿Por qué el rendimiento está bien por la mañana y terrible por la tarde?

A menudo: contención de almacenamiento (otros workloads en el nodo), jobs en background, backups, rotación de logs o recolección de basura del SSD bajo escrituras sostenidas. Mide la latencia de disco a lo largo del tiempo y correlaciónala con eventos de carga.

6) Mi slow query log muestra consultas simples tardando una eternidad. ¿Cómo?

Porque SQL “simple” aún puede requerir fsync en commit, esperar locks o esperar flushes del buffer pool. Busca esperas de commit, esperas de I/O y esperas de locks antes de reescribir consultas.

7) ¿Debería desactivar InnoDB doublewrite para acelerar?

Sólo si entiendes completamente el riesgo y tu stack de almacenamiento garantiza escrituras atómicas de página (muchos no lo hacen). Desactivarlo puede mejorar rendimiento de escritura pero aumenta el riesgo de corrupción por escrituras parciales. La mayoría debería dejarlo activado y arreglar la latencia del almacenamiento.

8) ¿Cómo sé si las tablas temporales me están perjudicando?

Revisa Created_tmp_disk_tables y vigila el uso de disco donde apunta tmpdir. Si las tablas temporales en disco suben durante periodos lentos, probablemente tienes consultas que spillan a disco o tmpdir es lento.

9) ¿La replicación es más lenta en contenedores?

No inherentemente. Pero los contenedores facilitan ejecutar primarios y réplicas en distintos tipos de almacenamiento, cuotas de CPU o vecinos ruidosos. La consistencia de infraestructura importa más que “contenedor vs VM.”

10) ¿Cuál es la única mejor mejora de rendimiento?

Pon tu datadir y redo/binlogs en almacenamiento de baja latencia y verifica el comportamiento de fsync. La mayoría de las quejas “MySQL es lento en Docker” son problemas de semánticas de almacenamiento disfrazados de SQL.

Conclusión: siguientes pasos que puedes hacer hoy

Si ejecutas MySQL/MariaDB en Docker y el rendimiento es un misterio, deja de adivinar. Haz esto en orden:

  1. Confirma que /var/lib/mysql está en un volumen real, no en overlay.
  2. Mide la latencia de escritura sincronizada en ese volumen (no “throughput de disco”).
  3. Verifica límites de memoria del contenedor y dimensionamiento del buffer pool que coincidan con la realidad.
  4. Revisa las perillas de durabilidad y confirma que coinciden con el contrato del negocio.
  5. Mueve tmpdir/logs/binlogs al almacenamiento correcto y rota esos archivos.

Luego afina consultas. Luego afina índices. No al revés.

← Anterior
Pánico del kernel: cuando Linux dice “nope” en público
Siguiente →
Fundamentos de SR-IOV en Debian 13: por qué falla y cómo depurar la primera vez

Deja un comentario