Docker en cgroups v2: dolor, errores y la senda de solución

¿Te fue útil?

Actualizas una distribución. Docker antes funcionaba. Ahora no. O peor: Docker arranca, pero los límites de recursos no se aplican, las CPUs se comportan raro y tu contenedor “memoria limitada a 2G” devora el host. Miras docker run como si te hubiera traicionado personalmente.

cgroups v2 es mejor ingeniería. También supone una tasa de compatibilidad. Esta es la guía de campo para los errores exactos que verás, qué significan realmente y una ruta de solución que no implica rezar frente a /sys/fs/cgroup.

Qué cambió con cgroups v2 (y por qué Docker lo siente)

Los grupos de control (“cgroups”) son el marco del kernel para contabilidad y limitación: CPU, memoria, IO, pids y compañía. Docker los usa para implementar flags como --memory, --cpus, --pids-limit y para mantener los procesos del contenedor en un árbol ordenado.

cgroups v1 y v2 no son solo versiones distintas. Son modelos distintos.

cgroups v1: muchas jerarquías, muchas trampas

v1 permite que cada controlador (cpu, memory, blkio, etc.) se monte por separado. Esa flexibilidad creó un pasatiempo para las distribuciones Linux: montar controladores en lugares distintos y luego ver cómo las herramientas asumen cosas. Docker creció aquí. También muchos scripts mediofuncionales.

cgroups v2: una jerarquía, reglas coherentes, semántica distinta

v2 es “jerarquía unificada”: un solo árbol, controladores habilitados por subárbol, y reglas sobre delegación. Es más coherente. También significa que el software debe ser explícito sobre cómo crea y gestiona cgroups, y si systemd está a cargo.

Dónde tropieza Docker

  • Desajuste de driver: Docker puede gestionar cgroups por sí mismo (driver cgroupfs) o delegar a systemd (driver systemd). Con v2, systemd-como-gestor es la vía recomendada en la mayoría de distros con systemd.
  • runc/containerd antiguos: el soporte inicial para v2 fue parcial; versiones antiguas explotan con “unsupported” o silenciosamente ignoran límites.
  • Restricciones en rootless: la delegación en v2 es más estricta. Docker rootless puede funcionar, pero depende de servicios de usuario systemd y una delegación correcta.
  • Modos híbridos: algunos sistemas ejecutan una configuración “mixta” v1/v2. Eso es una receta para “funciona en este host pero no en aquel”.

Idea parafraseada de John Allspaw (ingeniería de confiabilidad): “Echar la culpa rara vez arregla incidentes; entender los sistemas sí”. Esa es la postura que necesitas con cgroups v2: deja de pelear con el síntoma, mapea el sistema.

Hechos y breve historia que te ahorrarán tiempo

Estas son las verdades pequeñas y concretas que evitan que depures la capa equivocada.

  1. Los cgroups se integraron al kernel en 2007 (bajo el esfuerzo de “process containers”). El modelo original asumía muchas jerarquías independientes.
  2. cgroups v2 empezó a aterrizar alrededor de 2016 para corregir la fragmentación de v1 y errores semánticos sutiles, especialmente en memoria y delegación.
  3. systemd adoptó profundamente los cgroups y se volvió el gestor por defecto en muchas distribuciones; Docker inicialmente resistió, luego convergió hacia el driver systemd como la opción sensata para setups modernos.
  4. v2 cambia el comportamiento de memoria: la contabilidad y aplicación de memoria es más limpia, pero debes entender memory.max, memory.high y señales de presión. Hábitos antiguos como “oom-kill como control de flujo” quedan expuestos.
  5. El control de IO cambió nombres y significado: blkio.* de v1 pasa a io.* en v2, y algunos consejos de afinamiento antiguos simplemente ya no aplican.
  6. v2 requiere habilitación explícita de controladores: puedes tener un árbol de cgroups donde los controladores existen pero no están habilitados para un subárbol, produciendo un comportamiento de “archivo no encontrado” que parece un problema de permisos.
  7. La delegación es intencionalmente estricta: v2 impide que procesos no privilegiados creen subárboles arbitrarios a menos que el padre esté configurado para delegación. Por eso los setups rootless fallan de formas nuevas.
  8. Algunas banderas de Docker dependen de la configuración del kernel: aun con v2, la ausencia de ciertas características del kernel produce errores confusos de “no soportado” que parecen bugs de Docker.

