Tutorial: configurar cgroup v2 y systemd ResourceControl para limitar CPU, memoria e I/O de contenedores Docker en Debian

Linux & Sistemas · Por · Publicado el · 9 min de lectura

Tutorial: configurar cgroup v2 y systemd ResourceControl para limitar CPU, memoria e I/O de contenedores Docker en Debian
Foto: Google DeepMind / Pexels

En este paso a paso configurarás cgroup v2 y systemd ResourceControl para limitar CPU, memoria e I/O de contenedores Docker en Debian. Crearemos slices dedicadas, asociaremos contenedores a esas slices y validaremos los cambios leyendo los archivos del cgroup v2. El resultado: límites previsibles y fáciles de mantener, sin trucos por contenedor.

Requisitos y lo que vamos a configurar

El objetivo es aplicar límites de CPU, memoria e I/O a contenedores Docker usando cgroup v2 gestionado por systemd. Estandarizaremos límites por slice (grupo de servicios) y, cuando haga falta, ajustaremos un contenedor específico.

Lo que necesitas antes de comenzar:

- Debian reciente (11 o 12) con systemd y acceso sudo.

- Docker instalado y funcionando. Si aún no lo tienes, consulta cómo instalar Docker y Docker Compose en Linux.

- Disponibilidad para un reinicio si es necesario habilitar cgroup v2.

- Conocimiento del dispositivo de bloque usado por Docker (para los límites de I/O).

Avisos importantes:

- Modificar parámetros del cargador de arranque y reiniciar puede afectar servicios en producción. Planifica una ventana de mantenimiento.

- Límites de memoria muy agresivos pueden matar procesos (OOMKill). Aplícalos de forma gradual y monitoriza.

Confirmar (y habilitar) cgroup v2 en Debian

Necesitamos cgroup v2 en modo unificado. Valida la presencia de cgroup.controllers y los controladores disponibles. Si no está activo, habilítalo pasando el parámetro al kernel y reinicia.

En Debian 12 suele venir en v2 por defecto. En Debian 11 puede ser necesario ajustar GRUB. Reiniciar es obligatorio tras modificar GRUB.

# Verificar si cgroup v2 está activo (debe listar controladores, ex.: cpu io memory)
cat /sys/fs/cgroup/cgroup.controllers

# Se o arquivo não existir ou estiver vazio, habilite cgroup v2 e reinicie
# (cuidado: ajuste de GRUB exige reboot)
sudo sed -i 's/^GRUB_CMDLINE_LINUX="\([^"]*\)"/GRUB_CMDLINE_LINUX="\1 systemd.unified_cgroup_hierarchy=1"/' /etc/default/grub
sudo update-grub
sudo reboot

Configurar Docker para usar el driver de cgroup systemd

Para que Docker delegue correctamente los cgroups a systemd, usa el driver de cgroup "systemd". También aseguramos la directiva Delegate del servicio docker. Tras el cambio, recarga systemd y reinicia Docker.

# Definir driver de cgroup = systemd
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json >/dev/null <<'JSON'
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {"max-size": "10m", "max-file": "3"}
}
JSON

# (Opcional, a maioria das instalações já possui Delegate=yes)
sudo mkdir -p /etc/systemd/system/docker.service.d
sudo tee /etc/systemd/system/docker.service.d/override.conf >/dev/null <<'INI'
[Service]
Delegate=yes
INI

sudo systemctl daemon-reload
sudo systemctl restart docker

# Conferir se o Docker está usando systemd como cgroup driver
docker info | grep -i 'Cgroup Driver'

Crear slices de systemd con ResourceControl (límites por defecto)

Crearemos slices para agrupar contenedores según su función (por ejemplo: producción y analytics). Las propiedades ResourceControl de systemd se traducen en archivos del cgroup v2.

Consejo rápido sobre el disco de Docker: la ruta por defecto es /var/lib/docker. El dispositivo de bloque asociado (para IO*Max) puede descubrirse con herramientas como findmnt o lsblk.

Aplica límites de forma gradual y monitoriza. Considera comenzar con MemoryHigh y, después, establecer MemoryMax.

