Privación de IOPS en Docker: por qué un contenedor de DB hace que todo vaya lento

¿Te fue útil?

El síntoma: despliegas un contenedor de base de datos, y de repente todo el host parece funcionar como si estuviera arrancando desde una memoria USB de 2009. SSH tarda segundos en mostrar caracteres. Otros contenedores hacen timeouts. La CPU está “bien”. La memoria está “bien”. Y, aun así, todo es un desastre.

La causa: contención de almacenamiento. En concreto, una carga está consumiendo (o provocando) la mayor parte del presupuesto de IOPS y elevando la latencia para todos los demás. Docker no “rompió” tu servidor. Aprendiste, de la forma difícil, que los discos se comparten, el encolamiento es real y las semánticas de sincronización de las bases de datos no negocian.

Cómo se manifiesta la privación de IOPS en un host Docker

La privación de IOPS no es “el disco está lleno”. Es “el disco está ocupado”. Más precisamente: la ruta de almacenamiento está saturada de modo que la latencia media de las peticiones se dispara, y todas las cargas que comparten esa ruta pagan el precio.

En hosts Linux ejecutando Docker, esto normalmente se presenta como:

  • Alto iowait (pero no siempre). Si miras solo el porcentaje de CPU, puedes pasarlo por alto.
  • Picos de latencia en lecturas y/o escrituras. Las bases de datos odian la latencia de escritura porque fsync es un contrato, no una sugerencia.
  • Acumulación en la cola. Las peticiones se apilan en la capa de bloque o en el dispositivo, y todo se vuelve “lento”, incluidos servicios no relacionados.
  • Síntomas indirectos y extraños: DNS lento dentro de contenedores, logging lento, servicios systemd con timeouts, problemas en el daemon de Docker, incluso checks de salud “aleatorios” fallando.

¿Por qué SSH va lento? Porque tu terminal escribe en un PTY, los shells tocan el disco para el historial, los logs se vacían, y el kernel está ocupado orquestando IO. El sistema no está muerto. Está esperando a que el recurso compartido más lento vuelva de su pausa.

Por qué un contenedor puede perjudicar a todo

Los contenedores Docker son aislamiento de procesos más algo de magia en el sistema de archivos. No son aislamiento físico. Si varios contenedores comparten:

  • el mismo dispositivo de bloque (mismo volumen root, mismo disco EBS, mismo grupo RAID, mismo LUN SAN),
  • el mismo sistema de archivos,
  • las mismas colas del planificador de I/O,
  • y a menudo el mismo comportamiento de writeback y journaling,

…entonces un contenedor puede absolutamente degradar a todos los demás. Esto es el “vecino ruidoso” en su forma más pura.

Las bases de datos amplifican la latencia

La mayoría de motores de BD hacen muchas operaciones de IO pequeñas y aleatorias. También ejecutan fsync (o similar) para garantizar durabilidad. Si el almacenamiento es lento, la BD no “se esfuerza más” para ponerse al día; bloquea y espera. Ese bloqueo puede propagarse a pools de conexiones, hilos de aplicación y tormentas de reintentos.

Y aquí entra Docker para empeorar: la pila de almacenamiento por defecto puede añadir overhead cuando la BD escribe muchos ficheros pequeños o churn de metadatos.

Los sistemas overlay pueden aumentar la amplificación de escritura

Muchos hosts Docker usan overlay2 para la capa escribible del contenedor. Los sistemas overlay son geniales para el layering de imágenes y la ergonomía del desarrollador. A las bases de datos no les importa tu ergonomía; les importa la latencia predecible.

Si tu BD escribe en la capa escribible del contenedor (en lugar de un volumen dedicado), puedes desencadenar operaciones extra de metadatos, comportamiento de copy-up y patrones de escritura menos favorables. A veces va bien. A veces es una escena del crimen de rendimiento con recibos.

“Pero el disco solo está al 20% de throughput” es una trampa

El throughput (MB/s) no es toda la historia. IOPS y latencia importan más para muchas cargas BD. Puedes tener un disco haciendo 5 MB/s y aun así estar completamente saturado en IOPS con escrituras aleatorias de 4K.

Por eso tu monitor que rastrea “ancho de banda de disco” dice que todo está tranquilo mientras el host llora silenciosamente en iowait.

Chiste #1: Cuando una base de datos dice que está “esperando al disco”, no es pasivo-agresiva. Está siendo precisa.

