Tutorial: configurar cgroup v2 e systemd ResourceControl para limitar CPU, memória e I/O de containers Docker no Debian

Linux & Sistemas · Por · Publicado em · 9 min de leitura

Tutorial: configurar cgroup v2 e systemd ResourceControl para limitar CPU, memória e I/O de containers Docker no Debian
Foto: Google DeepMind / Pexels

Neste passo a passo você vai configurar cgroup v2 e systemd ResourceControl para limitar CPU, memória e I/O de containers Docker no Debian. Vamos criar slices dedicadas, apontar containers para essas slices e validar com arquivos do cgroup v2. O resultado: limites previsíveis e fáceis de manter, sem gambiarras específicas por container.

Pré-requisitos e o que vamos configurar

A meta é aplicar limites de CPU, memória e I/O em containers Docker usando cgroup v2 controlado pelo systemd. Vamos padronizar limites por slice (grupo de serviços) e, quando necessário, ajustar um container específico.

O que você precisa antes de começar:

- Debian recente (11 ou 12) com systemd e acesso sudo.

- Docker instalado e funcional. Se ainda não tiver, veja como instalar Docker e Docker Compose no Linux.

- Disponibilidade para um reboot caso precise habilitar cgroup v2.

- Conhecimento do dispositivo de disco usado pelo Docker (para limites de I/O).

Avisos importantes:

- Modificar parâmetros do bootloader e reiniciar pode afetar serviços em produção. Faça janela de manutenção.

- Limites de memória muito agressivos podem matar processos (OOMKill). Aplique gradualmente e monitore.

Confirmar (e habilitar) cgroup v2 no Debian

Precisamos do cgroup v2 em modo unificado. Valide a presença do arquivo cgroup.controllers e os controladores disponíveis. Se não estiver ativo, habilite via parâmetro do kernel e reinicie.

No Debian 12 geralmente já vem em v2. No Debian 11 pode ser necessário ajustar o GRUB. Reiniciar é obrigatório após alterar o GRUB.

# Verificar se cgroup v2 está ativo (deve 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 driver de cgroup systemd

Para o Docker delegar corretamente os cgroups ao systemd, use o driver de cgroup "systemd". Também garantimos a diretiva Delegate do serviço docker. Após a alteração, recarregue o systemd e reinicie o 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'

Criar slices do systemd com ResourceControl (limites padrão)

Vamos criar slices para agrupar containers por função (ex.: produção e analytics). As propriedades ResourceControl do systemd traduzem para arquivos do cgroup v2.

Dica rápida para o disco do Docker: o caminho padrão é /var/lib/docker. O dispositivo de bloco associado (para IO*Max) pode ser descoberto com ferramentas como findmnt/lsblk.

Aplique limites gradualmente e monitore. Considere começar com MemoryHigh e só depois MemoryMax.

Propriedade systemd Efeito Arquivo cgroup v2 Observações
CPUQuota=50% Quota de CPU (metade de 1 CPU) cpu.max Converte em quota/period (ex.: 50000 100000)
CPUWeight=200 Peso relativo de CPU cpu.weight 1–10000; 100 = padrão; maior = mais prioridade
AllowedCPUs=0-1 Pin de CPUs cpuset.cpus.effective Restringe a CPUs específicas
MemoryHigh=1G Limite “soft” de memória memory.high Pressiona reclaim antes do hard limit
MemoryMax=2G Limite “hard” de memória memory.max Ultrapassar pode matar processos (OOM)
MemorySwapMax=0 Swap permitido memory.swap.max 0 desabilita swap; "max" sem limite
IOReadBandwidthMax=/dev/sda 10M Limite de leitura (bytes/s) io.max Use o device correto (ex.: /dev/nvme0n1)
IOWriteBandwidthMax=/dev/sda 5M Limite de escrita (bytes/s) io.max Pode usar IOPS com 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

Executar containers dentro da slice (docker run e Compose)