Los errores que verás en la vida real

Los fallos con cgroups v2 se agrupan en tres bloques: inicio del demonio, inicio del contenedor y “arranca pero los límites mienten”. Aquí los clásicos, con lo que suelen significar.

El demonio no arranca

  • failed to mount cgroup o cgroup mountpoint does not exist: Docker espera el layout v1 pero el host está en v2 unificado, o los montajes faltan/están bloqueados.
  • cgroup2: unknown option al montar: kernel demasiado antiguo o opciones de montaje incorrectas para ese kernel.
  • OCI runtime create failed: ... cgroup ... durante la inicialización del demonio: incompatibilidad entre runc/containerd y el modo de cgroup del host.

El contenedor no arranca (errores OCI)

  • OCI runtime create failed: unable to apply cgroup configuration: ... no such file or directory: archivos controladores no disponibles en el cgroup objetivo, a menudo porque los controladores no están habilitados en ese subárbol.
  • permission denied al escribir en /sys/fs/cgroup/...: problema de delegación (rootless o gestor de cgroups anidado) o systemd posee el subárbol y Docker intenta usar cgroupfs.
  • cannot set memory limit: ... invalid argument: usando knobs de la era v1 o un kernel que no soporta una determinada opción; también ocurre cuando los límites de swap están mal configurados.

Los contenedores arrancan pero los límites no se aplican

  • docker stats muestra memoria ilimitada: Docker no puede leer el límite de memoria desde la ruta cgroup v2 que espera; desajuste de driver o Docker antiguo.
  • Cotas de CPU ignoradas: el controlador cpu no está habilitado para ese subárbol, o estás usando una configuración de drivers que nunca adjunta tareas donde piensas que lo hace.
  • El throttling de IO no funciona: estás en v2 y aun intentas afinar blkio; o el controlador del dispositivo de bloque no soporta la política solicitada.

Broma #1: cgroups v2 es como la adultez: más estructura, menos resquicios y todo lo que antes hacías “porque funcionaba” ahora es ilegal.

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

Si solo tienes cinco minutos antes de que termine la ventana de cambios, haz esto en orden.

Primero: confirma en qué modo de cgroup está realmente el host

No infieras por la versión de la distro. Revisa el sistema de archivos y los flags del kernel.

  • ¿Es /sys/fs/cgroup tipo cgroup2 (unificado) o múltiples montajes v1?
  • ¿Está el sistema en modo híbrido?

Segundo: comprueba el driver de cgroup de Docker y las versiones del runtime

La mayoría del “dolor cgroups v2” es o driver equivocado o runtime antiguo.

  • ¿Docker usa systemd o cgroupfs?
  • ¿son containerd y runc suficientemente nuevos para tu kernel/distro?

Tercero: verifica disponibilidad de controladores y delegación

Si falla un límite específico (memoria, cpu, pids), confirma que el controlador esté habilitado donde Docker coloca contenedores.

  • cgroup.controllers existe y lista el controlador.
  • cgroup.subtree_control lo incluye para el cgroup padre.
  • Permisos y propiedad tienen sentido (especialmente en rootless).

Tareas prácticas: comandos, significado de la salida y decisiones

Estas son las tareas de “deja de adivinar”. Cada una tiene: comando, qué significa la salida y qué decisión tomar después.

Tarea 1: Identificar tipo de sistema de archivos cgroup (v2 vs v1)

cr0x@server:~$ stat -fc %T /sys/fs/cgroup
cgroup2fs

Significado: cgroup2fs significa cgroups v2 unificado. Si ves tmpfs aquí y montajes separados para controladores, probablemente estás en v1/híbrido.

Decisión: Si es v2, planifica Docker + driver systemd (o al menos verifica compatibilidad). Si es v1/híbrido, decide si migrar o forzar modo legacy para consistencia.

Tarea 2: Confirmar qué está montado bajo /sys/fs/cgroup

cr0x@server:~$ mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

Significado: Un único montaje cgroup2: modo unificado. Si ves muchas líneas como cgroup on /sys/fs/cgroup/memory, eso es v1.

Decisión: Si es unificado, deja de intentar depurar rutas v1 como /sys/fs/cgroup/memory. No existirán.