Datos interesantes y contexto histórico

  1. IOPS se volvió métrica mainstream por OLTP: las bases de datos transaccionales empujaron a la industria a medir “cuántas operaciones pequeñas por segundo”, no solo MB/s.
  2. El scheduler CFQ de Linux solía ser el predeterminado para equidad en muchas distribuciones; los sistemas modernos suelen usar mq-deadline o none para NVMe, cambiando la sensación de “justicia” en la contención.
  3. cgroups v1 tenía controles blkio tempranos (peso y throttling). cgroups v2 cambió a otra interfaz (io.max, io.weight), lo que pilla a la gente en migración.
  4. Overlayfs se construyó para union mounts y capas, no para ficheros de bases de datos con alto churn. Ha mejorado mucho, pero la desalineación de cargas aún aparece en los peores lugares: las colas de latencia.
  5. Existen barreras de escritura y flushes porque las caches mienten: los dispositivos reordenan escrituras por rendimiento, así que el SO usa flushes/FUA para imponer orden cuando las aplicaciones exigen durabilidad.
  6. Los sistemas de archivos con journaling intercambian escrituras adicionales por consistencia: ext4 y XFS son fiables, pero el comportamiento del journal puede aumentar la carga de IO bajo churn fuerte de metadatos.
  7. El almacenamiento en la nube a menudo tiene créditos de ráfaga: puedes ser rápido por un tiempo y luego de repente lento. El contenedor no cambió; el nivel de almacenamiento lo hizo.
  8. NVMe mejoró la paralelización masivamente con múltiples colas, pero no eliminó la contención; solo la movió a diferentes colas y límites.
  9. Las “tormentas de fsync” son un modo clásico de fallo: muchos hilos llaman a fsync, las colas se llenan, la latencia se dispara y un sistema que parecía bien en p50 se desintegra en p99.

Guía rápida de diagnóstico

Esta es la secuencia “tengo cinco minutos y producción está en llamas”. No debatas arquitectura mientras el host se queda atascado. Mide, identifica el cuello de botella y luego decide si limitar, mover o aislar.

Primero: confirma que es latencia de IO, no CPU o memoria

  • Revisa load, iowait y cola de ejecución.
  • Revisa latencia de disco y profundidad de cola.
  • Comprueba si un proceso/contenedor es el mayor consumidor de IO.

Segundo: mapea el dolor a un dispositivo y montaje

  • ¿Qué dispositivo de bloque está lento?
  • ¿Está Docker root en ese dispositivo?
  • ¿Los ficheros de la base de datos están en overlay2 o en un volumen dedicado?

Tercero: decide una mitigación inmediata

  • Limitar el contenedor ruidoso (IOPS o ancho de banda) para salvar el resto del host.
  • Mover los datos de la BD a un volumen/dispositivo dedicado.
  • Detener la hemorragia: desactivar logs chatos, pausar jobs no críticos, reducir concurrencia.

Cuarto: planifica la solución real

  • Usar volúmenes para datos de BD (no las capas escribibles del contenedor).
  • Usar aislamiento de IO (cgroups v2 io.max / weights) cuando sea posible.
  • Elegir opciones de almacenamiento y sistema de archivos apropiadas para los patrones de sync de la BD.

Tareas prácticas: comandos, salidas, decisiones

Abajo hay tareas reales que puedes ejecutar en un host Linux con Docker. Cada una incluye: comando, salida de ejemplo, qué significa y la decisión que tomas a partir de ello. Ejecuta como root o con sudo donde haga falta.

Task 1: Check overall CPU, iowait, and load

cr0x@server:~$ top -b -n 1 | head -n 5
top - 12:41:02 up 21 days,  4:17,  2 users,  load average: 9.12, 7.84, 6.30
Tasks: 312 total,   4 running, 308 sleeping,   0 stopped,   0 zombie
%Cpu(s):  8.1 us,  2.4 sy,  0.0 ni, 61.7 id, 25.9 wa,  0.0 hi,  1.9 si,  0.0 st
MiB Mem :  64035.7 total,   3211.3 free,  10822.9 used,  499... buff/cache
MiB Swap:  8192.0 total,   8192.0 free,      0.0 used.  5... avail Mem

Significado: 25.9% iowait y alta carga sugieren muchos hilos bloqueados en IO. La CPU no está “ocupada”, está esperando.

Decisión: Deja de culpar al planificador y empieza a medir latencia de disco y encolamiento.

Task 2: Identify which disks are suffering (latency, utilization, queue depth)

