Te das cuenta durante un incidente, porque es cuando eres más honesto contigo mismo.
La app está “bien”, la CPU es aburrida, la red está tranquila, pero las peticiones se atoran.
En algún lugar, un contenedor escribe en disco como si cincelara bytes en granito.
Nueve de cada diez veces, el culpable no es “Docker es lento”. Es el sistema de archivos por defecto de los contenedores (overlay2) haciendo exactamente para lo que fue diseñado:
facilitar imágenes y capas de contenedor. No es rápido para cargas con muchas escrituras y que exigen fsync.
Overlay2 en un modelo mental (y por qué las escrituras duelen)
overlay2 es el driver de almacenamiento de Docker que usa Linux OverlayFS: un sistema de archivos en unión que superpone un “upperdir” escribible sobre una o más capas de solo lectura “lowerdir” (tus capas de imagen).
Cuando un contenedor lee un archivo que existe en la imagen, lo lee desde las capas inferiores. Cuando escribe, lo hace en la capa superior.
El problema comienza en la intersección de copy-on-write y tormenta de metadatos.
Si un contenedor modifica un archivo que estaba en la capa inferior, OverlayFS a menudo necesita “copiar hacia arriba” el archivo al upperdir primero.
Ese copy-up es I/O real. Puede ser grande. Además conlleva operaciones de metadatos que en teoría son baratas y en la práctica son costosas a escala.
Para cargas con muchas escrituras, hay dos patrones especialmente castigadores:
- Escrituras pequeñas y aleatorias con fsync (bases de datos, SQLite, colas con flags de durabilidad). La capa en unión no es el único problema; es la indirección extra y el trabajo de metadatos, además de cómo se comporta el sistema de archivos del host bajo esa carga.
- Mucha creación/eliminación de archivos (cachés de build, descompresión de archivos, gestores de paquetes de lenguajes, directorios temporales). Overlay2 puede convertir “muchas escrituras pequeñas” en “muchas escrituras pequeñas más muchas operaciones de metadatos pequeñas”.
Los volúmenes existen porque al final admitimos que ejecutamos cargas con estado. Un volumen Docker es esencialmente un directorio gestionado por Docker, montado en el contenedor directamente desde el sistema de archivos del host (esa ruta no pasa por la unión).
En la ruta montada, evitas la sobrecarga de copy-on-write y normalmente reduces la amplificación de escritura.
La decisión no es filosófica. Es mecánica: si los datos son mutables y sensibles al rendimiento, deja de almacenarlos en la capa del contenedor.
Mantén el sistema de archivos del contenedor para binarios y configuración; guarda el estado en un lugar aburrido y directo.
Datos interesantes y contexto histórico
Puntos breves y concretos que explican por qué estamos aquí:
- Docker no empezó con overlay2. Las primeras versiones usaban AUFS; era relativamente rápido y con muchas funcionalidades, pero no era amigable con el kernel upstream. El cambio hacia OverlayFS fue en parte para estar “en el kernel” y ser más mantenible.
- Las cicatrices de la era device mapper son reales. El driver devicemapper (especialmente loop-lvm) causó latencias de escritura espectaculares y momentos de “¿por qué mi disco está lleno?”. overlay2 se convirtió en la opción por defecto por buenas razones.
- OverlayFS maduró a través de varias versiones del kernel. Funcionalidades como múltiples capas lower y parches de rendimiento llegaron de forma iterativa. La versión del kernel importa más de lo que la gente admite.
- En XFS importa d_type. OverlayFS requiere soporte de tipo de entrada de directorio (d_type). En XFS eso se controla con
ftype=1. Si está mal, Docker mostrará advertencias—o peor, obtendrás comportamientos/performance extraños. - Copy-up no es hipotético. Editar un archivo que estaba en la capa de imagen desencadena la copia de ese archivo al upperdir del contenedor. Para archivos grandes, pagas de inmediato.
- Los whiteouts son cómo funcionan las eliminaciones en los sistemas en unión. Cuando borras un archivo que existe en una capa inferior, OverlayFS registra un “whiteout” en upperdir. Eso es metadatos extra, y se acumula.
- Los sistemas de archivos con journaling cambian latencia por seguridad. ext4 y XFS tienen comportamientos por defecto distintos; añade apps que exigen fsync y puedes amplificar el dolor. Overlay2 no lo borra; puede magnificarlo.
- Los logs de contenedores no son especiales. Si registras en un archivo dentro de la capa del contenedor, estás escribiendo en overlay2. Si registras en stdout, el driver de logs de Docker escribe en otro lugar—todavía en disco, pero con rutas y modos de fallo distintos.
- “Va rápido en mi portátil” suele ser cache de página, no throughput. Overlay2 puede parecer excelente hasta que te encuentras con escrituras sincronas, presión de memoria o un nodo con vecinos ruidosos.
Cómo se ve “escrituras lentas” en producción
La lentitud de overlay2 rara vez es un único disparador. Es un conjunto de síntomas que riman:
- La latencia P99 sube mientras la CPU se mantiene tranquila. Los hilos de la app se bloquean por I/O.
- Los checkpoints o compactaciones de la base de datos tardan más dentro de contenedores que en el host.
- “El disco no está lleno” pero las escrituras se detienen: en realidad estás sin inodos, bloqueado por contención del journal o limitado por la cola del dispositivo de bloques subyacente.
- Subidas de iowait a nivel de nodo sin una historia de “alto throughput”. Es un signo clásico de escrituras pequeñas y sync.
- Los reinicios de contenedor son más lentos con el tiempo si la capa escribible acumula muchos archivos y whiteouts. Los recorridos de metadatos no envejecen bien.
La parte complicada: overlay2 no siempre es el cuello de botella. Puede que el problema sea el almacenamiento en bloque, las opciones de montaje del sistema de archivos, el kernel,
o que pusiste un WAL de base de datos en un sistema en unión y luego le pediste que haga fsync como si su trabajo dependiera de ello (y lo hace).
Guía de diagnóstico rápido
Cuando estás de guardia, no quieres una investigación de 40 pasos. Quieres una escalera corta que te lleve a “mover datos a un volumen” o “esto es el disco subyacente” rápidamente.
Primero: confirma qué está escribiendo y dónde
- Identifica los principales escritores a nivel de host (proceso, dispositivo).
- Mapea el PID del contenedor a un nombre de contenedor.
- Determina si el camino caliente está dentro de
/var/lib/docker/overlay2o en un volumen/bind mount.
Segundo: decide si es latencia por sync o throughput
- Si
awaites alto y%utiles alto: el dispositivo está saturado o con colas. - Si
awaites alto pero%utiles moderado: podrías estar pagando latencia por operación (fsync, bloqueos del journal, tormentas de metadatos). - Si la app llama a fsync constantemente: estás en el “juego de la latencia”, no en el “juego MB/s”.
Tercero: busca amplificadores específicos de overlay2
- Disparadores de copy-up (escribir en archivos que venían de la capa de imagen).
- Gran número de archivos en la capa escribible (presión de inodos, coste de recorrido de directorios).
- Desajuste del sistema de archivos subyacente (XFS ftype, opciones de montaje raras).
Cuarto: elige el cambio más pequeño y seguro
- Si son datos mutables: muévelos a un volumen o bind mount. Prefiere volúmenes por higiene operativa.
- Si es datos efímeros/caché de build: considera tmpfs si cabe, o acepta escrituras más lentas pero deja de persistirlos.
- Si es el disco subyacente: arregla la historia del disco (IOPS, latencia, scheduler, clase de almacenamiento). overlay2 solo es el mensajero.
Tareas prácticas: comandos, salidas, decisiones (12+)
Estas son las tareas que realmente ejecutas a las 2 a.m. Cada una tiene: un comando, qué significa una salida típica y qué decisión tomar a continuación.
Los comandos asumen un host Linux con Docker Engine y overlay2.
Task 1: Confirmar que Docker usa overlay2 (y en qué sistema de archivos está)
cr0x@server:~$ docker info --format '{{.Driver}} {{.DockerRootDir}}'
overlay2 /var/lib/docker
Qué significa: El driver de almacenamiento es overlay2; la raíz de datos de Docker es /var/lib/docker.
Decisión: Todas las capas escribibles de los contenedores viven bajo esa raíz a menos que la hayas movido. Ahí buscas el calor.
Task 2: Comprobar tipo de sistema de archivos 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: La raíz de Docker está en ext4, con opciones típicas.
Decisión: Si esto es almacenamiento en red o un HDD lento, deja de culpar a overlay2 y empieza a culpar a la física. Si es XFS, verifica ftype=1 (Task 3).
Task 3: Si usas XFS, verifica soporte d_type (ftype=1)
cr0x@server:~$ xfs_info /dev/nvme0n1p2 | grep ftype
naming =version 2 bsize=4096 ascii-ci=0, ftype=1
Qué significa: OverlayFS puede funcionar correctamente. ftype=0 es una señal de alarma.
Decisión: Si ftype=0, planifica una migración a un sistema de archivos formateado correctamente. No “tunes” alrededor de un desajuste estructural.
Task 4: Encontrar dispositivos de bloque principales y si están saturados
cr0x@server:~$ iostat -xz 1 5
Linux 6.2.0 (server) 01/03/2026 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.10 0.00 3.40 22.80 0.00 61.70
Device r/s w/s rMB/s wMB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-s wareq-s svctm %util
nvme0n1 10.0 980.0 0.5 12.0 0.0 20.0 0.0 2.0 1.20 18.50 20.10 52.0 12.5 0.90 89.0
Qué significa: Las escrituras dominan; w_await es alto y %util está cerca de saturación. El dispositivo está haciendo cola.
Decisión: Esto no es puramente overlay2. Necesitas más IOPS/menor latencia de almacenamiento, o menos escrituras sync, o aislar cargas.
Task 5: Identificar qué proceso está haciendo el I/O
cr0x@server:~$ sudo iotop -oPa
Total DISK READ: 0.00 B/s | Total DISK WRITE: 25.30 M/s
PID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
23144 be/4 999 0.00 B/s 18.20 M/s 0.00 % 35.00 % postgres: wal writer process
19872 be/4 root 0.00 B/s 5.10 M/s 0.00 % 10.00 % dockerd --host=fd://
Qué significa: Un proceso de base de datos (probablemente dentro de un contenedor) está empujando escrituras WAL; dockerd también está activo.
Decisión: Mapea el PID al contenedor y verifica dónde vive su directorio de datos. Bases de datos en overlay2 suelen ser un auto-sabotaje.
Task 6: Mapear un PID a un contenedor
cr0x@server:~$ ps -o pid,cmd -p 23144
PID CMD
23144 postgres: wal writer process
cr0x@server:~$ sudo cat /proc/23144/cgroup | grep -E 'docker|kubepods' | head -n 1
0::/docker/5c3b1f2d0b0a8b3d5a2d6c5d8c1f0e9a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3
Qué significa: El proceso pertenece a un contenedor Docker con ese prefijo de ID.
Decisión: Inspecciona los mounts de ese contenedor (Task 7). Si PGDATA no es un volumen/bind mount, arréglalo.
Task 7: Comprobar mounts del contenedor y confirmar si los datos están en overlay2
cr0x@server:~$ docker inspect 5c3b1f2d0b0a --format '{{range .Mounts}}{{println .Destination .Type .Source}}{{end}}'
/var/lib/postgresql/data volume /var/lib/docker/volumes/pgdata/_data
Qué significa: El directorio de datos es un volumen Docker (bien). Si no ves ningún mount para PGDATA, estaría dentro de overlay2 (mal).
Decisión: Si ya es un volumen y sigue lento, el cuello de botella probablemente sea el almacenamiento subyacente o los patrones de fsync, no el layering de overlay2.
Task 8: Probar si las escrituras tocan rutas de overlay2
cr0x@server:~$ sudo lsof -p 23144 | grep overlay2 | head
postgres 23144 999 cwd DIR 8,2 4096 131081 /var/lib/docker/overlay2/9d2f.../merged/var/lib/postgresql/data
Qué significa: Si ves archivos abiertos bajo /var/lib/docker/overlay2/.../merged, ese proceso está operando en el montaje en unión.
Decisión: Mueve esa ruta a un volumen. Si es una base de datos, no lo discutas—hazlo.
Task 9: Revisar agotamiento de inodos (el sigiloso “disco lleno”)
cr0x@server:~$ df -hi /var/lib/docker
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/nvme0n1p2 20M 19M 1M 95% /var/lib/docker
Qué significa: Estás cerca de quedarte sin inodos. Las escrituras pueden fallar o degradarse mientras el FS lucha.
Decisión: Limpia imágenes/capas, mueve directorios con alta rotación a volúmenes, y considera un sistema de archivos con más inodos o un layout distinto para la raíz de Docker.
Task 10: Encontrar capas escribibles grandes y procesos que churn
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.ID}}'
NAMES ID
api-1 2f1c9b8c0d3a
worker-1 8a7d6c5b4e3f
postgres-1 5c3b1f2d0b0a
cr0x@server:~$ docker container inspect api-1 --format '{{.GraphDriver.Data.UpperDir}}'
/var/lib/docker/overlay2/1a2b3c4d5e6f7g8h9i0j/upper
cr0x@server:~$ sudo du -sh /var/lib/docker/overlay2/1a2b3c4d5e6f7g8h9i0j/upper
18G /var/lib/docker/overlay2/1a2b3c4d5e6f7g8h9i0j/upper
Qué significa: La capa escribible del contenedor es enorme. Normalmente significa que alguien escribe datos que deberían ser un volumen, o que se está cacheando agresivamente dentro del sistema de archivos del contenedor.
Decisión: Identifica qué directorios crecen y muévelos a volúmenes/bind mounts (o tmpfs). Trata un upperdir gigante como un olor operativo.
Task 11: Confirmar si tu carga está limitada por fsync
cr0x@server:~$ sudo strace -p 23144 -e trace=fdatasync,fsync -tt -T -f
18:20:11.102938 fdatasync(7) = 0 <0.024981>
18:20:11.128201 fdatasync(7) = 0 <0.031442>
Qué significa: Cada llamada sync cuesta ~25–30ms. Eso es brutal para el throughput de una base de datos. overlay2 puede añadir overhead, pero el enemigo real es la latencia de sync.
Decisión: Pon el WAL/datos en el almacenamiento más rápido que puedas justificar, y evita rutas en union FS. Si no puedes cambiar el almacenamiento, reduce la frecuencia de fsync solo si tu modelo de durabilidad lo permite.
Task 12: Benchmarkea la ruta overlay2 vs una ruta de volumen (A/B test, no intuiciones)
cr0x@server:~$ docker run --rm -it alpine sh -lc 'apk add --no-cache fio >/dev/null && fio --name=randwrite --directory=/tmp --size=512m --bs=4k --rw=randwrite --iodepth=1 --direct=1 --numjobs=1 --runtime=20 --time_based --fsync=1'
randwrite: (groupid=0, jobs=1): err= 0: pid=23: Fri Jan 3 18:20:45 2026
write: IOPS=120, BW=480KiB/s (492kB/s)(9600KiB/20001msec)
clat (usec): min=2000, max=65000, avg=8000.00, stdev=5000.00
cr0x@server:~$ docker volume create fiotest
fiotest
cr0x@server:~$ docker run --rm -it -v fiotest:/data alpine sh -lc 'apk add --no-cache fio >/dev/null && fio --name=randwrite --directory=/data --size=512m --bs=4k --rw=randwrite --iodepth=1 --direct=1 --numjobs=1 --runtime=20 --time_based --fsync=1'
randwrite: (groupid=0, jobs=1): err= 0: pid=23: Fri Jan 3 18:21:15 2026
write: IOPS=320, BW=1280KiB/s (1311kB/s)(25600KiB/20002msec)
clat (usec): min=1500, max=32000, avg=3500.00, stdev=2000.00
Qué significa: La ruta de volumen ofrece IOPS significativamente mejores y menor latencia para escrituras sincronas de 4k.
Decisión: Si tu carga real se parece a este patrón (bases de datos, colas), muévela fuera de overlay2.
Task 13: Comprobar el driver de logs de Docker y el impacto de la ruta de logs
cr0x@server:~$ docker info --format '{{.LoggingDriver}}'
json-file
cr0x@server:~$ docker inspect api-1 --format '{{.LogPath}}'
/var/lib/docker/containers/2f1c9b8c0d3a.../2f1c9b8c0d3a...-json.log
cr0x@server:~$ sudo ls -lh /var/lib/docker/containers/2f1c9b8c0d3a.../*json.log
-rw-r----- 1 root root 9.2G Jan 3 18:21 /var/lib/docker/containers/2f1c9b8c0d3a.../2f1c9b8c0d3a...-json.log
Qué significa: Los logs están creciendo en la raíz de Docker. Eso puede competir con las escrituras de overlay2 y llenar discos rápido.
Decisión: Rota logs, pon límites, o cambia a un driver de logs adecuado para tu entorno. No dejes que el “printf debugging” se convierta en un DoS de almacenamiento.
Task 14: Buscar errores de propagación de montajes (volumen no montado realmente)
cr0x@server:~$ docker exec -it api-1 sh -lc 'mount | grep -E "/data|overlay" | head -n 3'
overlay on / type overlay (rw,relatime,lowerdir=...,upperdir=...,workdir=...)
/dev/nvme0n1p2 on /data type ext4 (rw,relatime)
Qué significa: /data es un montaje real (bien). Si solo vieras el montaje overlay y no un montaje separado para tu ruta de datos, tu “volumen” no está montado donde crees.
Decisión: Arregla la especificación del contenedor (ruta de destino errónea, typo, falta de -v), y vuelve a probar el rendimiento. Las suposiciones son donde nacen los outages.
Cuándo cambiar a volúmenes (y cuándo no)
Cambia a volúmenes cuando los datos son mutables y se cumple alguno de estos
- Es una base de datos (Postgres, MySQL, MongoDB, Redis con AOF, etc.). Las bases de datos no son “solo archivos”. Son máquinas de fsync cuidadosamente coreografiadas.
- Es un write-ahead log o journal (segmentos tipo Kafka, logs de durabilidad de colas, translogs de motores de búsqueda). Estas cargas castigan la latencia.
- Es estado de alta rotación (subidas, directorios de caché importantes, contenido generado por usuarios).
- Es output de build que necesitas entre reinicios (cachés de CI, caches de paquetes) y quieres predictibilidad.
- Necesitas semánticas de backup/restore que no sean “commitea el contenedor y reza”. Los volúmenes te dan un límite claro para snapshot y migración.
Mantén overlay2 para lo que es bueno
- Bits de aplicación inmutables: binarios, librerías, plantillas de configuración. Para eso sirven las capas de imagen.
- Archivos temporales de corta vida que no necesitan persistir y no hacen un millón de fsync. Si necesitas velocidad, usa tmpfs. Si necesitas persistencia, usa un volumen.
Volúmenes vs bind mounts: elige deliberadamente
Un bind mount es “monta esta ruta del host en el contenedor”. Un volumen Docker es “Docker gestiona una ruta del host y la monta por ti”.
El rendimiento puede ser similar porque ambos evitan la capa en unión en esa ruta. La diferencia es operativa:
- Los volúmenes son más fáciles de enumerar, migrar y razonar con herramientas de Docker. También evitan acoplamientos accidentales al layout del host.
- Los bind mounts son excelentes cuando necesitas control fino (árboles de directorio preexistentes, sistema de archivos específico o integración con herramientas del host). También facilitan montar algo que no pretendías montar.
Mi sesgo: en producción, prefiere volúmenes salvo que tengas una razón clara para no hacerlo.
Tu yo futuro agradece menos conversaciones tipo “¿por qué esta ruta está vacía en el nodo nuevo?”.
Chiste #1: Ejecutar una base de datos en overlay2 es como usar chanclas en una obra—posible, pero el informe de lesiones se escribe solo.
Cómo cambiar de forma segura: patrones que no te despertarán por la noche
Patrón 1: Directorios de datos explícitos, nunca “lo que usa la imagen por defecto”
Las imágenes suelen definir rutas de datos por defecto dentro del sistema de archivos del contenedor. Si no las sobrescribes con un montaje de volumen, estás usando implicitamente overlay2.
Para bases de datos, sé explícito y ruidoso.
cr0x@server:~$ docker volume create pgdata
pgdata
cr0x@server:~$ docker run -d --name pg \
-e POSTGRES_PASSWORD=example \
-v pgdata:/var/lib/postgresql/data \
postgres:16
c1d2e3f4a5b6c7d8e9f0
Patrón 2: Mueve solo la ruta caliente, no todo el sistema de archivos
No necesitas montar /. No necesitas replattformar todo. Identifica los directorios con muchas escrituras:
datos de la base de datos, WAL, subidas, cachés, logs (a veces).
Monta esos. Deja el resto tranquilo.
Patrón 3: Separa WAL/log de los datos cuando las necesidades de latencia difieran
No todos los despliegues lo necesitan, pero cuando lo hacen, es un salvavidas:
pon el WAL (o equivalente) en la clase de almacenamiento más rápida y deja los datos en almacenamiento de capacidad.
Esto es más fácil con orquestadores, pero también puedes hacerlo con Docker si eres disciplinado.
Patrón 4: Evita escribir logs en las capas del contenedor
Loggear a stdout no es automáticamente “gratis”, pero evita el sistema en unión para logs de la app.
Si registras en archivos, monta un volumen o bind mount para el directorio de logs y rota.
Patrón 5: Decide qué durabilidad necesitas realmente
El debate del driver de almacenamiento a menudo oculta una decisión de negocio: ¿es aceptable la pérdida de datos?
Si desactivas fsync (o usas modos asíncronos), obtendrás mejores números—hasta que no.
“Podemos perder los últimos 5 segundos de datos” es una política válida. “No lo habíamos pensado” no lo es.
Tres micro-historias corporativas desde el frente
Micro-historia 1: El incidente causado por una suposición equivocada
Un equipo desplegó un nuevo servicio API que ingería eventos y los escribía en una cola local antes de enviarlos al pipeline principal.
En staging parecía bien. En producción, empezó a hacer timeouts bajo carga. El gráfico del on-call mostró latencia en aumento y un repentino incremento de iowait.
La CPU estaba baja, lo que hizo que todos sospecharan de “la red” porque así es como actuamos cuando la CPU no es culpable.
Asumieron que porque la cola era “solo un archivo”, se comportaría como un archivo del host. Pero el archivo vivía dentro del sistema de archivos del contenedor.
Eso significaba overlay2. Eso significaba semánticas de montaje en unión. Y bajo carga, la cola hizo lo que hacen las colas: muchas pequeñas adiciones, muchos fsync.
El síntoma fue extraño: el throughput iba bien unos minutos tras el deploy y luego degradaba.
La capa escribible creció, los metadatos se calentaron y la cola del dispositivo se construyó.
Alguien intentó “más CPU” porque los dashboards parecían vacíos. No hizo nada. Por supuesto que no hizo nada.
La solución fue aburrida: montar un volumen en el directorio de la cola y redeployar.
La latencia se estabilizó. La amplificación de escritura bajó. El incidente terminó en silencio, que es el único tipo de final que deberías perseguir.
La lección real: si un componente es un límite de durabilidad (cola, journal, base de datos), trata su ruta de almacenamiento como infraestructura, no como un detalle de implementación.
La capa del contenedor no es infraestructura; es empaquetado.
Micro-historia 2: La optimización que salió mal
Un equipo de plataforma vio tiempos de build lentos en CI. Los contenedores estaban desempaquetando dependencias repetidamente.
Decidieron “acelerarlo” cacheando directorios de paquetes dentro del sistema de archivos del contenedor, pensando que así evitarían I/O externo.
También les gustó la simplicidad: menos volúmenes, menos montajes, menos “estado”.
Funcionó—por un tiempo. Luego el uso de disco se disparó bajo /var/lib/docker/overlay2.
El host no estaba sin bytes, pero sangraba inodos. Los trabajos de limpieza empezaron a tardar más.
Los nuevos builds se volvieron más lentos, no más rápidos, porque cada contenedor partía con una capa escribible grande y mucho churn de directorio.
El equipo entonces “optimizó” la limpieza podando imágenes agresivamente entre jobs.
Eso redujo el uso de disco pero añadió tráfico al registry y fallos de cache, e incrementó la presión de escritura durante el desempaquetado porque nada estaba caliente ya.
El efecto neto fue mayor variabilidad y más timeouts esporádicos.
La solución fue mover las cachés a un volumen dedicado (o un directorio de cache por runner vía bind mount) y gestionarlo intencionalmente:
límites, expiración y propiedad clara. El output de build volvió a ser predecible.
La moraleja: el cache es almacenamiento. Si lo tratas como un efecto secundario, él tratará a tu disco como una sugerencia.
Micro-historia 3: La práctica aburrida pero correcta que salvó el día
Otra organización ejecutaba varios servicios stateful en contenedores—no por moda, sino porque reducía drift y hacía las actualizaciones menos temibles.
Tenían una regla que sonaba poco glamorosa en las revisiones de diseño: todo estado persistente debe ir en volúmenes con nombre, y cada servicio debe documentar sus mounts de volumen.
Cuando un nodo empezó a mostrar latencia de escritura elevada, pudieron separar inmediatamente “escrituras overlay2” de “escrituras en volúmenes”.
Sus dashboards monitorizaban latencia por dispositivo, y sus specs de despliegue dejaban claro qué vivía dónde.
El triage fue rápido porque el layout era consistente entre servicios.
Durante el incidente, migraron en vivo una carga a un nodo con mejor almacenamiento y re-adjuntaron volúmenes.
Nada de la capa del contenedor importaba, así que no tuvieron que copiar directorios aleatorios desde /var/lib/docker/overlay2.
El tiempo de recuperación se midió en minutos, no en arqueología.
A los propietarios del servicio les disgustaba la regla durante el prototipado porque les obligaba a pensar en rutas desde el inicio.
Al equipo de on-call le encantó para siempre.
Chiste #2: Nada arruina la narrativa de “microservicio sin estado” como una capa escribible de 20GB llamada “upper”.
Errores comunes: síntoma → causa raíz → solución
Esta es la sección que lees después de haber intentado reiniciarlo. No te preocupes, todos hemos estado ahí.
1) Las escrituras son lentas solo dentro del contenedor
Síntoma: La misma operación en el host es rápida; dentro del contenedor es lenta.
Causa raíz: La ruta de datos está dentro de overlay2, pagando copy-on-write y sobrecarga de metadatos; o el contenedor usa patrones de fsync distintos por configuración.
Solución: Mueve la ruta escribible a un volumen/bind mount. Verifica con mount en el contenedor y benchmarkea A/B.
2) Latencia de base de datos que sube durante checkpoints/flushes
Síntoma: Paradas periódicas; el writer de WAL o el proceso de checkpoint muestra alto IO wait.
Causa raíz: Latencia de fsync y contención del journal en el almacenamiento subyacente; overlay2 puede magnificarlo si los archivos DB están en la capa escribible.
Solución: Asegura que los datos/WAL de la BD estén en volúmenes sobre almacenamiento rápido. Si sigue lento, arregla IOPS/latencia subyacente (clase de almacenamiento, dispositivo, tuning FS).
3) Errores de “disco lleno” pero df -h muestra espacio disponible
Síntoma: Las escrituras fallan; los pulls de Docker fallan; contenedores colapsan; pero no se han agotado los bytes.
Causa raíz: Agotamiento de inodos en la raíz de Docker, a menudo por alto churn de archivos en upperdirs de overlay2 o caches de build.
Solución: Verifica con df -hi. Prunea, reduce churn, mueve rutas de alto churn a volúmenes, y considera un sistema de archivos/layout con suficientes inodos.
4) El rendimiento degrada con el tiempo sin aumento de carga
Síntoma: Mismo RPS, peor latencia después de días/semanas.
Causa raíz: Las capas escribibles acumulan muchos archivos/whiteouts; las operaciones de metadatos se vuelven más lentas; backups/antivirus tocan la raíz de Docker; los archivos de logs crecen.
Solución: Mantén el estado fuera de overlay2, rota logs, prunea imágenes/contenedores no usados, y no pongas la raíz de Docker en un FS compartido con procesos ruidosos del host.
5) “Migramos a volúmenes y sigue lento”
Síntoma: Los datos están en un volumen, pero la latencia de escritura sigue alta.
Causa raíz: El almacenamiento subyacente es el cuello de botella (disco de red limitado en IOPS, créditos de ráfaga, throttling), o la carga está limitada por latencia de sync.
Solución: Mide latencia del dispositivo con iostat, mira tiempos de fsync, y mejora/aisla el almacenamiento. Un volumen no puede hacer rápido un disco lento.
6) Errores misteriosos en raíz Docker en XFS
Síntoma: Comportamiento extraño de archivos o advertencias; a veces rendimiento pobre.
Causa raíz: XFS formateado con ftype=0 (sin d_type) o combinación kernel/filesystem no soportada.
Solución: Migra la raíz de Docker a XFS con ftype=1 (o usa ext4). Esto es trabajo de reconstrucción/migración, no un toggle.
7) Crecimiento masivo de la raíz de Docker y nodo lento
Síntoma: /var/lib/docker crece rápidamente; el nodo se vuelve torpe; operaciones de contenedor lentas.
Causa raíz: Logs descontrolados de contenedores, capas escribibles grandes, o artefactos de build fuera de control dentro de contenedores.
Solución: Implementa límites/rotación de logs, mueve artefactos a volúmenes, y aplica políticas sobre builds e escrituras en tiempo de ejecución.
Listas de verificación / plan paso a paso
Checklist A: Decide si debes cambiar a volúmenes (filtro rápido)
- ¿Los datos son mutables? Si sí, inclínate por volumen.
- ¿La carga hace fsync/fdatasync con frecuencia? Si sí, evita overlay2 para esa ruta.
- ¿La ruta crecerá más allá de unos cientos de MB? Si sí, no la dejes en upperdir.
- ¿Necesitas backups o migración? Si sí, los volúmenes lo hacen manejable.
- ¿Es una caché que puedes descartar? Si sí, considera tmpfs o acepta overlay2 pero con límites/limpieza.
Checklist B: Plan de migración para un servicio stateful (sin drama)
- Identifica los verdaderos directorios de datos. Lee la config de la app. No adivines. Para bases de datos, encuentra data dir y WAL/redo logs.
- Crea volúmenes con nombres significativos. Evita “data1” para todo; lo odiarás luego.
- Detén el servicio limpiamente. Déjalo flush. “Kill -9” no es herramienta de migración.
- Copia los datos de la ruta antigua al volumen. Preserva propiedad y permisos.
- Monta el volumen en la misma ruta exacta que la app espera. La consistencia gana.
- Inicia y valida con comprobaciones a nivel de aplicación. No solo “el contenedor está corriendo”.
- Benchmarkea la ruta de escritura (fio o métricas de la app) y compárala con la línea base.
- Define políticas: rotación de logs, límites de tamaño, calendarios de prune.
Checklist C: Si los volúmenes no lo arreglaron (realidad del almacenamiento)
- Mide latencia y saturación del dispositivo con
iostat. - Busca throttling por almacenamiento burstable (los volúmenes cloud lo hacen silenciosamente).
- Confirma salud del sistema de archivos y opciones de montaje.
- Busca vecinos ruidosos: otros contenedores escribiendo logs o haciendo compactaciones.
- Considera separar raíz de Docker, volúmenes y logs en diferentes dispositivos.
Preguntas frecuentes
1) ¿overlay2 es “lento” por diseño?
overlay2 está optimizado para capas de imagen y uso razonable del sistema de archivos del contenedor. No está diseñado como un sistema de archivos de alto rendimiento para bases de datos.
Para escrituras intensas y sensibles a la latencia de sync, suele ser más lento que un montaje directo.
2) ¿Los volúmenes Docker siempre rinden mejor que overlay2?
No siempre. Si el almacenamiento subyacente es lento, un volumen no arreglará la física.
Pero los volúmenes eliminan la sobrecarga del sistema en unión en la ruta montada, lo que suele ayudar para cargas ricas en metadatos y fsync.
3) ¿Y los bind mounts—son “más rápidos” que los volúmenes?
El rendimiento suele ser similar. La diferencia es manejabilidad y seguridad.
Los volúmenes son más fáciles de inventariar y migrar con herramientas Docker; los bind mounts son rutas explícitas del host y útiles cuando necesitas ese control.
4) ¿Por qué las escrituras pequeñas perjudican más que las grandes secuenciales?
Las escrituras pequeñas y sincronas están dominadas por la latencia por operación: actualizaciones de metadatos, commits de journal, flushes y a veces barreras.
overlay2 añade capas extras de trabajo de sistema de archivos. Las escrituras grandes secuenciales pueden ser bufferizadas y transmitidas de forma más eficiente.
5) ¿Puedo “tunear” overlay2 para que rinda como volúmenes?
Puedes reducir el dolor (actualizaciones del kernel, elección del sistema de archivos, evitar patrones de copy-up), pero no puedes eliminar las semánticas fundamentales de un sistema en unión.
Si te importa el rendimiento de escrituras en datos mutables, móntalos desde el host.
6) ¿Debería poner /var/lib/docker en un disco separado?
A menudo sí en producción. Separar la raíz de Docker del disco del SO reduce contención y facilita la planificación de capacidad.
Si los logs también están en el mismo disco, básicamente invitas a un servicio ruidoso a compartir habitación con alguien que duerme ligero.
7) ¿Es tmpfs una buena alternativa a volúmenes para rendimiento?
Para datos verdaderamente efímeros, sí. tmpfs es rápido y evita latencias de disco.
Pero consume RAM (y puede hacer swap si no tienes cuidado). No pongas datos “importantes” en tmpfs salvo que disfrutes explicar pérdidas de datos.
8) ¿Kubernetes cambia el consejo?
El principio se mantiene: no almacenes estado mutable y sensible al rendimiento en la capa del contenedor.
En Kubernetes usarás PersistentVolumes, hostPath (con cuidado) o volúmenes efímeros como emptyDir (respaldados por disco o memoria) según necesidades de durabilidad.
9) Si mi base de datos está en un volumen, ¿puedo ignorar overlay2 completamente?
No por completo. El arranque/parada de contenedores, pulls de imagen y cualquier escritura fuera de tus directorios montados todavía golpean overlay2 y la raíz de Docker.
Mantén la raíz de Docker saludable: espacio, inodos, rotación de logs y buena higiene de imágenes.
10) ¿Cuál es la regla más simple que previene la mayoría de incidentes de escrituras overlay2?
Si persiste tras reinicios y te entristecería que desaparezca, va en un volumen. Si es una base de datos, va en un volumen incluso si crees que estarías bien sin ello.
Próximos pasos prácticos
overlay2 es un gran valor por defecto para empaquetar y lanzar software. No es un buen lugar para esconder datos mutables, de alta rotación o sensibles a la durabilidad.
Cuando las escrituras son lentas, la pregunta correcta es: “¿Qué ruta está caliente y vive en la capa del contenedor?”
Pasos que puedes hacer hoy:
- Ejecuta la guía de diagnóstico rápido: identifica el escritor, mapea al contenedor, confirma la ruta de datos.
- Haz un benchmark overlay2 vs ruta de volumen con un rápido test fio A/B que refleje tu carga (sync vs async importa).
- Mueve bases de datos, colas y logs importantes a volúmenes. Mantén el resto en la imagen.
- Pon guardarraíles: límites de logs, monitorización de inodos y una política simple de que el estado persistente nunca vive en upperdir.
Una idea parafraseada de Werner Vogels (CTO de Amazon): construyes fiabilidad esperando fallos y diseñando sistemas que sigan funcionando de todas formas.
Trata a overlay2 como una capa de empaquetado, no como una estrategia de almacenamiento, y tus sistemas serán aburridos de la mejor manera posible.