Tarea 3: Comprobar parámetros de arranque del kernel que afectan cgroups

cr0x@server:~$ cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-6.5.0 root=/dev/mapper/vg0-root ro quiet systemd.unified_cgroup_hierarchy=1

Significado: systemd.unified_cgroup_hierarchy=1 fuerza v2 unificado. Algunos sistemas usan lo contrario (o valores por defecto de la distro) para forzar v1.

Decisión: Si tu organización quiere comportamiento predecible, estandariza este flag en la flota (o v2 en todas partes, o v1 en todas partes). Flotas mixtas son donde la on-call va a morir de alegría.

Tarea 4: Preguntar a systemd qué piensa sobre cgroups

cr0x@server:~$ systemd-analyze --version
systemd 253 (253.5-1)
+PAM +AUDIT +SELINUX +APPARMOR +IMA +SMACK +SECCOMP +GCRYPT -GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS +FIDO2 +IDN2 -IDN +IPTC +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 -PWQUALITY +P11KIT +QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD -BPF_FRAMEWORK -XKBCOMMON +UTMP +SYSVINIT default-hierarchy=unified

Significado: default-hierarchy=unified significa que systemd espera cgroups v2.

Decisión: Si systemd es unificado, alinea Docker con el driver systemd a menos que tengas una razón contundente para no hacerlo.

Tarea 5: Inspeccionar el driver de cgroup y la versión de cgroup que ve Docker

cr0x@server:~$ docker info --format '{{json .CgroupVersion}} {{json .CgroupDriver}}'
"2" "systemd"

Significado: Docker ve cgroups v2 y usa el driver systemd. Es la combinación estable en distros modernas con systemd.

Decisión: Si obtienes "2" "cgroupfs" en un host con systemd, considera cambiar al driver systemd para evitar sorpresas de delegación y subtree-control.

Tarea 6: Comprobar versiones de componentes runtime (containerd, runc)

cr0x@server:~$ docker info | egrep -i 'containerd|runc|cgroup'
 Cgroup Driver: systemd
 Cgroup Version: 2
 containerd version: 1.7.2
 runc version: 1.1.7

Significado: Combinaciones antiguas containerd/runc son donde el soporte v2 se vuelve “creativo”. Las versiones modernas son menos excitantes.

Decisión: Si estás en un paquete Docker Engine antiguo fijado por “estabilidad”, desafíalo—porque ahora es inestable. Actualiza el conjunto engine/runc/containerd como unidad cuando sea posible.

Tarea 7: Validar que los controladores existan (a nivel host)

cr0x@server:~$ cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory hugetlb pids rdma misc

Significado: Estos son los controladores soportados por el kernel y expuestos en la jerarquía unificada.

Decisión: Si falta un controlador que necesitas (como memory o io), no es un “problema de configuración de Docker”. Es un problema de kernel/características.

Tarea 8: Verificar que los controladores estén habilitados para el subárbol que usará Docker

cr0x@server:~$ cat /sys/fs/cgroup/cgroup.subtree_control
+cpu +io +memory +pids

Significado: La lista con prefijo más indica qué controladores están habilitados para cgroups hijos en ese nivel. Si +memory no está aquí, los cgroups hijos no tendrán memory.max y compañeros.

Decisión: Si los controladores no están habilitados, o arreglas la configuración del cgroup padre (a menudo vía settings de unidad systemd) o mueves la colocación de Docker a un subárbol correctamente delegado (el driver systemd ayuda).

Tarea 9: Verificar dónde coloca Docker un contenedor en el árbol cgroup

cr0x@server:~$ docker run -d --name cgtest --memory 256m --cpus 0.5 busybox:latest sleep 100000
b7d32b3f6b1f3c3b2c0b9c9f8a7a6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9

cr0x@server:~$ docker inspect --format '{{.State.Pid}}' cgtest
22145

cr0x@server:~$ cat /proc/22145/cgroup
0::/system.slice/docker-b7d32b3f6b1f3c3b2c0b9c9f8a7a6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9.scope

Significado: En v2 hay una única entrada unificada (0::). El contenedor está en una unidad scope de systemd bajo system.slice, que es lo deseable cuando se usa el driver systemd.