cr0x@server:~$ iostat -x 1 3
Linux 6.2.0 (server) 	01/03/2026 	_x86_64_	(16 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          7.52    0.00    2.61   23.98    0.00   65.89

Device            r/s     w/s   rkB/s   wkB/s  rrqm/s  wrqm/s  %rrqm  %wrqm r_await w_await aqu-sz  %util
nvme0n1         95.0   820.0  3200.0  9870.0     0.0    15.0   0.00   1.80    3.2   42.7  18.90  98.50

Significado: %util cerca del 100% y w_await ~43ms significa que el dispositivo está saturado en escrituras. aqu-sz (tamaño medio de cola) es grande, confirmando backlog.

Decisión: Encuentra al escritor. No tunées bases de datos a ciegas; identifica el proceso/contenedor que genera esas escrituras.

Task 3: See if the block layer is backlogged (per-device stats)

cr0x@server:~$ cat /proc/diskstats | grep -E "nvme0n1 "
259       0 nvme0n1  128930 0 5128032 12043  942110 0 9230016 390122  0 220010 402210  0 0 0 0

Significado: Los campos son densos, pero una indicación rápida es el alto tiempo dedicado a IO comparado con la línea base. Combina con iostat -x para confirmar.

Decisión: Si ves este pico solo durante carga de BD, estás en una situación clásica de “una carga satura el dispositivo”.

Task 4: Find the top IO processes (host view)

cr0x@server:~$ sudo iotop -b -n 3 -o
Total DISK READ:         0.00 B/s | Total DISK WRITE:      74.32 M/s
Current DISK READ:       0.00 B/s | Current DISK WRITE:    71.91 M/s
  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
21491 be/4 postgres     0.00 B/s   55.12 M/s  0.00 %  89.15 % postgres: checkpointer
21510 be/4 postgres     0.00 B/s   10.43 M/s  0.00 %  63.20 % postgres: walwriter
 4321 be/4 root         0.00 B/s    3.01 M/s  0.00 %  12.10 % dockerd

Significado: Procesos de Postgres dominan la escritura y pasan mucho % de tiempo en espera de IO. dockerd escribiendo un poco es normal (logs, capas), pero no es el agresor principal.

Decisión: Confirma qué contenedor posee esos procesos y decide si limitar o mover su almacenamiento.

Task 5: Map a process to a container

cr0x@server:~$ ps -o pid,cgroup,cmd -p 21491 | sed -n '1,2p'
  PID CGROUP                                                          CMD
21491 0::/docker/8b6c3b7e4a3a9b7d2a7b55c4a1a2f9b9b0f6c0f9d1a7b1e3c9e3a2c1e5b  postgres: checkpointer

Significado: El proceso está en un cgroup Docker nombrado por el ID del contenedor.

Decisión: Inspecciona ese contenedor. Confirma sus montajes y si usa overlay2 o un volumen.

Task 6: Inspect container mounts and storage driver

cr0x@server:~$ docker inspect -f 'Name={{.Name}} Driver={{.GraphDriver.Name}} DataRoot={{json .GraphDriver.Data}} Mounts={{json .Mounts}}' 8b6c3b7e4a3a
Name=/db Driver=overlay2 DataRoot={"LowerDir":"/var/lib/docker/overlay2/l/..","MergedDir":"/var/lib/docker/overlay2/1d3.../merged","UpperDir":"/var/lib/docker/overlay2/1d3.../diff","WorkDir":"/var/lib/docker/overlay2/1d3.../work"} Mounts=[{"Type":"volume","Name":"pgdata","Source":"/var/lib/docker/volumes/pgdata/_data","Destination":"/var/lib/postgresql/data","Driver":"local","Mode":"z","RW":true,"Propagation":""}]

Significado: El driver de almacenamiento es overlay2 para la capa escribible, pero el directorio de datos de la BD está montado como volumen nombrado. Eso es bueno. Si los datos de BD estuvieran en UpperDir, sería peor.

Decisión: Si los datos de BD no están en un volumen, soluciona eso primero. Si lo están, verifica dónde vive físicamente el volumen (¿el mismo dispositivo que todo lo demás?).

Task 7: Check what device backs Docker data root and volumes

cr0x@server:~$ docker info --format 'DockerRootDir={{.DockerRootDir}} Driver={{.Driver}}'
DockerRootDir=/var/lib/docker Driver=overlay2
cr0x@server:~$ df -hT /var/lib/docker /var/lib/docker/volumes/pgdata/_data
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/nvme0n1p2 ext4  900G  612G  242G  72% /
/dev/nvme0n1p2 ext4  900G  612G  242G  72% /

Significado: Docker root y el volumen de base de datos están en el mismo sistema de archivos ext4 en la misma partición NVMe. Así que la contención está garantizada.

Decisión: Si necesitas que la BD y el resto del host coexistan, debes aislar IO (controles cgroup IO) o mover los datos de la BD a un dispositivo dedicado.

Task 8: Check filesystem mount options (barriers, atime, etc.)

cr0x@server:~$ findmnt -no TARGET,SOURCE,FSTYPE,OPTIONS /
/ /dev/nvme0n1p2 ext4 rw,relatime,errors=remount-ro

Significado: relatime está bien. Nada obviamente extraño. Si ves opciones exóticas (o tuning mal aplicado), es una pista.

Decisión: No cambies opciones de montaje durante un incidente a menos que estés seguro; usa throttling/migración como primeros auxilios más seguros.

Task 9: Check if the host is stuck in writeback congestion

cr0x@server:~$ cat /proc/meminfo | egrep 'Dirty|Writeback|WritebackTmp'
Dirty:              82456 kB
Writeback:         195120 kB
WritebackTmp:           0 kB

Significado: Writeback elevado puede indicar muchos datos siendo vaciados al disco. No prueba culpa de la BD, pero apoya “pipeline de almacenamiento sobrecargado”.

Decisión: Si writeback es persistentemente alto y la latencia también, reduce la presión de escritura y aísla al escritor pesado.

Task 10: Look at per-process IO counters (sanity)

cr0x@server:~$ sudo cat /proc/21491/io | egrep 'write_bytes|cancelled_write_bytes'
write_bytes: 18446744073709551615
cancelled_write_bytes: 127385600

Significado: Algunos kernels exponen contadores de manera confusa (y algunos sistemas de archivos no reportan claramente). Trátalos como direccionales, no absolutos.

Decisión: Si esto es inconcluso, apóyate en iotop, iostat y métricas de latencia a nivel de dispositivo.

Task 11: Check Docker container resource limits (CPU/mem) and note the absence of IO limits

cr0x@server:~$ docker inspect -f 'CpuShares={{.HostConfig.CpuShares}} Memory={{.HostConfig.Memory}} BlkioWeight={{.HostConfig.BlkioWeight}}' 8b6c3b7e4a3a
CpuShares=0 Memory=0 BlkioWeight=0

Significado: Sin límites explícitos. CPU y memoria son comúnmente limitadas; IO a menudo no lo está. Así es como un único contenedor aplasta un host.

Decisión: Añade controles de IO (Docker blkio en cgroups v1, o controles systemd/cgroups v2), o aísla la carga en distinto almacenamiento.

Task 12: Apply a temporary IO throttle (bandwidth) to a container (incident mitigation)

cr0x@server:~$ docker update --device-write-bps /dev/nvme0n1:20mb 8b6c3b7e4a3a
8b6c3b7e4a3a

Significado: Docker aplicó un throttle de ancho de banda de escritura para ese dispositivo al contenedor. Es una herramienta brusca. Puede proteger el host a costa de latencia y rendimiento de la BD.

Decisión: Usa esto para detener el daño colateral. Luego mueve la BD a almacenamiento dedicado o implementa reparto justo con controladores IO adecuados.

Task 13: Apply an IOPS throttle (more relevant for small IO)

cr0x@server:~$ docker update --device-write-iops /dev/nvme0n1:2000 8b6c3b7e4a3a
8b6c3b7e4a3a

Significado: Limita IOPS de escritura. Esto normalmente se alinea mejor con el dolor de BD que los throttles en MB/s.

Decisión: Si el throttling mejora la capacidad de respuesta del host, has confirmado “vecino ruidoso IO” como modo principal de fallo. Ahora debes solucionarlo arquitectónicamente.

Task 14: Check cgroup v2 IO controller status

cr0x@server:~$ stat -fc %T /sys/fs/cgroup
cgroup2fs
cr0x@server:~$ cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory pids

Significado: El host usa cgroups v2 y soporta el controlador io.

Decisión: Prefiere el control IO de cgroups v2 cuando esté disponible; es más claro y generalmente la dirección hacia la que va Linux.

Task 15: Inspect a container’s IO limits via its cgroup (v2)

cr0x@server:~$ CID=8b6c3b7e4a3a; cat /sys/fs/cgroup/docker/$CID/io.max
8:0 rbps=max wbps=max riops=max wiops=max

Significado: No hay límites actualmente. Major:minor 8:0 es un ejemplo; tu NVMe puede ser diferente. “max” significa ilimitado.

Decisión: Si usas systemd o un runtime que integra cgroups v2, establece io.max o pesos para el scope del servicio/contenedor.

Task 16: Check device major:minor for correct throttling target

cr0x@server:~$ lsblk -o NAME,MAJ:MIN,SIZE,TYPE,MOUNTPOINT | sed -n '1,6p'
NAME        MAJ:MIN   SIZE TYPE MOUNTPOINT
nvme0n1     259:0   953.9G disk 
├─nvme0n1p1 259:1     512M part /boot/efi
└─nvme0n1p2 259:2   953.4G part /

Significado: Si configuras io.max debes especificar el major:minor correcto (p. ej., 259:0 para el disco, o 259:2 para una partición según la configuración).

Decisión: Apunta al dispositivo real que sufre contención. Throttlear el major:minor equivocado es la forma más segura de “arreglar” nada con máxima confianza.

Causas raíz que aparecen en incidentes reales

1) Escrituras de BD viviendo en la capa escribible del contenedor

