Docker “too many open files”: aumentar límites correctamente (systemd + contenedor)

¿Te fue útil?

Siempre es la misma sensación: todo va bien hasta el pico de tráfico; entonces tu aplicación empieza a lanzar EMFILE, los registros se llenan con “too many open files” y el canal de incidentes se convierte en una sesión de terapia grupal.

Cuando esto ocurre en Docker, la gente suele “arreglarlo” subiendo algún límite en un lugar aleatorio. A veces funciona. Más a menudo no—o funciona hasta el siguiente despliegue, reinicio o rotación de nodo. Hagámoslo bien: encuentra qué límite se está alcanzando realmente, elévalo en la capa correcta (systemd, demonio Docker, contenedor) y asegúrate de que no estás simplemente escondiendo una fuga.

Qué significa realmente “too many open files” en contenedores

En Linux, “too many open files” suele corresponder a EMFILE (límite de descriptores de archivo por proceso alcanzado) o a ENFILE (agotamiento de la tabla de archivos a nivel de sistema). La mayoría de los incidentes en contenedores son EMFILE: un proceso (o unos pocos) alcanza su tope de descriptores y colapsa de manera dramática: no puede aceptar conexiones, no puede abrir archivos de registro, no puede resolver DNS, no puede comunicarse con upstreams, no puede crear sockets. En otras palabras: no puede hacer su trabajo.

En Docker, los descriptores de archivo (FDs) no son una abstracción del contenedor. Son un recurso del kernel. Los contenedores comparten el mismo kernel y, en última instancia, la misma tabla global de archivos. Pero cada proceso sigue teniendo un límite por proceso (RLIMIT_NOFILE) que puede variar por servicio, por contenedor, por proceso, dependiendo de quién lo haya iniciado y qué límites se hayan aplicado.

Así que cuando un contenedor dice “too many open files”, estás depurando una pila de límites y configuraciones, incluyendo:

  • Límites globales del kernel para la tabla de archivos y máximos por proceso
  • Límites de sesión de usuario (PAM, /etc/security/limits.conf)—a veces relevantes, a menudo malinterpretados
  • Límites de unidad systemd para dockerd (y a veces para el runtime del contenedor si es separado)
  • Configuraciones de ulimit de Docker pasadas a los contenedores
  • El comportamiento de tu app: pools de conexiones, keep-alives, watchers de archivos, fugas, patrones de logging

Y sí, puedes “simplemente subir el límite”. Pero si lo haces a ciegas, le das a un proceso con fuga un cubo más grande y lo llamas fiabilidad. Eso no es ingeniería; es una estrategia de aplazamiento.

Una cita para mantener la honestidad: “Hope is not a strategy.” — General Gordon R. Sullivan

Guía de diagnóstico rápido (comprueba primero/segundo/tercero)

Cuando suena la alerta y necesitas detener la hemorragia, no empiezas editando cinco configuraciones y reiniciando el host. Empiezas identificando qué límite alcanzaste y dónde.

Primero: ¿es por proceso (EMFILE) o a nivel de sistema (ENFILE)?

  • Si solo un servicio falla y el host parece estable, sospecha EMFILE.
  • Si muchos servicios no relacionados empiezan a fallar al abrir sockets/archivos a la vez, sospecha ENFILE o agotamiento a nivel de host.

Segundo: confirma el límite dentro del contenedor que falla

  • Revisa ulimit -n dentro del contenedor (o vía /proc para el proceso exacto).
  • Comprueba cuántos FDs tiene el proceso abiertos ahora mismo (ls /proc/<pid>/fd | wc -l).

Tercero: verifica qué le concedió systemd a Docker

  • Si dockerd está limitado a algo bajo, puede restringir lo que los contenedores heredan.
  • Confirma LimitNOFILE en la unidad systemd de Docker y qué límite tiene actualmente el proceso Docker.

Cuarto: descarta “lo subí y no cambió nada”

  • Cambiar /etc/security/limits.conf no afecta a los servicios gestionados por systemd.
  • Editar unidades systemd no afecta procesos ya en ejecución hasta que se reinician.
  • Modificar las opciones por defecto del demonio Docker no cambia retroactivamente los contenedores en ejecución.

Quinto: decide si estás enmascarando una fuga

  • Un conteo de FDs que sube de forma sostenida durante horas/días es un patrón de fuga.
  • Un pico de FDs que sube con el tráfico y luego baja suele ser un escalado “normal”, aunque merece margen adicional.