Decisión: Si el contenedor cae en un lugar inesperado (o en un cgroup creado por Docker fuera del árbol de systemd), reconcilia el driver de cgroup de Docker y la configuración de systemd.

Tarea 10: Confirmar que el límite de memoria se aplica realmente (archivos v2)

cr0x@server:~$ CGPATH=/sys/fs/cgroup/system.slice/docker-b7d32b3f6b1f3c3b2c0b9c9f8a7a6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9.scope
cr0x@server:~$ cat $CGPATH/memory.max
268435456

cr0x@server:~$ cat $CGPATH/memory.current
1904640

Significado: memory.max está en bytes. 268435456 es 256 MiB. memory.current muestra el uso actual.

Decisión: Si memory.max es max (ilimitado) a pesar de flags de Docker, tienes un desajuste de colocación/driver de cgroup o un bug de runtime. No afines la aplicación; arregla la tubería de cgroups.

Tarea 11: Confirmar que la cuota de CPU se aplica (semántica v2)

cr0x@server:~$ cat $CGPATH/cpu.max
50000 100000

Significado: En v2, cpu.max es “cuota y periodo”. Aquí: cuota 50ms por periodo 100ms = 0.5 CPU.

Decisión: Si ves max 100000, la cuota no está aplicada. Verifica si el controlador cpu está habilitado y si Docker realmente respetó --cpus en tu versión.

Tarea 12: Comprobar límite de pids (un fallo común “funciona hasta que no”)

cr0x@server:~$ cat $CGPATH/pids.max
1024

cr0x@server:~$ cat $CGPATH/pids.current
3

Significado: El control pids funciona. Sin esto, fork-bombs se convierten en “pruebas de carga inesperadas”.

Decisión: Si pids.max falta, los controladores no están habilitados para el subárbol. Arregla delegación/habilitación, no los flags de Docker.

Tarea 13: Buscar la evidencia contundente en journald

cr0x@server:~$ journalctl -u docker -b --no-pager | tail -n 20
Jan 03 08:12:11 server dockerd[1190]: time="2026-01-03T08:12:11.332111223Z" level=error msg="failed to create shim task" error="OCI runtime create failed: unable to apply cgroup configuration: mkdir /sys/fs/cgroup/system.slice/docker.service/docker/xyz: permission denied: unknown"
Jan 03 08:12:11 server dockerd[1190]: time="2026-01-03T08:12:11.332188001Z" level=error msg="failed to start daemon" error="... permission denied ..."

Significado: Docker intenta crear cgroups en un lugar que systemd no permite (o el proceso carece de derechos de delegación). La ruta es la pista.

Decisión: Cambia a driver systemd, o corrige las reglas de delegación si es rootless/anidado, en lugar de hacer chmod en archivos aleatorios de sysfs (eso no es una solución, es una confesión).

Tarea 14: Confirmar que systemd es el gestor de cgroup para Docker

cr0x@server:~$ systemctl show docker --property=Delegate,Slice,ControlGroup
Delegate=yes
Slice=system.slice
ControlGroup=/system.slice/docker.service

Significado: Delegate=yes es crucial: le dice a systemd permitir que el servicio cree/gestione sub-cgroups. Sin ello, la delegación en cgroups v2 falla de formas que parecen errores de permisos aleatorios.

Decisión: Si Delegate=no, arregla el drop-in de la unidad (o usa la unidad empaquetada que ya lo establece correctamente). A menudo este es todo el problema.

Tarea 15: Detectar modo rootless (reglas distintas, dolores distintos)

cr0x@server:~$ docker info --format 'rootless={{.SecurityOptions}}'
rootless=[name=seccomp,profile=default name=rootless]

Significado: Rootless cambia qué cgroups puedes tocar y cómo debe configurarse la delegación en sesiones de usuario.

Decisión: Si rootless está habilitado y las escrituras en cgroup fallan, deja de intentar “arreglar” en /sys/fs/cgroup como root. Configura servicios de usuario systemd y delegación correctamente, o acepta límites funcionales.

La senda de solución (elige tu ruta, no adivines)

Solo hay unos pocos estados finales estables. Elige uno deliberadamente. “Funciona en mi portátil” no es una arquitectura.

Ruta A (recomendada en distros con systemd): cgroups v2 + driver systemd de Docker