Si tu base de datos almacena sus datos dentro del sistema de archivos del contenedor (overlay2 upperdir), estás apilando una carga con muchas escrituras sobre un mecanismo diseñado para imágenes por capas. Espera peor latencia y más churn de metadatos. Usa un volumen Docker o un bind mount a una ruta dedicada.

2) Un dispositivo compartido, cero controles de equidad IO

Aun si los datos de BD están en un volumen, si ese volumen es solo un directorio en el mismo filesystem root, sigues compartiendo el dispositivo. Sin pesos o throttles, el escritor más ocupado gana. Todos los demás pierden.

3) Acantilados de latencia por almacenamiento en la nube con ráfagas

Muchos volúmenes en la nube proporcionan rendimiento “base + ráfaga”. Una BD ocupada quema la capacidad de ráfaga, luego el volumen cae a la tasa base. Tu incidente empieza exactamente cuando se acaban los créditos. Nada de Docker explica el timing, por eso los equipos pierden horas mirando la capa equivocada.

4) Journaling + fsync + alta concurrencia = latencia en cola catastrófica

Las bases de datos suelen hacer muchas escrituras concurrentes, más WAL/redo logs, más checkpointing. Añade el comportamiento del filesystem con journaling y flushes de caché del dispositivo, y puedes tener throughput medio “perfecto” con latencia p99 catastrófica.