Propiedad systemd Efecto Archivo cgroup v2 Observaciones
CPUQuota=50% Cuota de CPU (la mitad de 1 CPU) cpu.max Convierte en quota/period (ej.: 50000 100000)
CPUWeight=200 Peso relativo de CPU cpu.weight 1–10000; 100 = valor por defecto; mayor = más prioridad
AllowedCPUs=0-1 Pineo de CPUs cpuset.cpus.effective Restringe a CPUs específicas
MemoryHigh=1G Límite “soft” de memoria memory.high Presiona reclaim antes del límite duro
MemoryMax=2G Límite “hard” de memoria memory.max Superarlo puede matar procesos (OOM)
MemorySwapMax=0 Swap permitido memory.swap.max 0 deshabilita swap; "max" sin límite
IOReadBandwidthMax=/dev/sda 10M Límite de lectura (bytes/s) io.max Usa el dispositivo correcto (ej.: /dev/nvme0n1)
IOWriteBandwidthMax=/dev/sda 5M Límite de escritura (bytes/s) io.max Puede usarse IOPS con IO*IOPSMax
# Descobrir device do /var/lib/docker (ajuste se necessário)
# DEV=$(lsblk -no PKNAME $(findmnt -n -o SOURCE /var/lib/docker) | head -n1); echo "/dev/${DEV}"

# Criar duas slices com limites distintos
sudo tee /etc/systemd/system/containers-prod.slice >/dev/null <<'UNIT'
[Unit]
Description=Slice para containers de producao

[Slice]
CPUQuota=200%
CPUWeight=500
MemoryHigh=4G
MemoryMax=6G
MemorySwapMax=1G
# Ajuste o device conforme seu disco do Docker
IOReadBandwidthMax=/dev/sda 30M
IOWriteBandwidthMax=/dev/sda 15M
UNIT

sudo tee /etc/systemd/system/containers-analytics.slice >/dev/null <<'UNIT'
[Unit]
Description=Slice para containers de analytics

[Slice]
CPUQuota=100%
CPUWeight=200
MemoryHigh=2G
MemoryMax=3G
# I/O mais restritivo
IOReadBandwidthMax=/dev/sda 10M
IOWriteBandwidthMax=/dev/sda 5M
UNIT

sudo systemctl daemon-reload

Ejecutar contenedores dentro de la slice (docker run y Compose)

Coloca el contenedor dentro de la slice con --cgroup-parent. En Compose, usa cgroup_parent en el servicio. Así, todos los procesos del contenedor heredan los límites de la slice.

Si mantienes imágenes privadas, puede interesarte configurar un registro Docker privado con mirror en VPS. Para gestión visual, consulta también cómo usar Portainer.

# docker run com slice de producao
docker run -d --name web \
  --cgroup-parent=containers-prod.slice \
  -p 8080:80 nginx:alpine

# docker run com slice de analytics
docker run -d --name etl \
  --cgroup-parent=containers-analytics.slice \
  alpine:3 sh -c 'while true; do dd if=/dev/zero of=/tmp/file bs=1M count=50; sleep 5; done'

# docker compose (compose.yaml) — campo cgroup_parent por serviço
cat > compose.yaml <<'YML'
services:
  api:
    image: nginx:alpine
    ports: ["8080:80"]
    cgroup_parent: containers-prod.slice
  worker:
    image: alpine:3
    command: ["sh","-c","while true; do sha256sum /dev/zero | head -c 100000 > /dev/null; done"]
    cgroup_parent: containers-analytics.slice
YML

docker compose up -d

Ajustar límites de un contenedor en ejecución (set-property)

Para un ajuste puntual sin recrear el contenedor, modifica el scope de systemd generado por Docker. El nombre del scope sigue el patrón docker-<CONTAINERID>.scope. Los cambios con --runtime son temporales (hasta reboot); para persistir, usa slices.

# ID curto do container
CID=$(docker ps -qf name=web)
SCOPE="docker-${CID}.scope"

# Ver o scope
systemctl status "$SCOPE" --no-pager

# Aplicar limites em runtime (exemplos)
sudo systemctl set-property --runtime "$SCOPE" CPUQuota=50%
sudo systemctl set-property --runtime "$SCOPE" MemoryMax=512M
# Ajuste o device conforme seu disco
sudo systemctl set-property --runtime "$SCOPE" IOReadBandwidthMax=/dev/sda 5M