Broma #1: Los descriptores de archivo son como los tenedores en la cocina de la oficina: todos piensan que hay suficientes hasta la hora del almuerzo.

Datos interesantes y contexto histórico (corto y concreto)

  1. Los UNIX tempranos tenían límites pequeños de FDs (a menudo 20 o 64 por proceso) porque la RAM era cara y las cargas de trabajo eran simples comparadas con hoy.
  2. select(2) históricamente limitaba FDs a 1024 debido a FD_SETSIZE, lo que influyó en cómo se escribieron servidores durante años—aun cuando existían APIs mejores.
  3. poll(2) y epoll(7) se convirtieron en la respuesta práctica para escalar a muchos sockets sin el techo de select.
  4. Linux cuenta “archivos abiertos” de forma amplia: archivos regulares, sockets, pipes, eventfds, signalfds—muchos de los cuales no parecen “archivos” para desarrolladores de apps.
  5. systemd cambió las reglas al controlar los límites de servicio; editar los archivos de inicio de shell no afecta a un servicio iniciado por PID 1.
  6. Los valores por defecto de Docker pueden ser conservadores según el empaquetado de la distro y la configuración del daemon; asumir que “los contenedores heredan el límite del host” a menudo es incorrecto.
  7. La presión en la tabla de archivos del kernel se manifiesta con comportamientos raros antes del fallo: picos de latencia, errores de connect(), y fallos de I/O aparentemente aleatorios.
  8. Algunos runtimes consumen FDs por conveniencia (como watchers de archivos agresivos en modos de desarrollo); llevar eso a producción es cómo te llaman a las 2 a.m.

La pila de límites: kernel, usuario, systemd, Docker, contenedor

No solucionas este problema memorizando un único mando mágico. Lo resuelves entendiendo la cadena de custodia de RLIMIT_NOFILE y los límites globales del kernel.

1) Tabla de archivos a nivel de kernel: fs.file-max

fs.file-max es un tope del sistema sobre el número de manejadores de archivos que el kernel asignará. Si lo alcanzas, muchas cosas fallan a la vez. Esta es la variante ENFILE y suele ser catastrófica a nivel de host.

2) Máximo por proceso: fs.nr_open

fs.nr_open es el techo impuesto por el kernel para archivos abiertos por proceso. Incluso si pones ulimit -n a un millón, el kernel te detendrá en nr_open. Esta es una trampa común de “¿por qué no se aplicó mi límite?”.

3) Límites de proceso: RLIMIT_NOFILE y herencia

Cada proceso tiene un límite suave y uno duro. El límite suave es el que usa el proceso; el límite duro es el máximo al que puede elevarse sin privilegio. Los procesos hijos heredan límites del padre a menos que se cambien explícitamente. Ese detalle importa porque:

  • dockerd hereda de systemd
  • los contenedores heredan del runtime y de la configuración de Docker
  • tu proceso de aplicación hereda del init/entrypoint del contenedor

4) Límites de servicio de systemd: el lugar adulto para configurarlos

En hosts de producción que ejecutan Docker como servicio systemd, la forma más fiable de fijar los límites de FD de Docker es un override de systemd para docker.service. Editar /etc/security/limits.conf no es incorrecto; es frecuentemente irrelevante para servicios gestionados por systemd.

5) ulimits de contenedor en Docker: explícito es mejor que “creo que hereda”

Puedes fijar ulimits por contenedor usando flags de Docker run, Compose o valores por defecto del daemon. Aquí la postura práctica: define límites por servicio explícitamente para cargas críticas. Hace el comportamiento portable entre hosts y reduce el drama de “funcionó en ese nodo”.

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

Estas son las comprobaciones que realmente ejecuto. Cada una incluye lo que significa la salida y qué decisión tomar a partir de ello.

Task 1: Confirmar el tipo de error en los logs (EMFILE vs ENFILE)

cr0x@server:~$ docker logs --tail=200 api-1 | egrep -i 'too many open files|emfile|enfile' || true
Error: EMFILE: too many open files, open '/app/logs/access.log'

Significado: EMFILE apunta a agotamiento de FDs por proceso, no a un colapso de la tabla de archivos de todo el host.

Decisión: Enfócate en ulimits del contenedor/aplicación y en el patrón de uso de FDs del proceso antes de tocar ajustes a nivel de kernel.