5) Logging en el mismo dispositivo que la BD

Cuando el disco está saturado, los logs no solo “se escriben más despacio”. Pueden bloquear hilos de aplicación, llenar buffers y generar más IO en el peor momento posible. Los logs JSON son simpáticos hasta que se convierten en tu carga principal de escritura durante un outage.

Chiste #2: Si pones una base de datos y logs debug verbosos en el mismo disco, has inventado un nuevo sistema distribuido: “latencia”.

Errores comunes (síntoma → causa → solución)

Esta es la parte donde dejas de repetir el mismo outage con nombres distintos.

1) Síntoma: la CPU está baja, pero el load average es alto

  • Causa: hilos atrapados en sleep de IO ininterrumpible; el load los cuenta.
  • Solución: revisa iostat -x buscando await y %util; identifica procesos top con iotop; aisla o limita.

2) Síntoma: todos los contenedores se vuelven lentos, incluidos servicios “no relacionados”

  • Causa: dispositivo de bloque compartido saturado; writeback y operaciones de metadatos del kernel impactan a todos.
  • Solución: mueve la BD a dispositivo/volumen dedicado; aplica controles IO de cgroup; separa Docker root, logs y datos de BD en dispositivos distintos cuando sea posible.

3) Síntoma: picos de latencia de BD durante checkpoints o compactación

  • Causa: fases de escritura intensiva y bursted que causan acumulación en colas y tormentas de flush.
  • Solución: ajusta parámetros de checkpoint/compactación de la BD con cuidado; limita IO para escritores de background; asegura que el almacenamiento tenga IOPS sostenidas suficientes.

4) Síntoma: “El throughput del disco parece estar bien” en el monitoring

  • Causa: estás monitorizando MB/s, no latencia de IO ni IOPS; las IO pequeñas y aleatorias saturan IOPS primero.
  • Solución: monitoriza await, aqu-sz, percentiles de latencia de dispositivo si están disponibles, y límites de IOPS por volumen en entornos cloud.

5) Síntoma: el contenedor de BD es rápido en solitario, lento en el host de producción

  • Causa: el host de pruebas tenía almacenamiento aislado o menos vecinos ruidosos; producción comparte el disco root con todo.
  • Solución: prueba rendimiento en un stack representativo; aplica aislamiento vía dispositivos separados o controles IO.