Esta es la opción más limpia en Ubuntu/Debian/Fedora/RHEL modernos donde systemd es PID 1 y la jerarquía unificada es por defecto.

1) Asegurar que Docker esté configurado para el driver systemd

Revisa el estado actual primero (Tarea 5). Si no es systemd, establécelo.

cr0x@server:~$ sudo mkdir -p /etc/docker
cr0x@server:~$ sudo tee /etc/docker/daemon.json >/dev/null <<'EOF'
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "journald"
}
EOF

Qué significa: Esto indica a dockerd crear contenedores bajo cgroups gestionados por systemd en lugar de gestionar su propia jerarquía cgroupfs.

Decisión: Si usas Kubernetes, asegúrate de que kubelet use el mismo driver. El desajuste es un clásico modo de fallo “el nodo parece bien hasta que no”.

2) Reiniciar Docker y verificar

cr0x@server:~$ sudo systemctl daemon-reload
cr0x@server:~$ sudo systemctl restart docker
cr0x@server:~$ docker info | egrep -i 'Cgroup Driver|Cgroup Version'
 Cgroup Driver: systemd
 Cgroup Version: 2

Qué significa: Estás alineado: systemd + v2 + driver Docker.

Decisión: Si Docker falla al iniciar, revisa inmediatamente journald (Tarea 13). No “reinicies a ciegas”. Los reinicios convierten un problema reproducible en folclore.

3) Confirmar que la delegación es correcta en la unidad docker.service

La mayoría de las unidades empaquetadas son correctas. Las personalizadas a menudo no lo son.

cr0x@server:~$ systemctl show docker --property=Delegate
Delegate=yes

Decisión: Si Delegate=no, añade un drop-in:

cr0x@server:~$ sudo systemctl edit docker <<'EOF'
[Service]
Delegate=yes
EOF

Ruta B: forzar cgroups v1 (modo legacy) para ganar tiempo

Esta es una retirada táctica. Puede ser apropiada cuando tienes agentes de terceros, kernels antiguos o appliances de proveedor que aún no soportan v2. Pero trátalo como deuda con interés.

1) Cambiar systemd a jerarquía legacy vía línea de boot del kernel

El mecanismo exacto depende de la distro/bootloader. El principio: establecer un parámetro del kernel para desactivar la jerarquía unificada.

cr0x@server:~$ cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-6.5.0 root=/dev/mapper/vg0-root ro quiet systemd.unified_cgroup_hierarchy=0

Significado: Estás forzando comportamiento v1/híbrido.

Decisión: Si haces esto, documenta y conviértelo en estándar de flota. Una flota medio-v2 es como tener “solo esa AZ rota”.

Ruta C: Docker rootless en cgroups v2 (funciona, pero no lo romantices)

Rootless es excelente para máquinas de desarrollador y algunos escenarios multi-tenant. En producción está bien si aceptas las limitaciones y las pruebas. cgroups v2 hace rootless más coherente, pero solo con delegación de usuario systemd correcta.

Reglas típicas:

  • Usa servicios de usuario systemd cuando sea posible.
  • Espera que algunos controles de recursos se comporten distinto que en modo rootful.
  • Acepta que la postura de seguridad y la operabilidad intercambian prioridades.

Broma #2: Docker rootless con cgroups v2 es totalmente viable, como hacer un espresso mientras vas en bicicleta—solo no lo intentes por primera vez durante un incidente.

Ruta D: usar containerd directamente (cuando la ergonomía de Docker no compensa)

Algunas organizaciones migran a containerd + nerdctl (o orquestación) para reducir capas. Si ya estás profundo en Kubernetes, el valor de Docker es mayormente UX para desarrolladores. En producción, menos capas pueden significar menos sorpresas de cgroups.

Pero la senda de solución es la misma: hacer de systemd el gestor de cgroup, mantener containerd/runc actualizados y validar la habilitación de controladores.

Tres microhistorias corporativas (dolorosamente plausibles)

Microhistoria 1: El incidente causado por una suposición errónea

La empresa migró un gran lote de cargas desde un LTS antiguo a uno más nuevo. La tarjeta de cambio decía “Actualización Docker + parches de seguridad”. La suposición fue simple: “Mismos contenedores, mismos límites”.