Task 2: Ver el ulimit del contenedor desde dentro

cr0x@server:~$ docker exec -it api-1 sh -lc 'ulimit -n; ulimit -Hn'
1024
1048576

Significado: El límite suave es 1024 (muy bajo para muchos servidores de red). El límite duro es alto, así que el proceso podría elevar su suave si lo solicitara (o si el entrypoint lo hace).

Decisión: Aumenta el límite suave mediante Docker/Compose ulimits para que la app arranque con un valor sensato.

Task 3: Identificar el PID del proceso worker real

cr0x@server:~$ docker exec -it api-1 sh -lc 'ps -eo pid,comm,args | head'
PID COMMAND         COMMAND
1   tini            tini -- node server.js
7   node            node server.js

Significado: PID 7 es el proceso Node real; PID 1 es el wrapper init.

Decisión: Inspecciona límites y FDs abiertos para PID 7, no solo para PID 1.

Task 4: Leer el límite del proceso directamente desde /proc

cr0x@server:~$ docker exec -it api-1 sh -lc "grep -E 'Max open files' /proc/7/limits"
Max open files            1024                 1048576              files

Significado: Confirma el límite en tiempo de ejecución aplicado al proceso.

Decisión: Si es bajo, arregla la configuración de ulimit del contenedor; si es alto pero sigue fallando, busca fugas o problemas con fs.file-max/fs.nr_open.

Task 5: Contar FDs abiertos del proceso ahora mismo

cr0x@server:~$ docker exec -it api-1 sh -lc 'ls /proc/7/fd | wc -l'
1018

Significado: El proceso está básicamente en su techo de 1024; el fallo es esperado.

Decisión: Aumenta el límite e intenta reducir inmediatamente la presión de FDs si es posible (reducir concurrencia, pools de conexiones, watchers).

Task 6: Ver qué son esos FDs

cr0x@server:~$ docker exec -it api-1 sh -lc 'ls -l /proc/7/fd | head -n 15'
total 0
lrwx------ 1 root root 64 Jan  3 10:11 0 -> /dev/null
lrwx------ 1 root root 64 Jan  3 10:11 1 -> /app/logs/stdout.log
lrwx------ 1 root root 64 Jan  3 10:11 2 -> /app/logs/stderr.log
lrwx------ 1 root root 64 Jan  3 10:11 3 -> socket:[390112]
lrwx------ 1 root root 64 Jan  3 10:11 4 -> socket:[390115]
lrwx------ 1 root root 64 Jan  3 10:11 5 -> anon_inode:[eventpoll]

Significado: Mayormente sockets y eventpoll—típico en un servidor ocupado. Si ves miles del mismo path de archivo, sospecha una fuga en manejo de archivos o logging.

Decisión: Si son principalmente sockets, revisa la reutilización de conexiones upstream y keepalive del cliente; si son archivos, audita la apertura/cierre de archivos.

Task 7: Comprobar uso de la tabla de archivos a nivel de host

cr0x@server:~$ cat /proc/sys/fs/file-nr
15872	0	9223372036854775807

Significado: El primer número son manejadores de archivos asignados. El tercero es el máximo del sistema (este ejemplo es efectivamente “muy alto”). Si lo asignado se acerca al máximo, estás en territorio ENFILE.

Decisión: Si estás cerca del máximo, aumenta fs.file-max y busca los consumidores de FDs a nivel de host.

Task 8: Comprobar techos del kernel para archivos abiertos por proceso

cr0x@server:~$ sysctl fs.nr_open
fs.nr_open = 1048576

Significado: Ningún proceso puede exceder este número de archivos abiertos, independientemente de las solicitudes de ulimit.

Decisión: Si necesitas más y entiendes el coste en RAM, eleva fs.nr_open (raro; normalmente innecesario).

Task 9: Inspeccionar el límite actual del daemon Docker (herencia systemd)

cr0x@server:~$ pidof dockerd
1240
cr0x@server:~$ cat /proc/1240/limits | grep -E 'Max open files'
Max open files            1048576              1048576              files

Significado: El demonio Docker tiene un límite alto; bien. Si fuera 1024 o 4096, arreglarías systemd primero.

Decisión: Si el límite de dockerd es bajo, aplica un override de systemd y reinicia Docker. No discutas con PID 1.

Task 10: Confirmar límites de unidad systemd para Docker

cr0x@server:~$ systemctl show docker --property=LimitNOFILE
LimitNOFILE=1048576