Cómo confirmar que funcionó (verificación práctica)

Valida con herramientas de systemd y leyendo archivos del cgroup v2. Debes ver el contenedor dentro de la slice y los archivos cpu.max, memory.max e io.max reflejando los valores configurados.

Monitoriza de forma continua. Un stack de métricas ayuda — revisa cómo configurar Prometheus y Grafana con Docker Compose.

# Ver a hierarquia de cgroups/slices
systemd-cgls | sed -n '1,120p'

# Conferir uso ao vivo (cgroups)
systemd-cgtop | head -n 20

# Ler arquivos do cgroup v2 do container no slice
CID=$(docker ps -qf name=web)
CG="/sys/fs/cgroup/containers-prod.slice/docker-${CID}.scope"

printf "cpu.max:    "; cat "$CG/cpu.max" || true
printf "memory.max: "; cat "$CG/memory.max" || true
printf "io.max:     "; cat "$CG/io.max" || true

Solución de problemas comunes

- cgroup v2 ausente: comprueba /sys/fs/cgroup/cgroup.controllers. Si está vacío, habilita cgroup v2 en GRUB y reinicia.

- "cgroup parent not found": la slice debe existir en /etc/systemd/system y systemd necesita daemon-reload.

- Sin efecto en los límites de I/O: probablemente el dispositivo es incorrecto. Descubre el device de /var/lib/docker y úsalo en las directivas IO*Max.

- Contenedores que ignoran cgroup_parent: confirma que el driver de cgroup de Docker es systemd (docker info).

- OOM matando procesos: comienza con MemoryHigh y aumenta MemoryMax de forma gradual.

- Persistencia: los cambios vía set-property en scopes son de runtime. Para mantenerlos tras reinicio del contenedor/host, usa slices + --cgroup-parent.

- Rootless Docker: la ruta y permisos de cgroups difieren; considera mantener límites por slice en el usuario del servicio. Para entornos de CI, revisa consejos en BuildKit y caché remota en CI Docker.

Preguntas frecuentes

¿Necesito Debian 12 para usar cgroup v2 con Docker?

No necesariamente. Debian 12 ya incluye cgroup v2 por defecto. En Debian 11 puedes habilitarlo con systemd.unified_cgroup_hierarchy=1 en GRUB y reiniciar. Luego, configura Docker para usar el driver de cgroup systemd.

¿Cuál es la diferencia entre CPUQuota y CPUWeight?

CPUQuota establece un límite absoluto de CPU (porcentaje). CPUWeight ajusta la prioridad relativa cuando hay contención entre grupos. Usa CPUWeight para equilibrar servicios y CPUQuota para imponer un techo estricto.

¿Cómo descubrir el device correcto para IOReadBandwidthMax?

Identifica el dispositivo que sostiene /var/lib/docker con findmnt y lsblk. Ejemplo: usa findmnt -n -o SOURCE /var/lib/docker y luego mapea al device de bloque principal con lsblk. Aplica la ruta, por ejemplo /dev/sda o /dev/nvme0n1.

¿Puedo limitar un único contenedor sin recrearlo?

Sí. Usa systemctl set-property en el scope docker-<ID>.scope con --runtime para ajustes temporales. Para persistir entre reinicios, es mejor crear una slice con los límites y arrancar el contenedor con --cgroup-parent apuntando a esa slice.

¿Compose soporta cgroup_parent?

Sí. En Docker Compose v2 puedes usar la clave cgroup_parent en el servicio. Todos los procesos de ese servicio heredan la slice y los límites definidos. Asegura que Docker use el driver de cgroup systemd.

¿Estos límites funcionan con diferentes drivers de almacenamiento o redes?

Los límites de CPU y memoria son independientes. Para I/O, los límites se aplican al dispositivo de bloque subyacente. En storages con capas (LVM, RAID, volúmenes remotos), evalúa dónde aplicar y confirma que el kernel exponga controles io.* para el device elegido.

¿Necesitas ayuda para aplicar límites sin interrumpir tu entorno?

Si tu cluster Docker necesita límites previsibles de CPU, memoria e I/O, puedo ayudarte con el diagnóstico e implementación gradual usando systemd y cgroup v2.

Hablar sobre tu caso

Ver también