Tras la actualización, Docker arrancó. Los jobs arrancaron. Todo parecía verde. Entonces los hosts comenzaron a usar swap, la latencia se disparó y servicios no relacionados empezaron a hacer timeouts. El on-call hizo lo que hace: reinició contenedores, drenó nodos y culpó a “vecinos ruidosos”. Empeoró.

La suposición errónea fue que --memory se estaba aplicando. En el nuevo SO, el host estaba en cgroups v2. Docker seguía usando el driver cgroupfs porque una imagen dorada antigua tenía un daemon.json persistente de años atrás. Los contenedores se colocaron en un subárbol sin el controlador memory habilitado. El kernel no estaba ignorando los límites por mala leche; literalmente no tenía dónde aplicarlos.

La solución no fue afinar los jobs. La solución fue alinear Docker al driver systemd, verificar Delegate=yes y luego demostrar la aplicación leyendo memory.max en el cgroup scope del contenedor. La acción del postmortem no fue “monitorizar más la memoria”. Fue “estandarizar modo y driver de cgroup a nivel de flota”.

Microhistoria 2: La optimización que rebotó

Un equipo de plataforma intentó reducir el ruido de throttling de CPU “suavizando cuotas”. Pasaron muchos servicios de cuotas estrictas a shares, pensando que reduciría la contención y aumentaría el throughput. También habilitaron un nuevo conjunto de defaults del kernel al pasarse a cgroups v2 unificado.

Al principio se veía bien: menos alertas de throttling, mejor latencia mediana. Luego un servicio batch empezó a hacer ráfagas periódicas. Las ráfagas eran legales bajo scheduling por shares, pero robaron suficiente CPU para empujar a una API sensible a la latencia hacia una espiral de cola en la cola de latencia.

El modelo mental del equipo era de la era v1: “shares son suaves, cuotas son duras”. En v2, con contabilidad unificada y comportamientos distintos de controladores, la ausencia de un límite duro permitió que el job de batch domine durante ráfagas, sobre todo cuando el controlador cpu no estaba habilitado donde creían. La “optimización” movió el cuello de botella del throttling visible a la cola invisible.

La solución fue aburrida: restaurar límites explícitos cpu.max para la carga ráfaga, habilitar consistentemente el controlador cpu en el subárbol correcto y mantener shares para servicios realmente cooperativos. También añadieron un paso de validación en CI que inspecciona los archivos cgroup en ejecución tras el despliegue. “Confía pero verifica” es un cliché porque sigue siendo cierto.

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

Un servicio financiero corría en un pequeño clúster que siempre parecía sobredimensionado. Alguien preguntó por qué pagaban por recursos ociosos. La respuesta SRE fue predeciblemente irritante: “Porque nos gusta dormir”.

Tenían una práctica: por cada cambio de imagen de nodo, ejecutaban un script de conformidad corto. No una suite enorme: solo una docena de comprobaciones: modo cgroup, driver Docker, disponibilidad de controladores y un contenedor de prueba que pone límites de memoria y CPU y los demuestra leyendo memory.max y cpu.max. Tomaba menos de dos minutos.

Un día se coló una nueva imagen base con cgroups unificados habilitados pero un paquete runtime desactualizado debido a un repositorio fijado. Docker arrancó, los contenedores arrancaron, pero los límites de memoria eran inestables y a veces fallaban con invalid argument. Su script de conformidad lo detectó antes del despliegue en producción.

La solución fue tan sosa como efectiva: despinnear los paquetes runtime, actualizar el conjunto, volver a ejecutar las comprobaciones y avanzar. Sin incidente. Sin hilo de mails ejecutivos. Solo la satisfacción silenciosa de tener razón por adelantado.

Errores comunes: síntoma → causa raíz → arreglo

Esta sección es intencionalmente directa. Estos son los patrones que sigo viendo en el campo.

1) “Docker arranca, pero los límites de memoria no funcionan”

Síntoma: docker run --memory tiene éxito; el contenedor usa más que el límite; docker stats muestra ilimitado.

Causa raíz: Docker usa driver cgroupfs en un host systemd+v2, colocando contenedores en un subárbol sin +memory en cgroup.subtree_control, o un runtime antiguo que reporta mal límites v2.