Coloque o container dentro da slice com --cgroup-parent. Em Compose, use cgroup_parent no serviço. Assim, todos os processos do container herdam os limites da slice.

Se você mantém imagens privadas, pode interessar configurar um registry Docker privado com mirror. Para gestão visual, veja também como usar o 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 limites de um container em execução (set-property)

Para um ajuste pontual sem recriar container, altere o scope do systemd gerado pelo Docker. O nome do scope segue o padrão docker-<CONTAINERID>.scope. Mudanças com --runtime são temporárias (até reboot); para persistir, prefira 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

Como confirmar que funcionou (verificação prática)

Valide com ferramentas do systemd e lendo arquivos do cgroup v2. Você deve ver o container dentro da slice e os arquivos cpu.max, memory.max e io.max refletindo os valores configurados.

Monitore continuamente. Um stack com métricas ajuda — veja um caminho em configurar Prometheus e Grafana com 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

Solução de problemas comuns

- cgroup v2 ausente: confira /sys/fs/cgroup/cgroup.controllers. Se vazio, habilite cgroup v2 no GRUB e reinicie.

- "cgroup parent not found": a slice deve existir em /etc/systemd/system e o systemd precisa de daemon-reload.

- Sem efeito nos limites de I/O: provavelmente o device está errado. Descubra o device de /var/lib/docker e use-o nas diretivas IO*Max.

- Containers ignoram cgroup_parent: confirme que o driver de cgroup do Docker é systemd (docker info).

- OOM matando processos: comece com MemoryHigh e aumente MemoryMax gradualmente.

- Persistência: alterações via set-property em scopes são de runtime. Para manter após reinício do container/host, use slices + --cgroup-parent.

- Rootless Docker: o caminho e permissões de cgroups diferem; considere manter limites por slice no usuário do serviço. Para ambientes CI, veja dicas em BuildKit e cache remoto no CI Docker.

Perguntas frequentes

Preciso do Debian 12 para usar cgroup v2 com Docker?

Não necessariamente. O Debian 12 já vem com cgroup v2 por padrão. No Debian 11, você pode habilitar com systemd.unified_cgroup_hierarchy=1 no GRUB e reiniciar. Depois, configure o Docker para usar o driver de cgroup systemd.

Qual a diferença entre CPUQuota e CPUWeight?

CPUQuota define um teto absoluto de CPU (porcentagem). CPUWeight ajusta a prioridade relativa quando há contenção entre grupos. Use CPUWeight para equilíbrio entre serviços e CPUQuota para impor um limite rígido.

Como descobrir o device correto para IOReadBandwidthMax?

Descubra o dispositivo que sustenta /var/lib/docker com findmnt e lsblk. Exemplo: use findmnt -n -o SOURCE /var/lib/docker e então mapeie para o device de bloco principal com lsblk. Aplique o caminho, como /dev/sda ou /dev/nvme0n1.

Posso limitar um único container sem recriá-lo?

Sim. Use systemctl set-property no scope docker-<ID>.scope com --runtime para ajustes temporários. Para persistência entre reinícios, é melhor criar uma slice com os limites e iniciar o container com --cgroup-parent apontando para essa slice.

Compose suporta cgroup_parent?

Sim. Em Docker Compose v2 você pode usar a chave cgroup_parent no serviço. Todos os processos daquele serviço herdam a slice e os limites definidos. Garanta que o Docker esteja com driver de cgroup systemd.

Esses limites funcionam com drivers de armazenamento ou redes diferentes?

Limites de CPU e memória são independentes. Para I/O, os limites se aplicam ao device de bloco subjacente. Em storages com camadas (LVM, RAID, volumes remotos), avalie onde aplicar e confirme se o kernel expõe controles de io.* para o device escolhido.

Precisa de ajuda para aplicar limites sem interromper seu ambiente?

Se o seu cluster Docker precisa de limites previsíveis de CPU, memória e I/O, posso apoiar no diagnóstico e implementação gradual com systemd e cgroup v2.

Falar sobre seu caso

Veja também