6) Síntoma: tras “optimizar”, empeoró

  • Causa: desactivar durabilidad, cambiar opciones de montaje o aumentar concurrencia empujó al sistema a peores colas de latencia o riesgo de datos.
  • Solución: prioriza latencia predecible y durabilidad; ajusta una variable a la vez; valida con medidas de latencia, no con sensaciones.

Tres mini-historias corporativas desde las trincheras del almacenamiento

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

El equipo estaba migrando un monolito a “unos cuantos contenedores” en una gran VM Linux. Empezaron con la base de datos porque era lo más temido, y “funcionó bien” en staging. En producción, cada despliegue después de que llegó el contenedor de BD traía una ola de timeouts de servicios no relacionados: la API, el job runner, incluso el sidecar de métricas.

La suposición inicial fue clásica: “Los contenedores aíslan recursos.” Limitaron CPU y memoria del contenedor de BD, se felicitaron y siguieron. Cuando la carga del host se fue a la luna con la CPU mayormente ociosa, la culpa rotó por networking, DNS, la red overlay de Docker y una breve pero apasionada discusión sobre versiones del kernel.

Le tomó a una persona ejecutar iostat -x para terminar el debate. El disco root estaba ~100% en utilización con latencia de escritura parecida a una cordillera. El directorio de datos de la BD era un bind mount dentro de /var/lib/docker en el mismo filesystem root que todo lo demás, incluidos logs de contenedores y capas de imagen.

Una vez que aceptaron que “contenedor” no significa “disco separado”, la solución fue directa: adjuntar un volumen dedicado para la BD, montarlo en el directorio de datos y mover los logs fuera del disco root. El host pasó de “fallo sistémico misterioso” a “computadora aburrida”, que es el mayor elogio en operaciones.

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

Otra compañía tenía un problema de rendimiento: su contenedor Postgres estaba limitado en escrituras durante picos. Alguien propuso una optimización: reducir la presión de fsync relajando durabilidad, razonando que “tenemos replicación” y “el almacenamiento cloud es fiable”. El cambio mejoró el throughput inmediatamente en un benchmark sintético y se desplegó.

Dos semanas después, un nodo se cayó durante un evento ruidoso de almacenamiento. No fue catastrófico, pero el timing fue perfecto: alta carga de escritura, lag de réplicas y failover. No perdieron toda la base de datos, pero sí suficientes transacciones recientes para desencadenar una semana de conversaciones incómodas. Mientras tanto, el síntoma original (lentitud a nivel de host) volvió durante ráfagas, porque el verdadero cuello de botella era la saturación de colas del dispositivo y la contención compartida de IO, no solo la sobrecarga de fsync.

Revirtieron la concesión de durabilidad e hicieron la solución adulta: aislar la BD en un dispositivo de bloque dedicado con IOPS predecibles, añadir pesos IO para mantener el resto del host usable y limitar las tareas de mantenimiento más abusivas. La “optimización” no era mala; estaba mal aplicada. Optimizó la capa equivocada y compró velocidad usando tus datos como garantía.

La lección que quedó: si vas a intercambiar durabilidad por rendimiento, dilo claramente, documentalo y consigue aprobación de quienes recibirán las páginas cuando falle.

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

Esta es menos dramática, que es exactamente por qué funcionó. Un equipo ejecutaba múltiples contenedores stateful en una pequeña flota: una BD, una cola y un buscador. Tenían una política: cada servicio stateful debe usar un punto de montaje dedicado respaldado por una clase de volumen dedicada, y cada servicio debe tener un presupuesto IO explícito escrito en las notas del despliegue.

No era sofisticado. No tenían parches de kernel a medida ni planificadores artesanales. Simplemente se negaron a poner datos persistentes en el filesystem root de Docker y mantuvieron los logs de aplicación en una ruta separada con rotación y niveles de verbosidad sensatos.

Un día, un job batch se volvió loco y empezó a golpear la persistencia de la cola. La latencia subió para ese servicio, pero el resto del host se mantuvo responsivo. Sus alertas apuntaron directamente al dispositivo usado por el volumen de la cola, no a “el servidor está lento”. Limitaron el IO del contenedor del job, estabilizaron y hicieron un postmortem sin que nadie tuviera que explicar por qué SSH era inutilizable.

A veces las prácticas “aburridas” son simplemente respuesta a incidentes prepagada.

Soluciones y guardarraíles que realmente funcionan

1) Pon los datos de la BD en un volumen real, no en la capa escribible del contenedor