Arreglo: Cambiar al driver systemd, confirmar Delegate=yes, verificar memory.max en la ruta scope del contenedor.

2) “OCI runtime create failed: permission denied” bajo /sys/fs/cgroup

Síntoma: Contenedores fallan al crear con errores de permisos en sysfs.

Causa raíz: Falta delegación para el servicio Docker, usuario rootless sin controladores delegados, o conflictos de SELinux/AppArmor que se manifiestan como fallos de escritura.

Arreglo: Asegurar que la unidad systemd tenga Delegate=yes, usar driver systemd, validar prerrequisitos rootless, revisar logs de auditoría si MAC está habilitado.

3) “No such file or directory” para archivos cgroup que deberían existir

Síntoma: Error referencia archivos como cpu.max o memory.max ausentes.

Causa raíz: Controlador no habilitado para ese subárbol. En v2, la presencia en cgroup.controllers no es lo mismo que estar habilitado para hijos.

Arreglo: Habilitar controladores en el nivel padre correcto (configuración del slice systemd o colocación correcta del subárbol). Re-check cgroup.subtree_control.

4) “Cuota de CPU ignorada”

Síntoma: --cpus puesto, pero el contenedor usa cores completos.

Causa raíz: controlador cpu no habilitado, o el contenedor quedó fuera del subárbol gestionado por desajuste de driver.

Arreglo: Validar cpu.max en el cgroup del contenedor. Si es max, arreglar habilitación de controlador y alineación de driver.

5) “Solo falla en algunos nodos”

Síntoma: Despliegue idéntico se comporta distinto en nodos diversos.

Causa raíz: Flota mixta v1/v2, drivers de cgroup Docker mezclados o versiones de runtime diversas.

Arreglo: Estandarizar. Elegir un modo de cgroup y aplicarlo vía build de imagen + flags de arranque + gestión de configuración. Luego verificar con una comprobación de conformidad.

6) “Forzamos cgroups v1 para arreglar y lo olvidamos”

Síntoma: Herramientas nuevas asumen v2; agente de seguridad asume v2; ahora hay expectativas contrapuestas.

Causa raíz: Una reversión táctica se convirtió en arquitectura permanente.

Arreglo: Rastrearlo como deuda técnica con un responsable y una fecha. Migrar deliberadamente, pool de nodos por pool de nodos.

Listas de verificación / plan paso a paso

Lista 1: Aceptación de nueva imagen de nodo (10 minutos, ahorra horas)

  1. Confirmar modo cgroup: stat -fc %T /sys/fs/cgroup debe coincidir con tu estándar de flota.
  2. Confirmar jerarquía systemd: systemd-analyze --version muestra default-hierarchy=unified (si el estándar es v2).
  3. Confirmar que Docker ve v2: docker info muestra Cgroup Version: 2.
  4. Confirmar driver Docker: Cgroup Driver: systemd (recomendado para v2+systemd).
  5. Confirmar delegación docker.service: systemctl show docker --property=Delegate sea yes.
  6. Ejecutar un contenedor de prueba con límites de CPU y memoria y leer memory.max y cpu.max desde su cgroup scope.
  7. Revisar journald por advertencias: journalctl -u docker -b.

Lista 2: Ruta de arreglo cuando Docker rompe justo después de una actualización de distro

  1. Confirmar modo real de cgroup (Tareas 1–3). No confíes en notas de lanzamiento.
  2. Comprobar driver/versión Docker (Tareas 5–6). Actualizar si está antiguo.
  3. Comprobar delegación y habilitación de controladores (Tareas 7–8, 14).
  4. Reproducir con un contenedor mínimo (Tarea 9). No depures aún tu JVM de 4GB.
  5. Confirmar que el límite se escribe (Tareas 10–12). Si no está en el archivo, no es real.
  6. Sólo entonces tocar el ajuste de la aplicación.

Lista 3: Plan de estandarización para una flota mixta de cgroups

  1. Elegir un objetivo: v2 unificado + driver systemd de Docker (más común), o v1 legacy (temporal).
  2. Definir una comprobación de conformidad (la lista de aceptación arriba) y ejecutarla por pool de nodos.
  3. Actualizar Docker/containerd/runc como bundle en cada pool.
  4. Cambiar modo cgroup vía flags de arranque solo en límites de pool (evitar mezcla dentro de un pool).
  5. Hacer canary, verificar leyendo /sys/fs/cgroup y archivos scope del contenedor.
  6. Documentar la decisión e incorporarla en pipelines de imagen para evitar deriva.