Significado: Esto es lo que systemd pretende para el servicio, no lo que esperas que sea.

Decisión: Si esto es bajo, necesitas un drop-in override; si es alto pero dockerd está bajo, olvidaste reiniciar.

Task 11: Comprobar los ulimits efectivos de un contenedor en ejecución desde fuera

cr0x@server:~$ docker inspect api-1 --format '{{json .HostConfig.Ulimits}}'
[{"Name":"nofile","Soft":1024,"Hard":1048576}]

Significado: Docker está fijando explícitamente el límite suave a 1024 para este contenedor.

Decisión: Arregla tu archivo Compose o flags de ejecución; no toques sysctls del kernel por un problema de límite suave por contenedor.

Task 12: Medir la tasa de crecimiento de FDs (fuga vs carga)

cr0x@server:~$ for i in 1 2 3 4 5; do docker exec api-1 sh -lc 'ls /proc/7/fd | wc -l'; sleep 5; done
812
845
880
914
950

Significado: El conteo de FDs aumenta de forma sostenida en 25 segundos. Eso no es “varianza normal”. Podría ser una subida de tráfico, pero huele a fuga o concurrencia descontrolada.

Decisión: Aumenta límites para estabilidad inmediata, pero abre un bug e instrumenta el uso de FDs. También limita la concurrencia hasta entender la pendiente.

Task 13: Encontrar los peores consumidores de FDs en el host

cr0x@server:~$ for p in /proc/[0-9]*; do pid=${p#/proc/}; if [ -r "$p/fd" ]; then c=$(ls "$p/fd" 2>/dev/null | wc -l); echo "$c $pid"; fi; done | sort -nr | head
21012 3351
16840 2980
9021  1240

Significado: PID 3351 y 2980 tienen decenas de miles de FDs. PID 1240 es dockerd con ~9k, lo cual puede ser normal en hosts ocupados.

Decisión: Mapea los PIDs principales a servicios. Si un proceso no relacionado consume FDs, podrías dirigirte a un problema a nivel de host.

Task 14: Mapear un PID a una unidad systemd (lado host)

cr0x@server:~$ ps -p 3351 -o pid,comm,args
PID COMMAND  COMMAND
3351 nginx    nginx: worker process

Significado: Es nginx. Ahora la pregunta es: ¿por qué nginx mantiene 21k FDs? Probablemente demasiados keepalives, muchas conexiones upstream, o una fuga por mala configuración.

Decisión: Podrías necesitar ajustar worker_connections de nginx o keepalive upstream, no solo subir ulimit.

Elevar límites correctamente: systemd + Docker + contenedor

Hay dos partes para hacerlo bien:

  1. Fija valores por defecto sensatos para el servicio Docker (para que tu plataforma no sea frágil).
  2. Establece ulimits explícitos para los contenedores que los necesitan (para que el comportamiento sea repetible y revisable).

Step 1: Fijar LimitNOFILE del servicio Docker con un drop-in de systemd

Edita usando el mecanismo soportado por systemd. No forkees los archivos de unidad del proveedor a menos que disfrutes conflictos en actualizaciones de paquetes.

cr0x@server:~$ sudo systemctl edit docker

En el editor, añade un drop-in como este:

cr0x@server:~$ cat /etc/systemd/system/docker.service.d/override.conf
[Service]
LimitNOFILE=1048576

Luego recarga systemd y reinicia Docker (sí, reinicia—los límites se aplican en el momento del exec):

cr0x@server:~$ sudo systemctl daemon-reload
cr0x@server:~$ sudo systemctl restart docker

Contra qué te estás protegiendo: un valor por defecto de la distro que sea 1024/4096, o una imagen de nodo futura que silenciosamente restablezca los límites del demonio. Esto es un cinturón de seguridad a nivel de plataforma.

Step 2: Asegurarse de que el kernel no sea el verdadero techo

La mayor parte del tiempo, fs.nr_open ya es lo suficientemente alto. Aun así, compruébalo una vez e incorpóralo a tu baseline si tus cargas usan muchos FDs.

cr0x@server:~$ sysctl fs.nr_open
fs.nr_open = 1048576

Si debes aumentarlo (raro), hazlo explícita y persistentemente:

cr0x@server:~$ sudo tee /etc/sysctl.d/99-fd-limits.conf >/dev/null <<'EOF'
fs.nr_open = 1048576
fs.file-max = 2097152
EOF
cr0x@server:~$ sudo sysctl --system
* Applying /etc/sysctl.d/99-fd-limits.conf ...
fs.nr_open = 1048576
fs.file-max = 2097152

Consejo con criterio: no subas fs.file-max hasta la luna “por si acaso.” No es gratis. Decide según la concurrencia real y el uso de FDs, luego añade margen.

Step 3: Establecer ulimits del contenedor explícitamente (Docker run)

Para pruebas puntuales o mitigación de emergencia:

cr0x@server:~$ docker run --rm -it --ulimit nofile=65536:65536 alpine sh -lc 'ulimit -n; ulimit -Hn'
65536
65536

Significado: El contenedor arranca con un límite suave/duro más alto.

Decisión: Si esto resuelve el error inmediato, mueve la configuración a Compose/Kubernetes para que no sea una solución manual efímera.

Step 4: Establecer ulimits del contenedor explícitamente (Docker Compose)

Compose hace esto revisable. Eso es buena gobernanza y buena disponibilidad.

cr0x@server:~$ cat docker-compose.yml
services:
  api:
    image: myorg/api:latest
    ulimits:
      nofile:
        soft: 65536
        hard: 65536

Recrea el contenedor (los ulimits no cambian en caliente):

cr0x@server:~$ docker compose up -d --force-recreate api
[+] Running 1/1
 ✔ Container project-api-1  Started

Decisión: Si el contenedor recreado sigue reportando 1024, no estás desplegando lo que crees (archivo equivocado, proyecto equivocado, versión antigua de Compose, u otro orquestador está a cargo).

Step 5: Considerar los valores por defecto del daemon Docker (con cuidado)

Docker puede fijar ulimits por defecto para todos los contenedores vía la configuración del daemon. Es tentador. También es un instrumento tosco.

Úsalo cuando controles toda la flota y quieras defaults consistentes, pero mantén overrides por servicio para cargas realmente hambrientas de FDs.

cr0x@server:~$ cat /etc/docker/daemon.json
{
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 65536,
      "Soft": 65536
    }
  }
}
cr0x@server:~$ sudo systemctl restart docker
cr0x@server:~$ docker run --rm alpine sh -lc 'ulimit -n'
65536