Si recuerdas una sola cosa, que sea esta: las bases de datos deben escribir en un montaje dedicado (volumen nombrado o bind mount), idealmente respaldado por un dispositivo dedicado. Eso implica:

  • El directorio de datos de la BD es un punto de montaje que puedes ver en findmnt.
  • Ese montaje se mapea a un dispositivo que puedes medir de forma independiente.
  • Docker root (/var/lib/docker) no soporta la carga de durabilidad de tu base de datos.

2) Separa responsabilidades: imágenes/logs vs datos durables

El root de Docker está ocupado: extracción de imágenes, descargas de capas, metadatos overlay, escrituras de logs de contenedores y más. Combínalo con una BD haciendo WAL y checkpoints y habrás construido una máquina de contención.

Una separación práctica:

  • Disco A: OS + Docker root + logs de contenedores (lo suficientemente rápido, pero no sagrado).
  • Disco B: Volumen de BD (IOPS predecibles, baja latencia, monitorizado).

3) Usa controles IO para imponer equidad

Si debes compartir un dispositivo, impón equidad. Para cgroups v2, el controlador io ofrece peso y throttling. La UX de Docker para esto varía por versión y runtime, pero el principio es estable: no dejes que un contenedor se convierta en una aspiradora de disco.

Throttling no es solo punitivo. Puede ser la diferencia entre “la BD está lenta” y “todo está caído”. En un incidente, a menudo quieres mantener el resto del host responsivo mientras decides qué hacer con la BD.

4) Mide latencia, no solo utilización

%util es útil pero no suficiente. Un dispositivo puede mostrar menos del 100% de utilización y aun así tener latencia terrible, especialmente en almacenamiento virtualizado o en red donde el “dispositivo” es una abstracción.

Lo que quieres saber:

  • Latencia media y en cola para lecturas/escrituras.
  • Tendencias de profundidad de cola bajo carga.
  • Límites de IOPS y si te acercas a ellos.

5) Ajusta el comportamiento de la BD solo después de arreglar la geometría de almacenamiento

El tuning de bases de datos tiene su lugar. Pero si la BD simplemente comparte el disco equivocado con todo lo demás, tunear es un impuesto a tu tiempo y un regalo para futuros incidentes.

Primero: aísla la ruta de dispositivo. Luego: evalúa checkpoints, ajustes WAL y trabajo background. Si no, terminarás con una configuración frágil que funciona hasta el próximo pico.

6) Evita que los logs de contenedores se conviertan en una carga de IO

Si registras en alto volumen en JSON y dejas que Docker lo escriba en el mismo filesystem que tu BD, compites por la misma cola. Usa rotación de logs, reduce la verbosidad y considera mover logs de alto volumen fuera del host o a un dispositivo separado.

7) No ignores el modelo de rendimiento del volumen cloud

Si tu volumen tiene rendimiento base/ráfaga, modela la carga acorde. Una BD que “va bien 20 minutos y luego es horrible” suele no ser un misterio; es un bucket de créditos que se vacía. Provisiona para rendimiento sostenido o diseña alrededor de esa limitación.

Una frase para recordar

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

Listas de verificación / plan paso a paso

Paso a paso: estabilizar un incidente (30 minutos)

  1. Confirma iowait/latencia: ejecuta top y iostat -x.
  2. Identifica al escritor: ejecuta iotop -o y mapea PIDs a contenedores vía cgroups.
  3. Verifica ubicación del almacenamiento: revisa docker inspect montajes y df -hT para Docker root y volúmenes.
  4. Mitiga: limita las IOPS o el ancho de banda del agresor, o reduce temporalmente su concurrencia (límites de conexión BD, jobs background).
  5. Reduce IO colateral: baja la verbosidad de logs; asegúrate de que la rotación de logs no esté atascada; pausa jobs batch no esenciales.
  6. Comunica: declara claramente “la latencia de disco del host está saturada” y la mitigación aplicada. Evita decir vagamente “Docker está lento”.

Paso a paso: solución permanente (un sprint)

  1. Mueve datos de BD a almacenamiento dedicado: dispositivo separado o clase de volumen con IOPS garantizadas.
  2. Separa Docker root de volúmenes stateful: mantiene /var/lib/docker fuera del dispositivo de BD.
  3. Implementa controles IO: usa cgroup v2 io.max / io.weight (o opciones blkio de Docker donde se soporte).
  4. Configura monitoreo que detecte esto temprano: latencia de dispositivo, profundidad de cola y estado de créditos/ráfaga si aplica.
  5. Prueba la pila real con carga: no “BD en mi portátil”, sino el backend de almacenamiento real y el runtime de contenedores.
  6. Escribe un runbook: incluye los comandos exactos de este artículo y los puntos de decisión.