Preguntas frecuentes

1) ¿Cómo sé si estoy en cgroups v2 sin leer un post?

Ejecuta stat -fc %T /sys/fs/cgroup. Si imprime cgroup2fs, estás en v2 unificado. Si ves múltiples montajes de controladores v1, estás en v1/híbrido.

2) ¿Debería usar el driver cgroupfs de Docker en un host con systemd?

Evítalo salvo que tengas una restricción explícita. En systemd + cgroups v2, el driver systemd es la elección estable porque alinea delegación, slices/scopes y habilitación de controladores.

3) ¿Por qué docker stats muestra límites incorrectos en v2?

Usualmente desajuste de runtime (Docker/containerd/runc antiguo), o Docker está leyendo datos cgroup desde un layout de rutas que no coincide con donde viven realmente los contenedores. Verifica leyendo memory.max directamente en el cgroup scope del contenedor.

4) Mi error dice “no such file or directory” para memory.max. Pero v2 está habilitado. ¿Por qué?

Porque v2 requiere que los controladores estén habilitados para el subárbol. El archivo no existirá si el controlador de memoria no está habilitado en el padre vía cgroup.subtree_control.

5) ¿Es aceptable forzar cgroups v1 como arreglo?

Como parche a corto plazo, sí. Como estrategia a largo plazo, es una carga. Cada vez más herramientas y distros asumirán v2. Si fuerzas v1, estandarízalo y planea la migración.

6) ¿Necesito cambiar mis flags de recursos de contenedor por v2?

Usualmente no; los flags de Docker permanecen iguales. Lo que cambia es si esos flags se traducen a los archivos v2 correctos y si el kernel los permite en ese subárbol.

7) ¿Cuál es el archivo más útil para mirar al depurar problemas de recursos v2?

La ruta cgroup del contenedor desde /proc/<pid>/cgroup, luego lee los archivos relevantes: memory.max, memory.current, cpu.max, pids.max. Si el valor no está allí, el límite no existe.

8) Docker rootless: ¿debería usarlo en producción con cgroups v2?

Puede ser adecuado en casos específicos, pero no lo trates como una comida gratis. Necesitas delegación de usuario systemd adecuada, y algunos controles difieren del modo rootful. Prueba los límites exactos de los que dependes.

9) Ángulo Kubernetes: ¿qué ocurre si kubelet y Docker usan drivers de cgroup diferentes?

Ese desajuste es un bug de fiabilidad. Los procesos quedan en subárboles inesperados, la contabilidad se vuelve rara y los límites pueden no aplicarse. Alinea ambos (systemd/systemd en v2 es el emparejamiento común).

10) ¿Y los límites de IO—por qué dejó de funcionar mi tuning blkio?

Porque blkio es terminología v1. En v2 busca controles io.* y confirma que el kernel y el dispositivo soportan la política. También verifica que el controlador io esté habilitado para el subárbol.

Conclusión: próximos pasos prácticos

cgroups v2 no es “Docker roto”. Es el kernel exigiendo un contrato más limpio. El dolor viene de expectativas desalineadas: drivers, delegación y habilitación de controladores.

Haz esto a continuación:

  1. Estandariza tu flota: elige cgroups v2 unificado (preferido) o v1 legacy (temporal) y aplícalo en el arranque.
  2. En hosts v2 con systemd, configura Docker para el driver systemd y verifica Delegate=yes.
  3. Crea un pequeño script de conformidad que ejecute un contenedor de prueba y demuestre límites leyendo archivos cgroup directamente.
  4. Actualiza Docker/containerd/runc como conjunto. Los runtimes antiguos son donde nace el “funciona a veces”.

Si no te llevas otra cosa: cuando los contenedores “ignoran” límites, no discutas los flags de docker run. Lee los archivos cgroup. El kernel es la fuente de la verdad y no acepta excusas.

← Anterior
Profundidad de cola en ZFS: por qué algunos SSD rinden en ext4 y fallan en ZFS
Siguiente →
ZFS RAIDZ3: cuándo la triple paridad compensa los discos

Deja un comentario