Todo parece estar bien. CPU está al 30%. El load average no asusta. Aun así las peticiones van lentas, el p99 explota y tu canal de on-call se llena de ese tipo de mensajes “¿está caído?” que acortan vidas.
Este es el truco del rendimiento en Docker: miras la CPU porque es medible y reconfortante, mientras que el problema real suele estar en algún lugar más desordenado: almacenamiento, red, presión de memoria, límites del kernel o una política de cgroup silenciosa que te ha estado estrangulando como un burócrata con sello.
Si no es la CPU, ¿qué es?
Cuando un contenedor “se retrasa”, rara vez significa que tu código de repente se volvió tonto. Significa que el trabajo del contenedor está esperando: esperando a que se vacíen los discos, esperando al stack de red, esperando al DNS, esperando fallos de página, esperando a un bloqueo dentro del kernel, esperando a un controlador de cgroup, esperando a que el driver de logs deje de bloquear las escrituras.
Los gráficos de CPU son seductores porque son limpios. También son incompletos. Un sistema puede estar lento con la CPU inactiva si hilos están bloqueados en sleep no interrumpible (usualmente I/O), atascados por contención en la runqueue debido a throttling, o hambrientos por reclaim de memoria. En contenedores, esos problemas son más fáciles de crear porque has introducido:
- Filesystems union/overlay (copy-up, churn de metadata).
- Capas de red extra (veth pairs, bridges, NAT, conntrack).
- Planos de control adicionales (cgroups, namespaces, límites, cuotas).
- Defaults “útiles” (logging driver, configuración de DNS, storage driver).
La mayoría de incidentes de rendimiento que he visto no eran “CPU”. La CPU fue simplemente el último testigo que no vio nada.
Exactamente una cita, porque sigue siendo cierta: “Hope is not a strategy.” — General Gordon R. Sullivan
Aquí está la conclusión: si tratas el rendimiento como una perilla única, seguirás enviando latencia. Si lo tratas como un juego de eliminación—mide, aísla, cambia una cosa, mide otra vez—dejarás de adivinar y empezarás a arreglar.
Datos y contexto interesantes (la edición “por qué estamos aquí”)
- Los contenedores no empezaron con Docker. Linux tuvo namespaces y cgroups años antes; Docker popularizó el empaquetado y el flujo de trabajo, no los primitivos del kernel.
- Los cgroups existen porque “nice” no era suficiente. La prioridad tradicional de procesos de Unix no proporcionaba aislamiento multi-tenant fiable para memoria y I/O; los cgroups se construyeron para hacer cumplir eso.
- Los filesystems overlay/union fueron diseñados por conveniencia. Cambian algo de simplicidad del sistema de ficheros por capas componibles. Ese intercambio aparece con cargas intensivas en metadata.
- Docker temprano usó AUFS ampliamente. AUFS fue rápido en algunos casos y doloroso en otros; el ecosistema se movió hacia overlay2 conforme mejoró el soporte en el kernel.
- Conntrack es una tabla de estado, no magia. NAT y connection tracking necesitan memoria y CPU. Bajo picos, la tabla se llena y el kernel empieza a descartar o caducar flujos.
- El DNS en contenedores suele ser diferente al DNS del host. El DNS embebido de Docker y el comportamiento del resolvedor pueden amplificar timeouts cuando el DNS upstream es lento.
- “fsync storms” son un problema antiguo con nuevos disfraces. Bases de datos y apps con journaling que llaman fsync con frecuencia pueden colapsar la E/S cuando el backend de almacenamiento no está diseñado para latencias consistentes.
- Logging siempre ha sido un asesino de throughput. En los días de syslog, el logging sincrónico podía bloquear aplicaciones. Los drivers de logs de contenedores pueden recrear ese dolor si no tienes cuidado.
- El reclaim de memoria en Linux es un evento de rendimiento. Incluso sin OOM kills, el direct reclaim y el swapping pueden convertir una latencia “bien” en una sierra.
Guía rápida de diagnóstico (primero/segundo/tercero)
Primero: prueba si estás esperando por I/O, memoria o red
- Revisa tareas bloqueadas y iowait: si los hilos se acumulan en D-state o iowait sube, tu gráfico “CPU baja” está mintiendo por omisión.
- Revisa la presión de memoria: si estás reclamando, haciendo swap o golpeando memory.high, puedes obtener latencia sin crashes.
- Revisa síntomas de red: retransmisiones, drops de conntrack y latencia de DNS crean bloqueos de aplicación que parecen “lentitud aleatoria”.
Segundo: localízalo—a nivel host, un contenedor o un mount
- A nivel host: saturación de dispositivo, contención del journal del filesystem, tabla conntrack llena, presión de memoria del nodo.
- Un contenedor: avalancha de logs, cuota de CPU muy pequeña causando throttling, ulimit demasiado bajo, un volumen vecino ruidoso.
- Un mount/camino: ruta de copy-up de overlay2, bind mount hacia almacenamiento de red lento, opciones de volumen inadecuadas para la carga.
Tercero: remueve o evita capas temporalmente
- Ejecuta la misma carga con tmpfs para datos temporales y ve si el disco es el cuello de botella.
- Cambia temporalmente el logging a none en una reproducción en staging para ver si el logging bloquea.
- Prueba con host networking para aislar bridge/NAT/conntrack (cuando sea seguro).
- Prueba la carga en una única capa escribible (volume) en lugar de overlay2 para la ruta caliente.
La velocidad importa durante incidentes. No necesitas la verdad perfecta; necesitas la próxima restricción.
Cuellos de botella de almacenamiento y sistema de ficheros (overlay2, fsync y el impuesto que olvidaste)
El disco es el sospechoso habitual porque es la forma más fácil de crear colas. Las CPUs modernas pueden superar al almacenamiento por órdenes de magnitud. Los contenedores no cambian la física; solo añaden algunas formas creativas de tropezar con ella.
Overlay2: lo suficientemente rápido hasta que no lo es
Overlay2 combina una pila de capas de imagen solo-lectura con una capa superior escribible. Las lecturas suelen golpear cache y se sienten genial. Las escrituras pueden disparar copy-up: la primera vez que modificas un archivo que existe en una capa inferior, overlay tiene que copiarlo en la capa escribible. Eso no es solo copiar datos; son operaciones de metadata, comprobaciones de permisos y a veces una cantidad de trabajo sorpresa para lo que pensabas que era una “escritura pequeña”.
El dolor de overlay2 aparece como:
- Instalaciones de paquetes o pasos de build lentos dentro de contenedores.
- Picos de latencia cuando una app escribe en rutas que estaban horneadas en la imagen.
- Alta IOPS de metadata (stat, open, rename, unlink) más que throughput secuencial grande.
Journaling, barriers y por qué fsync es un contrato de rendimiento
Las aplicaciones que llaman fsync (bases de datos, colas de mensajes, cualquier cosa que pretende ser durable) están pidiendo al stack de almacenamiento que confirme trabajo a medios permanentes. En buen NVMe esto está bien. En almacenamiento en red, SSDs de consumo con firmware inestable o volúmenes sobrecargados, se convierte en una cola. El contenedor no está lento; el almacenamiento está siendo honesto.
Un detalle más: incluso si tu app no llama fsync, tu journal del filesystem podría hacerlo. Las cargas intensivas en metadata pueden causar commits frecuentes del journal, especialmente si el dispositivo subyacente tiene alta latencia o comportamiento de writeback inconsistente.
Bind mounts y “no sabía que eso era NFS”
Los bind mounts son geniales. También son una pistola en el pie cuando el path montado reside en un backend lento o inconsistente: NFS, SMB, capas FUSE, cifrado o un dispositivo de bloque cloud que está silenciosamente rate-limited. Un contenedor puede ser “local” y aun así escribir en “algún otro lugar”.
Volumes: mejor para rutas de escritura caliente
Si tu app escribe con frecuencia, pon los directorios de escritura intensiva en un Docker volume o bind mount a un filesystem dedicado. Mantén la capa escribible del contenedor lo más fría posible. Overlay2 es un buen default, no un filesystem de alto rendimiento para bases de datos.
Broma #1 (corta, relevante): Los contenedores son como mudarse a un apartamento pequeño: puedes vivir allí, pero no intentes montar un aserradero en la cocina.
Presión de memoria y el problema “no es OOM pero se muere”
Los problemas de memoria no siempre se anuncian con un OOM kill. De hecho, los bugs de latencia más desagradables ocurren antes del OOM. Ahí es cuando el kernel intenta muy duro mantenerte vivo reclamando páginas, compactando memoria y ocasionalmente swapear algo importante.
Cómo se ve la presión de memoria en contenedores
- Picos de latencia durante ráfagas de tráfico, luego recuperación cuando baja la carga.
- Las pausas de GC empeoran (porque las asignaciones disparan reclaim y fallos de página).
- Sube la I/O de disco sin razón obvia (swap o writeback).
- La CPU se mantiene moderada, pero la app se siente “atascada”.
Los límites de memoria de cgroup cambian el comportamiento del kernel
Cuando estableces límites de memoria, no solo impides que el contenedor use demasiada RAM. Estás cambiando cuándo y cómo ocurre el reclaim. Si el contenedor está cerca de su límite, puede golpear direct reclaim más a menudo, lo cual puede bloquear hilos de aplicación. Si el swap está habilitado, puede hacer swap dentro del cgroup, lo que suele parecer latencia aleatoria.
También: la cache de ficheros importa. Privar a un contenedor de memoria puede reducir el cache efectivo y desplazar la carga al disco. La CPU se queda inactiva mientras tu stack de almacenamiento hace su danza interpretativa.
Red: latencia, conntrack y DNS que parece inocente
El rendimiento de red en contenedores a menudo es “suficientemente bueno” hasta que un día no lo es. El modo fallo bajo carga típicamente no es el ancho de banda. Es la latencia y las pérdidas: retransmisiones, encolamiento y timeouts.
Overhead de bridge/NAT/conntrack
La configuración bridge por defecto de Docker suele involucrar NAT y connection tracking. Cada conexión se convierte en estado en conntrack. Si tienes alta rotación de conexiones (llamadas HTTP de corta vida, service meshes, clientes agresivos), la tabla conntrack puede llenarse. Cuando se llena, el kernel empieza a descartar nuevas conexiones o a provocar timeouts. Tu app reporta “upstream timeout”, y tú miras la CPU porque está baja.
DNS: muerte por mil timeouts de 5 segundos
Los problemas de DNS son la fuente más subdiagnosticada de retrasos en contenedores. Un contenedor que ocasionalmente no puede resolver nombres se comportará como “lento”, no “roto”. El resolvedor espera. Tus hilos esperan. La CPU se queda ahí educadamente.
Configuraciones erróneas frecuentes incluyen:
- DNS upstream sobrecargado o rate-limited.
- Demasiados search domains que provocan consultas repetidas.
- El comportamiento del resolvedor de glibc creando consultas seriales y fallback lento.
- El DNS embebido de Docker bajo estrés (especialmente con muchos contenedores y reinicios frecuentes).
Cgroups: throttling, cuotas y por qué los “límites” no son gratis
Los cgroups son la razón no cantada por la que los contenedores pueden compartir un host sin empezar una pelea a cuchillo de inmediato. También son una forma fiable de crear “lentitud misteriosa”.
Throttling por cuota de CPU
Si estableces límites de CPU, el kernel los hace cumplir con cuotas por periodo. Bajo carga, un contenedor puede gastar su cuota temprano y luego ser throttled hasta el siguiente periodo. El resultado: el proceso no está usando CPU porque no se le permite. Tu gráfico de CPU se ve tranquilo. Tu gráfico de latencia no.
Controles de I/O (blkio / io controller)
Dependiendo de la versión de cgroup y configuración, los contenedores pueden tener peso o límite para I/O. Incluso sin límites explícitos de I/O, vecinos ruidosos pueden saturar un dispositivo, haciendo que todos estén lentos. Con límites, puedes accidentalmente dejar sin piernas a tu base de datos y luego culpar a la red.
Límites de PID y de descriptores de fichero
No todos los límites están en cgroups. PIDs y ulimits pueden limitar la concurrencia silenciosamente. Cuando te quedas sin descriptores, las apps pueden atascarse, fallar en aceptar nuevas conexiones o entrar en bucles de reintentos. No es CPU. Es el kernel diciéndote “no” repetidamente.
Logging: el acantilado de rendimiento disfrazado de observabilidad
Logging es I/O. Logging también suele ser lo bastante sincrónico como para hacer daño. El json-file logging por defecto de Docker puede convertirse en un cuello de botella cuando un contenedor emite un gran volumen de logs. El daemon escribe logs en disco. El disco se ocupa. Todo lo que quiere disco ahora espera. A veces el contenedor se bloquea en las escrituras a stdout/stderr si los buffers se llenan.
Este es uno de esos problemas que parece brujería: “Añadimos más logs de debug y el servicio se volvió más lento.” Sí. Convertiste tu nodo de producción en una máquina de escribir.
Broma #2 (corta, relevante): El logging de debug en producción es como añadir un segundo volante a tu coche—técnicamente más control, prácticamente más choques.
Tareas prácticas: comandos, salidas y decisiones (12+)
Estos no son académicos. Son los comandos que ejecutas cuando alguien dice “Docker está lento” y quieres evidencia antes de cambiar cosas.
Tarea 1: Comprueba si el kernel está gritando por tareas bloqueadas
cr0x@server:~$ dmesg -T | tail -n 20
[Mon Feb 3 10:14:52 2026] INFO: task myservice:23144 blocked for more than 120 seconds.
[Mon Feb 3 10:14:52 2026] Tainted: G W OE 5.15.0-97-generic #107-Ubuntu
[Mon Feb 3 10:14:52 2026] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
Qué significa: Un proceso está atascado, comúnmente en sleep no interrumpible (I/O). Esto es una bengala del kernel.
Decisión: Deja de debatir la CPU. Ve inmediatamente a comprobaciones de I/O y filesystem (iostat, pidstat, latencia del backend de almacenamiento).
Tarea 2: Identifica contenedores con churn de reinicios o estado raro
cr0x@server:~$ docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.RunningFor}}\t{{.Image}}'
NAMES STATUS RUNNING FOR IMAGE
api Up 3 hours (healthy) 3 hours myorg/api:4.2.1
worker Up 3 hours 3 hours myorg/worker:4.2.1
sidecar Restarting (1) 5s ago 2 minutes myorg/sidecar:1.9.0
Qué significa: Los loops de reinicio pueden crear carga (consultas DNS, churn de capas de imagen, spam de logs) y ocultar el cuello de botella real.
Decisión: Estabiliza los contenedores que crashean primero. Afinar rendimiento en un proceso inestable es teatro de rendimiento.
Tarea 3: Revisa el throttling de CPU por contenedor
cr0x@server:~$ docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}"
NAME CPU % MEM USAGE / LIMIT NET I/O BLOCK I/O
api 35.12% 1.2GiB / 2GiB 1.1GB / 980MB 18GB / 9GB
worker 12.44% 800MiB / 1GiB 200MB / 210MB 3GB / 1GB
Qué significa: Esto es una pista, no una prueba. Un alto CPU% aquí no muestra throttling; muestra uso.
Decisión: Si sospechas límites, revisa los contadores de throttling de cgroup (tarea siguiente). No asumas que “35%” significa holgura.
Tarea 4: Lee los contadores de throttling de CPU (cgroup v2)
cr0x@server:~$ CID=$(docker inspect -f '{{.Id}}' api)
cr0x@server:~$ CGP=$(find /sys/fs/cgroup -name "*$CID*" -type d 2>/dev/null | head -n 1)
cr0x@server:~$ cat "$CGP/cpu.stat"
usage_usec 987654321
user_usec 700000000
system_usec 287654321
nr_periods 123456
nr_throttled 45678
throttled_usec 912345678
Qué significa: nr_throttled y throttled_usec en ascenso rápido indican quota throttling. Tu app está siendo pausada por la política.
Decisión: Aumenta el límite de CPU, elimina la cuota para servicios sensibles a latencia o ajusta la concurrencia de workers. Si necesitas fairness, usa reservas/pesos con cuidado, no una cuota estricta.
Tarea 5: Detecta saturación de I/O a nivel host rápidamente
cr0x@server:~$ iostat -xz 1 3
Linux 5.15.0-97-generic (server) 02/04/2026 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.10 0.00 4.20 28.50 0.00 55.20
Device r/s w/s rkB/s wkB/s avgqu-sz await svctm %util
nvme0n1 120.0 900.0 8200.0 64000.0 35.2 28.4 0.9 98.0
Qué significa: %util cercano al 100% y alto await significan que el dispositivo está saturado y las peticiones están encoladas. iowait también está elevado.
Decisión: Mueve rutas de escritura pesada a almacenamiento más rápido, reduce la frecuencia de fsync (solo si aceptas trade-offs de durabilidad), divide workloads entre dispositivos o arregla el problema de “avalanchas de logs”.
Tarea 6: Identifica qué procesos están esperando por disco
cr0x@server:~$ pidstat -d 1 5
Linux 5.15.0-97-generic (server) 02/04/2026 _x86_64_ (16 CPU)
12:10:01 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
12:10:02 0 23144 10.00 52000.00 0.00 3200 myservice
12:10:02 0 14521 0.00 9000.00 0.00 600 dockerd
Qué significa: El servicio e incluso dockerd están escribiendo mucho. iodelay indica tiempo esperando I/O.
Decisión: Si dockerd está pesado, sospecha del logging driver o churn de capas de imagen. Si el servicio está pesado, inspecciona sus rutas de escritura y comportamiento de fsync.
Tarea 7: Prueba que overlay2 está en juego y dónde vive
cr0x@server:~$ docker info --format '{{.Driver}} {{.DockerRootDir}}'
overlay2 /var/lib/docker
Qué significa: Estás usando overlay2 bajo /var/lib/docker. Si ese filesystem es lento, todo lo que hace Docker será lento.
Decisión: Pon /var/lib/docker en SSD/NVMe local rápido para cargas serias. Si está en almacenamiento de red, espera dolor.
Tarea 8: Revisa el tipo de filesystem y opciones de montaje para la raíz de Docker
cr0x@server:~$ findmnt -no SOURCE,FSTYPE,OPTIONS /var/lib/docker
/dev/nvme0n1p2 ext4 rw,relatime,errors=remount-ro
Qué significa: ext4 con opciones típicas. Si ves NFS/FUSE u opciones extrañas sync, eso es una bandera roja.
Decisión: Si las opciones de montaje incluyen sync o el backend es remoto, para y rediseña. Los contenedores no arreglan almacenamiento lento.
Tarea 9: Revisa el crecimiento de archivos de log de Docker (json-file driver)
cr0x@server:~$ ls -lh /var/lib/docker/containers/*/*-json.log | sort -k5 -h | tail -n 5
-rw-r----- 1 root root 6.2G Feb 4 12:09 /var/lib/docker/containers/7c.../7c...-json.log
-rw-r----- 1 root root 7.8G Feb 4 12:09 /var/lib/docker/containers/aa.../aa...-json.log
Qué significa: Logs enormes significan escrituras enormes, además de dolor en la rotación si está mal configurada.
Decisión: Habilita rotación de logs, reduce la verbosidad y considera un logging driver que no ate todo al disco caliente del nodo.
Tarea 10: Mide la latencia de DNS desde dentro del contenedor
cr0x@server:~$ docker exec -it api bash -lc 'time getent hosts db.internal >/dev/null'
real 0m2.013s
user 0m0.000s
sys 0m0.004s
Qué significa: Dos segundos para resolver un nombre es un incidente de rendimiento esperando ocurrir.
Decisión: Inspecciona la configuración del resolvedor, rendimiento del DNS upstream y search domains. Arregla DNS antes de afinar hilos de aplicación.
Tarea 11: Inspecciona la configuración del resolvedor dentro del contenedor
cr0x@server:~$ docker exec -it api cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:5
search corp.example internal.example svc.cluster.local
Qué significa: DNS embebido de Docker (127.0.0.11) y ndots:5 con múltiples search domains pueden multiplicar las búsquedas.
Decisión: Reduce search domains, ajusta ndots cuando corresponda y asegúrate de que el DNS upstream esté sano. En algunos entornos, evita el DNS embebido usando resolvedores explícitos.
Tarea 12: Revisa el uso de la tabla conntrack
cr0x@server:~$ sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_count = 248932
net.netfilter.nf_conntrack_max = 262144
Qué significa: Estás cerca del techo. Nuevas conexiones empezarán a fallar bajo ráfagas.
Decisión: Aumenta conntrack max (con cuidado de memoria), reduce la rotación de conexiones (keepalive, pooling) o reduce dependencia de NAT (host networking, enrutamiento directo).
Tarea 13: Busca retransmisiones y miseria general de TCP
cr0x@server:~$ ss -s
Total: 2138
TCP: 1821 (estab 932, closed 756, orphaned 3, timewait 650)
Transport Total IP IPv6
RAW 0 0 0
UDP 45 40 5
TCP 1065 980 85
INET 1110 1020 90
FRAG 0 0 0
Qué significa: Muchos TIMEWAIT pueden indicar alta rotación de conexiones. No es automáticamente malo, pero es sospechoso bajo carga.
Decisión: Si ves churn, añade keepalives, reutiliza conexiones y revisa el comportamiento del cliente. Luego valida la capacidad de conntrack.
Tarea 14: Verifica presión de descriptores abiertos para un proceso containerizado
cr0x@server:~$ PID=$(pgrep -f myservice | head -n 1)
cr0x@server:~$ ls /proc/$PID/fd | wc -l
9823
Qué significa: Casi 10k FDs. Si tus límites son bajos, estás cerca de fallar; si ya fallas, verás “too many open files”.
Decisión: Aumenta ulimit para el servicio, audita fugas de conexiones y verifica fs.file-max a nivel host y los límites por proceso.
Tarea 15: Revisa la configuración de ulimit del contenedor
cr0x@server:~$ docker exec -it api bash -lc 'ulimit -n'
1024
Qué significa: 1024 es pequeño para servicios de red modernos bajo carga.
Decisión: Establece --ulimit nofile=... más alto o configúralo en tu orquestador. Luego verifica que la aplicación use pooling de conexiones.
Tarea 16: Detecta presión de memoria en el host (pistas de swap/reclaim)
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
3 2 524288 81234 10240 901234 5 8 120 1800 900 1400 12 4 55 29 0
Qué significa: si/so no nulo indica swapping. b procesos bloqueados más alto wa señalan espera.
Decisión: Reduce la presión de memoria: aumenta límites donde sea seguro, arregla fugas, añade RAM o re-balancea workloads. Hacer swap con un servicio sensible a latencia es una elección; elige diferente.
Tres mini-historias corporativas desde las trincheras de rendimiento
Mini-historia 1: El incidente causado por una suposición equivocada
Tenían una API de alto tráfico y un plan de migración limpio: moverla a contenedores en un nuevo pool de nodos. Mismos binarios, misma config, “solo Docker”. El equipo hizo lo razonable y miró la CPU. Permaneció baja. Declararon victoria.
Luego llegó el lunes. La latencia p99 se triplicó, pero la latencia media apenas empeoró. El autoscaler añadió más contenedores, lo que empeoró el problema de una forma que se sintió personal. Todos discutieron sobre thread pools y garbage collection porque son peleas que los ingenieros pueden tener sin salir de la silla.
La suposición equivocada fue simple: asumieron que el filesystem del contenedor se comportaba como el del host. En realidad, la app escribía archivos temporales en una ruta que vivía en la capa de imagen, no en un volume. Bajo carga, el copy-up de overlay2 generó churn y las operaciones de metadata explotaron. El disco no estaba lento en throughput; estaba sobrecargado en pequeñas escrituras aleatorias y commits del journal.
Lo probaron moviendo solo el directorio temp a un volume en almacenamiento rápido. Nada más cambió. La latencia volvió a la normalidad, la CPU siguió baja y el chat del incidente quedó en silencio. La lección no fue “overlay2 es malo.” La lección fue “las rutas de escritura son arquitectura”.
Mini-historia 2: La optimización que salió mal
Otra compañía tenía una factura de logging que no les gustaba. Alguien propuso una optimización simple: subir logs de debug solo en horas pico, así capturaban casos raros. Desplegaron un toggle de config. Funcionó en staging. Incluso funcionó en producción… por un día.
Al segundo día llegó el pico de tráfico con logging de debug máximo. La utilización del disco tocó techo. Dockerd pasó tiempo real escribiendo json logs. La aplicación empezó a bloquearse en stdout porque los buffers se llenaban durante las ráfagas. La latencia subió. Los reintentos aumentaron. Más logs. Más escrituras en disco. Se formó un bucle de feedback, como un tutorial de cómo montar tu propio outage.
La “optimización” se basó en un mito: que el logging es gratuito si hay CPU. El logging es I/O, y I/O bajo contención es latencia. Revertieron el debug logging, habilitaron rotación de logs (que deberían haber tenido de todos modos) y movieron logs de alto volumen a una pipeline asíncrona con backpressure.
Tras el postmortem, el equipo dejó de describir el logging como “observabilidad” y empezó a tratarlo como “una carga de producción que compite con el servicio”. Ese cambio de frase previno más incidentes que cualquier ajuste de config aislado.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día
Esta es menos glamorosa. Un equipo ejecutaba servicios stateful en contenedores con control de cambios estricto. Tenían la costumbre—poco fashion, casi embarazosa—de baselinear rendimiento de cada pool de nodos: latencia de disco, RTT de red, headroom de conntrack y comportamiento de reclaim de memoria. Guardaban los resultados como un informe simple y lo volvían a ejecutar después de upgrades del kernel.
Una tarde apareció un lote nuevo de nodos. Todo “funcionaba”, pero la latencia p99 era ligeramente peor. Sin alarmas. Solo una regresión silenciosa. Porque tenían baselines, notaron de inmediato que los percentiles de latencia de almacenamiento estaban fuera de sitio y que una opción de mount difería en la raíz de Docker.
Resultó que la pipeline de provisioning había aplicado una config de filesystem diferente en los nodos nuevos. No catastrófico, pero suficiente para aumentar la latencia de escritura bajo cargas sync-heavy. Dranearon los nodos, corrigieron la config y siguieron. Sin outage. Sin heroísmos. Solo competencia aburrida.
Si quieres fiabilidad, tienes que aceptar ser aburrido por adelantado. Eso no es un lema; es una línea del presupuesto.
Errores comunes: síntoma → causa raíz → arreglo
1) Síntoma: CPU baja, latencia alta, hilos “atascados”
Causa raíz: Saturación de I/O o tareas bloqueadas (D-state), a menudo por amplificación de escritura de overlay2 o almacenamiento de respaldo lento.
Arreglo: Pon rutas de escritura caliente en volumes; mueve Docker root a almacenamiento local rápido; reduce escrituras sincrónicas; confirma con iostat/pidstat.
2) Síntoma: Stalls aleatorios de 1–5s, especialmente en conexiones nuevas
Causa raíz: Timeouts de DNS amplificados por settings del resolvedor (ndots/search domains) o DNS upstream sobrecargado.
Arreglo: Mide tiempo de resolución dentro del contenedor; simplifica resolv.conf; arregla DNS upstream; considera resolvedores caché cercanos a la carga.
3) Síntoma: p99 picos durante ráfagas, sin OOM kills
Causa raíz: Presión de memoria causando reclaim/compaction; límites de memoria del contenedor demasiado ajustados; actividad de swap.
Arreglo: Aumenta headroom de memoria; establece límites de memoria apropiados; reduce asignaciones por petición; evita swap para servicios críticos de latencia.
4) Síntoma: El servicio escala pero se vuelve más lento
Causa raíz: Cuello de botella compartido: dispositivo de disco único saturado, egress de red compartido, presión de conntrack o cuello de botella en logging centralizado.
Arreglo: Identifica el recurso compartido. Escalar compute no escala el disco. Divide dispositivos, añade nodos con I/O independiente, reduce escrituras de logs.
5) Síntoma: “Tosido” periódico cada 100ms o 1s bajo carga
Causa raíz: Throttling por cuota de CPU (periodos de cgroup). El contenedor alcanza la cuota, se pausa, reanuda.
Arreglo: Afloja límites de CPU o usa shares/requests; mantiene cuotas para jobs batch, no para servicios sensibles al tail-latency.
6) Síntoma: Errores de conexión, timeouts, reintentos SYN en pico
Causa raíz: Tabla conntrack llena u overhead de NAT; alta rotación de conexiones creando tormentas de TIMEWAIT.
Arreglo: Incrementa límites de conntrack, reduce rotación con keepalive, y considera modos de red que reduzcan dependencia de NAT/conntrack.
7) Síntoma: “Too many open files” o fallos parciales extraños
Causa raíz: ulimit bajo en contenedor o host; fuga de FDs en la app.
Arreglo: Aumenta nofile; audita uso de FDs; usa pooling de conexiones; alerta sobre el conteo de FDs.
8) Síntoma: El disco del nodo se llena inesperadamente y luego todo degrada
Causa raíz: Archivos de log de contenedores sin límite o datos temporales descontrolados en Docker root.
Arreglo: Configura rotación de logs; limita el volumen de logs; almacena datos temporales en volumes dimensionados; alerta sobre uso de /var/lib/docker.
Listas de verificación / plan paso a paso (aburrido, repetible, efectivo)
Checklist de incidente: “los contenedores están lentos ahora”
- Confirma el alcance: ¿un servicio, un nodo o todo?
- Revisa saturación de I/O: ejecuta
iostat -xz; si%utilestá clavado yawaitalto, trata el almacenamiento como primaria. - Revisa tareas bloqueadas: escanea
dmesgpor hung tasks; confirma con estados de proceso. - Revisa presión de memoria:
vmstat, actividad de swap, eventos de memoria de cgroup si están disponibles. - Revisa throttling: lee
cpu.statpara contadores de throttling; compáralo con el patrón de latencia. - Revisa DNS: cronometra una resolución dentro del contenedor; mira
/etc/resolv.conf. - Revisa conntrack: compara count vs max; busca drops en logs del kernel si existen.
- Revisa logs: tamaño de json logs; uso de disco bajo
/var/lib/docker. - Aísla evitando capas (en staging o con cuidado): tmpfs para temp, volume para escrituras calientes, prueba con host networking.
- Haz un cambio: escoge el cuello de botella con mayor confianza y cambia una sola cosa.
- Mide otra vez: ¿mejoró p99? ¿mejoraron iostat/conntrack/tiempos de DNS?
Checklist preventivo: diseña contenedores que no se retrasen
- Pon rutas de escritura pesada en volumes: bases de datos, colas, caches con persistencia y directorios temporales.
- Mantén la capa de imagen fría: no escribas en rutas horneadas en la imagen en tiempo de ejecución.
- Establece ulimits sensatos: especialmente
nofiley límites de procesos para servicios de alta concurrencia. - Evita cuotas de CPU ajustadas para servicios sensibles: prefiere pesos y reservas; las cuotas son para fairness de batch.
- Configura rotación de logs: no confíes en defaults; limita tamaño y cantidad.
- Baselinea el nodo: latencia de disco, RTT de red, tiempo de resolución DNS, headroom de conntrack.
- Alerta sobre los cuellos reales: disk await, actividad de swap, conteo de conntrack, crecimiento de logs, contadores de throttling.
- Prueba con patrones de I/O similares a producción: especialmente comportamiento de fsync y workloads intensivos en metadata.
Plan de cambios: mejorar rendimiento sin tuning por cargo-cult
- Elige un servicio con dolor de latencia medible y patrones de tráfico estables.
- Captura una baseline: latencias p50/p95/p99, await de disco, tiempos de DNS, contadores de throttling y tasas de error.
- Identifica la restricción más fuerte: saturación de disco, reclaim de memoria, DNS, conntrack o throttling.
- Aplica la mínima solución viable: mueve un directorio a volume, cambia un límite, rota logs o ajusta DNS.
- Reejecuta la misma carga y compara con la baseline; mantén el cambio solo si mueve la aguja.
- Automatiza la comprobación: convierte los comandos manuales en dashboards/alertas y un runbook.
FAQ
¿Por qué mi contenedor se ralentiza cuando la CPU está baja?
Porque el trabajo está esperando, no computando. Los culpables habituales son latencia de disco, reclaim de memoria, timeouts DNS, pérdidas/retransmisiones de red o throttling por cuota de CPU.
¿overlay2 siempre es más lento que ejecutar en el filesystem del host?
No. Suele estar bien para workloads principalmente de lectura y escrituras moderadas. Se pone feo cuando haces muchas escrituras pequeñas, churn de metadata o modificas archivos que existen en capas inferiores de la imagen (coste de copy-up).
¿Debería poner bases de datos en Docker?
Puedes, pero debes tratar el almacenamiento como una decisión de diseño de primera clase: volumes dedicados, almacenamiento con latencia predecible y pruebas honestas de fsync. Si ejecutas una BD en un disco compartido saturado y culpas a Docker, el disco se reirá en silencio.
¿Los límites de CPU hacen el rendimiento más predecible?
Hacen el uso de CPU más predecible. La latencia a menudo se vuelve menos predecible porque el throttling introduce pausas periódicas. Para servicios sensibles al tail-latency, las cuotas ajustadas suelen ser la herramienta equivocada.
¿Cómo sé si el logging está dañando el rendimiento?
Mira archivos json de log grandes, alta actividad de escritura de dockerd y saturación de disco. Reducir temporalmente el volumen de logs en una prueba controlada es una forma fácil de validar causalidad.
¿Por qué los problemas de DNS parecen lentitud de la aplicación?
Porque los fallos de DNS suelen ser fallos lentos (timeouts) en lugar de errores rápidos. Las llamadas se bloquean mientras resuelven. Bajo carga, esos hilos bloqueados se cascadan en encolamiento y timeouts en otros lados.
¿Cuál es la forma más rápida de confirmar que el disco es el cuello de botella?
iostat -xz 1 en el host para saturación y pidstat -d para encontrar quién está escribiendo. Alto await más alta utilización es una pistola humeante.
¿Por qué escalar horizontalmente a veces lo empeora?
Porque estás escalando el recurso equivocado. Más contenedores pueden aumentar la contención en un disco compartido, NAT/conntrack compartido, DNS compartido o pipeline de logging centralizado.
¿Estos problemas son “específicos de Docker” o “específicos de Linux”?
Mayormente son específicos de Linux. Docker los hace más fáciles de desencadenar porque añade capas y defaults. El kernel aún hace el trabajo real, y en el kernel viven los cuellos de botella.
¿Debería cambiar a host networking por rendimiento?
A veces ayuda al reducir overhead de NAT/conntrack, pero cambia aislamiento y gestión de puertos. Úsalo primero como herramienta diagnóstica y luego decide según el riesgo y la ganancia medible.
Conclusión: próximos pasos que puedes hacer esta semana
Si tus contenedores se retrasan bajo carga y la CPU está baja, deja de mirar la CPU como si te debiera dinero. Trata el rendimiento como una investigación: encuentra qué está esperando, no qué está ocupado.
- Instrumenta los cuellos de botella que sigues perdiendo: await/utilización de disco, actividad de swap, contadores de throttling de cgroup, tiempo de resolución DNS, uso de conntrack y crecimiento de logs de Docker.
- Mueve las rutas de escritura caliente fuera de la capa escribible del contenedor: usa volumes para temp, caches y todo lo que se parezca a una base de datos.
- Arregla los “asesinos silenciosos”: search domains/ndots de DNS, techos de conntrack, ulimits y rotación de logs.
- Reevalúa límites: especialmente las cuotas de CPU. Si quieres fairness, no compres latencia por accidente.
- Baselinea nodos y guarda los informes: es aburrido, y previene outages por los que nunca recibirás crédito.
Los contenedores no son lentos. Las restricciones no medidas son lentas. Docker solo hace más fácil fingir que las restricciones no existen—hasta que tu gráfico de p99 empieza a gritar.