Advertencia con criterio: los defaults a nivel de daemon pueden sorprender a workloads que eran estables con 1024 pero que se comportan mal con más concurrencia (porque ahora pueden). Elevar límites puede exponer nuevos cuellos: conexiones máximas de BD, límites upstream, presión en tablas NAT, etc.

Step 6: Validar el cambio donde importa: el proceso

Siempre verifica contra el PID real que maneja tráfico.

cr0x@server:~$ docker exec -it api-1 sh -lc 'ps -eo pid,comm,args | head -n 5'
PID COMMAND         COMMAND
1   tini            tini -- node server.js
7   node            node server.js
cr0x@server:~$ docker exec -it api-1 sh -lc "grep -E 'Max open files' /proc/7/limits"
Max open files            65536                65536                files

Decisión: Si eso muestra el límite correcto, ya terminaste con la configuración. Ahora decide si el diseño de la aplicación necesita remediación.

Step 7: No olvides que la app puede fijar sus propios límites

Algunos runtimes y wrappers de servicio llaman a setrlimit() al arrancar. Eso puede bajar tu límite efectivo incluso si Docker/systemd son generosos.

Si el límite del contenedor es alto pero el límite del proceso es bajo, comprueba:

  • Scripts de entrypoint que ejecuten ulimit -n a un valor pequeño
  • Configuraciones del runtime de lenguaje (por ejemplo, JVM, patrones master/worker de nginx, gestores de procesos)
  • Defaults de la imagen base

Broma #2: “Lo pusimos en tres sitios” es la versión operativa de “lo guardé en mi escritorio”.

Tres micro-historias corporativas desde el terreno

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

La empresa ejecutaba una API de cara al cliente en una flota Docker. El servicio había sido estable durante meses. Luego se lanzó una nueva imagen de nodo—misma familia de OS, versión menor más nueva. Dos días después, la API empezó a fallar bajo carga con 500 intermitentes. Los logs mostraban EMFILE dentro de contenedores. La reacción inmediata fue incredulidad: “Ya pusimos nofile del host a 1,048,576. No puede ser límites”.