Checklist pre-despliegue para cualquier contenedor de BD

  • El directorio de datos de BD es un montaje dedicado (volumen/bind mount), no la capa escribible overlay2.
  • Ese montaje está en un dispositivo con características de IOPS y latencia sostenidas conocidas.
  • El contenedor tiene política IO definida (peso o throttle), no “ilimitado”.
  • Los logs están limitados en tasa y rotados; logs de alto volumen no van al disco de la BD.
  • Existen alertas para latencia y profundidad de cola, no solo para disco lleno.

Preguntas frecuentes

1) ¿Es esto un bug de Docker?

Normalmente no. Es contención de recursos compartidos. Docker facilita colocalizar cargas, lo que facilita colocalizar su dolor de almacenamiento por accidente.

2) ¿Por qué un solo contenedor de BD causa lentitud en todo el host?

Porque el dispositivo de bloque se comparte. Cuando ese contenedor satura IOPS o provoca alta latencia, las colas del kernel se llenan y todos esperan detrás de él.

3) ¿Mover la BD a un volumen nombrado lo arregla?

Sólo si el volumen está respaldado por un almacenamiento diferente o una clase de rendimiento distinta. Un volumen nombrado almacenado bajo /var/lib/docker/volumes en el mismo filesystem es organizativo, no un aislamiento real.

4) ¿Cuál es la diferencia entre IOPS y throughput en este contexto?

IOPS son operaciones por segundo (a menudo lecturas/escrituras pequeñas de 4K). Throughput es MB/s. Las bases de datos suelen embotellarse por IOPS y latencia, no por ancho de banda.

5) ¿Debería cambiar el planificador de IO?

A veces, pero rara vez es tu primera solución. Tweaks del scheduler no te salvan de un dispositivo compartido sin aislamiento con una BD haciendo muchas escrituras sync.

6) ¿overlay2 siempre hace lentas a las bases de datos?

No. Pero poner datos calientes de BD en la capa escribible es un error común, y el comportamiento de overlay puede empeorar patrones de escritura con mucho metadata churn. Usa volúmenes para datos de BD.

7) ¿Cómo limito IO para un contenedor de forma fiable?

Usa controles IO de cgroups. Docker soporta --device-read-bps, --device-write-bps y variantes IOPS para throttling. En cgroups v2 también puedes gestionar io.max vía el gestor de servicios/integración del runtime.

8) ¿Por qué ocurre “utilización de disco 100%” en NVMe? ¿No son rápidos?

NVMe es rápido, no infinito. Las escrituras sync pequeñas y los flushes aún pueden saturar colas, y un único dispositivo sigue teniendo una curva de latencia finita bajo carga.

9) ¿Por qué fallan los health checks durante contención IO?

Los checks de salud a menudo hacen operaciones de disco o red que dependen de un kernel responsivo y logging puntual. Bajo alto iowait, todo se retrasa, incluidos los checks “simples”.

10) ¿Cuál es la mitigación inmediata más segura si producción se está desangrando?

Limita el IO del contenedor ruidoso para restaurar la capacidad de respuesta del host, luego planifica la migración de datos a almacenamiento dedicado. Puede ser necesario parar la BD, pero el throttling compra tiempo.

Conclusión: próximos pasos que puedes hacer esta semana

Si un contenedor de BD hace que todo vaya lento, cree en las señales de almacenamiento. Mide latencia y profundidad de cola, identifica el contenedor y deja de fingir que los discos compartidos se organizarán solos en equidad.

Pasos concretos:

  1. Añade latencia de dispositivo y profundidad de cola a tus dashboards (no solo % de disco lleno o MB/s).
  2. Audita contenedores stateful: verifica que los directorios de datos sean volúmenes/bind mounts y mapea a dispositivos reales.
  3. Separa almacenamiento: mueve los datos de BD fuera del filesystem root de Docker a almacenamiento dedicado con rendimiento sostenido conocido.
  4. Implementa controles IO: establece throttles o pesos sensatos para que un contenedor no pueda tomar como rehén a todo el host.
  5. Escribe el runbook: copia la “guía rápida de diagnóstico” y las tareas en tu wiki de on-call, y ejecuta un game day.
← Anterior
Clones ZFS: copias instantáneas con dependencias ocultas (lee esto primero)
Siguiente →
Segmentación VPN + VLAN para oficina, almacén y cámaras (sin arrepentimientos)

Deja un comentario