La suposición equivocada fue sutil y común: pensar que cambiar /etc/security/limits.conf en el host afecta a los contenedores Docker. Afecta a las sesiones de login iniciadas vía PAM. Docker lo inicia systemd. systemd no se preocupa por los límites de PAM. Tiene su propia configuración de límites.

Durante el incidente, alguien subió el ulimit del contenedor en Compose y redeployó. Ayudó—en algunos nodos. En otros no. Esa inconsistencia fue la pista: la deriva de imagen de nodo había cambiado el valor por defecto LimitNOFILE de la unidad systemd de Docker, y solo algunos hosts seguían con el valor anterior.

La solución fue aburrida y permanente: drop-in de systemd para docker.service fijando LimitNOFILE, más ulimits por servicio explícitos en Compose. También añadieron una verificación post-provisión que marca el nodo como fallido si systemctl show docker -p LimitNOFILE no es lo esperado.

El resultado duradero no fue “un número más grande”. Fue un entendimiento compartido: los contenedores no flotan sobre el host; lo heredan. Las suposiciones son baratas. Los incidentes no lo son.

Mini-historia 2: la optimización que se volvió en contra

Otra organización ejecutaba una tubería de ingestión de alto rendimiento: capa web → cola → procesadores → almacenamiento de objetos. Afinaron para eficiencia, siempre persiguiendo la siguiente mejora de latencia. Alguien notó churn de conexiones TCP entre procesadores y la cola y decidió mantener conexiones vivas por más tiempo y aumentar la concurrencia de workers.

Se veía bien en pruebas de staging: menos handshakes, mejor rendimiento, CPU más estable. Lo desplegaron. La semana siguiente, producción tuvo brownouts en rollo. No caídas totales—peor. Un porcentaje de peticiones fallaba; los reintentos amplificaban la carga; la cola se retrasaba; el cliente de almacenamiento empezaba a hacer timeouts. Los logs estaban llenos de “too many open files”.

Subieron ulimits. Redujo la tasa de errores, pero el sistema siguió inestable. Ese es el revés: límites de FD más altos permitieron al servicio mantener aún más conexiones inactivas y semi-atrapadas abiertas, lo que aumentó el uso de memoria y la presión sobre límites downstream (máx conexiones DB y seguimiento del balanceador). El incidente no fue “alcanzamos 1024”. Fue “diseñamos una forma de concurrencia frágil”.

La solución fue multi-capa: aumento moderado de ulimit a una base sensata, tope en concurrencia, keepalive más cortos para ciertos upstreams y mejor comportamiento de backpressure. Después de eso, el conteo de FDs seguía alto en picos, pero quedó acotado y predecible.

La lección: elevar límites de FD no es una optimización; es capacidad. Si no gestionas concurrencia y backpressure, fallarás luego y más fuerte.

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

Otro equipo ejecutaba una flota mixta: algo de metal bare, algo virtual, mucho Docker, muchos traspasos entre equipos. Habían sido quemados por problemas de “funciona en mi nodo”, así que construyeron una pequeña lista de verificación en el bootstrap del nodo: verificar sysctls del kernel, verificar límites de systemd y verificar defaults del demonio Docker. Nada elegante. Sin dashboards brillantes. Solo guardarraíles.

Una tarde, un nuevo release de aplicación empezó a disparar EMFILE en una región. Los ingenieros pensaron en una regresión de código. Pero el on-call siguió la checklist: dentro del contenedor, ulimit -n era 1024. Eso ya era sospechoso. En el host, systemctl show docker -p LimitNOFILE informaba un número bajo en un subconjunto de nodos.

Resultó que esos nodos fueron provisionados por una pipeline paralela creada para un pico temporal de capacidad. Le faltó un paso del bootstrap. La aplicación no se había roto de repente; estaba aterrizando en hosts con límites distintos.

Puesto que el equipo tenía una baseline conocida y comprobaciones rápidas, la respuesta al incidente fue limpia: cordonear/evacuar de los nodos malos, parchear el bootstrap y reintroducir capacidad gradualmente. Sin caza de brujas, sin edits frenéticos en producción, sin heroicos “lo arreglé tecleando rápido”.

Las prácticas aburridas no se promocionan en presentaciones. Mantienen a los clientes online.

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

1) Síntoma: “Aumenté /etc/security/limits.conf pero los contenedores aún muestran 1024”

Causa raíz: Docker es iniciado por systemd; los límites de PAM no se aplican a servicios systemd.

Solución: Fija LimitNOFILE en un drop-in de systemd para docker.service, reinicia Docker y luego recrea contenedores.

2) Síntoma: ulimit -n del contenedor es alto, pero la app aún lanza EMFILE

Causa raíz: Comprobaste la shell, no el proceso. El worker real tiene un límite menor, o la app redujo el límite en tiempo de ejecución.

Solución: Inspecciona /proc/<pid>/limits del proceso worker. Audita scripts de entrypoint y gestores de procesos.

3) Síntoma: subir ulimit no ayudó; múltiples servicios no pueden abrir archivos/sockets

Causa raíz: Agotamiento de la tabla de archivos a nivel de host (ENFILE) u otro límite del kernel (puertos efímeros, conntrack, memoria).

Solución: Revisa /proc/sys/fs/file-nr, identifica los mayores consumidores de FDs y aumenta fs.file-max solo con evidencia. También inspecciona tablas de red si los síntomas incluyen fallos de connect().

4) Síntoma: el ulimit se aplica tras el redeploy, luego se reinicia o rota el nodo y vuelve a lo anterior

Causa raíz: Lo arreglaste manualmente en un nodo pero no lo persististe en gestión de configuración, drop-in de systemd o manifest de Compose/Kubernetes.

Solución: Compromete el cambio en infraestructura como código y en los manifiestos de despliegue; añade una verificación en bootstrap.

5) Síntoma: después de subir límites, la memoria sube y la latencia empeora

Causa raíz: Permitir más FDs permite más concurrencia; tu app ahora mantiene más sockets y buffers abiertos, aumentando memoria y carga downstream.

Solución: Ajusta concurrencia, pools, keepalive y backpressure. Fija límites basados en mediciones de estado estable más margen, no en máximos teóricos.

6) Síntoma: no puedo subir por encima de un número específico ni siquiera como root

Causa raíz: Has alcanzado fs.nr_open o el runtime del contenedor rehúsa valores mayores.

Solución: Comprueba sysctl fs.nr_open. Auméntalo si está justificado. Valida con /proc/<pid>/limits.

7) Síntoma: solo un contenedor alcanza EMFILE, pero el host tiene margen

Causa raíz: Límite específico del contenedor es bajo, a menudo heredado de defaults del daemon o explícitamente fijado a 1024.

Solución: Fija ulimits por servicio (Compose ulimits o --ulimit) y recrea el contenedor.

Listas de verificación / plan paso a paso

Checklist A: detener el incidente (15–30 minutos)

  1. Confirmar tipo de error en logs: EMFILE vs ENFILE.
  2. Comprobar límite del proceso vía /proc/<pid>/limits dentro del contenedor.
  3. Contar FDs abiertos del proceso worker. Si está cerca del límite, has encontrado la restricción inmediata.
  4. Aumentar ulimit del contenedor (flags de Compose/run) a un valor razonable (a menudo 32k–128k según la carga).
  5. Recrear el contenedor para que apliquen los nuevos límites.
  6. Verificar de nuevo contra el PID del worker.
  7. Aplicar un throttle temporal (reducir workers, concurrencia, keepalive) si el crecimiento de FDs es incontrolable.

Checklist B: fijarlo para que perdure (mismo día)

  1. Fijar LimitNOFILE en systemd para docker.service vía drop-in.
  2. Verificar el límite de dockerd desde /proc/<dockerd-pid>/limits.
  3. Definir ulimits por servicio en Compose para que el comportamiento sea portable.
  4. Registrar métricas base: conteo típico de FDs en carga estable y en pico.
  5. Añadir un health check (incluso un script simple) para alertar cuando el uso de FDs se acerque al 70–80% del límite.

Checklist C: prevenir recurrencias (siguiente sprint)

  1. Investigar fugas: ¿el conteo de FDs aumenta monótonamente bajo carga constante?
  2. Auditar pools de conexión: clientes de BD, keepalive HTTP, consumidores de colas.
  3. Revisar logging: evitar abrir archivos por petición; preferir stdout/stderr y agregación en contenedores.
  4. Pruebas de carga con visibilidad de FDs: monitoriza el conteo de FDs como señal de primera clase, no como un pensamiento posterior.
  5. Codificar la baseline de nodo: sysctls + límites systemd en tu pipeline de aprovisionamiento.

Preguntas frecuentes

1) ¿Por qué mi contenedor muestra ulimit -n como 1024?

Porque 1024 es un límite suave por defecto común. Docker puede estar pasándolo explícitamente, o tu imagen/entrypoint lo establece. Verifica con docker inspect ...HostConfig.Ulimits y con /proc/<pid>/limits.

2) Si fijo LimitNOFILE para Docker, ¿sigo necesitando ulimits por contenedor?

Sí, para servicios importantes. El límite del daemon es un baseline de plataforma; los ulimits por contenedor definen el comportamiento de la app. Límites por servicio explícitos previenen deriva entre hosts e imágenes futuras.

3) ¿Subir límites de FD daña el host?

No directamente, pero permite que los procesos mantengan más objetos del kernel y más memoria. Si la app usa esa capacidad, puedes aumentar uso de RAM y carga downstream. La capacidad es una herramienta, no una virtud en sí.

4) ¿Cuál es un límite “razonable” de nofile para contenedores en producción?

Depende. Muchos servicios web van bien con 32k–128k. Proxies de alto fan-out, brokers o nginx muy ocupados pueden necesitar más. Mide uso en estado estable y en pico, y añade margen.

5) Subí límites pero aún obtengo “too many open files” en búsquedas DNS o handshakes TLS. ¿Por qué?

Esas operaciones abren sockets y a veces archivos temporales. Si tu worker está en su límite de FDs, todo lo que requiera un nuevo FD falla en lugares extraños. Confirma el conteo y límite del worker; no persigas el sitio del síntoma.

6) ¿Los pods de Kubernetes se comportan diferente?

Los principios son los mismos: recursos del kernel + límites por proceso. Los knobs de configuración cambian (runtime, ajustes del kubelet, contexto de seguridad del pod y cómo el runtime aplica rlimits). Aun así: verifica en /proc para el proceso real.

7) ¿Por qué mi cambio no se aplicó hasta que reinicié cosas?

Porque los límites se aplican al arrancar un proceso (tiempo de exec). Los cambios en systemd requieren reiniciar el servicio. Los cambios de ulimit en contenedores requieren recrear el contenedor. Los procesos en ejecución mantienen sus límites actuales.

8) ¿Cómo distinguir fuga de simple tráfico?

Muestra el conteo de FDs a lo largo del tiempo con una carga relativamente constante. Una fuga tiende a crecer y no volver a bajar. El uso por tráfico sube y baja con la concurrencia. El bucle del Task 12 es una prueba rápida; un buen monitoreo es mejor.

9) ¿Puedo poner todo a 1,048,576 y listo?

Puedes. También facilitarás que un bug consuma muchos más recursos antes de fallar y trasladarás cuellos a lugares menos obvios. Elige un límite acorde a tu arquitectura y monitorea.

10) ¿“too many open files” puede ser causado por problemas de almacenamiento?

Indirectamente. Discos lentos y I/O atascado pueden hacer que descriptores permanezcan abiertos más tiempo, aumentando el uso concurrente de FDs. Pero el error sigue siendo de límite/capacidad—soluciona el límite y el comportamiento de I/O.

Conclusión: pasos siguientes que realmente perduran

Cuando Docker lanza “too many open files”, no lo trates como una rareza de Docker. Es Linux haciendo exactamente lo que le dijiste: aplicar límites de recursos. Tu trabajo es averiguar qué límite, en qué capa, y si la carga merece más capacidad o menos caos.

Haz esto a continuación, en orden:

  1. Verifica el límite real del proceso worker en /proc/<pid>/limits y su uso actual de FDs.
  2. Define ulimits explícitos para el contenedor del servicio (Compose/flags de run) y luego recrea el contenedor.
  3. Crea un drop-in systemd para docker.service con LimitNOFILE para que la plataforma sea consistente tras reinicios y rotaciones de nodo.
  4. Mide el comportamiento de FDs bajo carga y decide si estás resolviendo capacidad o escondiendo una fuga.

Si haces esas cuatro cosas, esto deja de ser un incidente recurrente y se convierte en un detalle operativo resuelto. Y eso es lo que quieres: menos sorpresas, menos heroicidades y un on-call más tranquilo.

← Anterior
Solucionar Proxmox “IOMMU no habilitado” para PCI Passthrough (VT-d/AMD-Vi) de forma segura
Siguiente →
Prioridad de Resilver en ZFS: Reconstruir Rápido Sin Colapsar IO de Producción

Deja un comentario