Cgroups e namespaces no Linux

Tabela de Conteúdo

O objetivo aqui é entender como o Docker e o Kubernetes isolam processos usando namespaces e cgroups, dois recursos que já existem no kernel Linux há mais de uma década.

Container não é máquina virtual. Não existe hypervisor nem kernel próprio rodando dentro dele é só um processo Linux comum, com dois mecanismos do kernel ligados nele: namespaces controlam o que o processo enxerga, cgroups controlam quanto ele pode consumir.

Namespaces

Cada namespace isola uma parte diferente do sistema:

  • pid: árvore de processos (o processo vira PID 1 dentro do namespace)
  • net: interfaces de rede, rotas, portas
  • mnt: pontos de montagem
  • uts: hostname e domainname
  • ipc: memória compartilhada, semáforos, filas de mensagens
  • user: mapeamento de UID/GID
  • cgroup: visão da própria hierarquia de cgroups

Para criar um namespace na mão:

sudo unshare --pid --net --mount --uts --ipc --fork /bin/bash

Dentro dele, esse bash vira o PID 1 e não enxerga nada do host:

ps aux

O hostname também muda só dentro dessa bolha:

hostname container-teste

É basicamente isso que o docker run faz antes de qualquer coisa: cria os namespaces e só depois monta a imagem e roda o entrypoint.

Para debugar um container em produção sem kubectl exec (por exemplo, quando ele não tem shell), o nsenter ajuda bastante:

lsns
sudo nsenter -t <pid> -n -m -p /bin/bash

lsns lista os namespaces ativos no sistema. nsenter entra nos namespaces de um processo que já está rodando.

Cgroups

Se namespace isola o que o processo vê, cgroup (control group) limita e contabiliza o que ele consome CPU, memória, I/O. É o mecanismo por trás de requests/limits no Kubernetes e de --memory/--cpus no Docker.

A maioria das distros hoje usa cgroup v2, montado em /sys/fs/cgroup. Dá pra criar um na mão:

sudo mkdir /sys/fs/cgroup/teste
echo "50000 100000" | sudo tee /sys/fs/cgroup/teste/cpu.max
echo "104857600" | sudo tee /sys/fs/cgroup/teste/memory.max
echo $$ | sudo tee /sys/fs/cgroup/teste/cgroup.procs

O primeiro comando limita a CPU a 50% de um core (50000us de cada 100000us). O segundo limita a memória a 100MB. O terceiro move o shell atual pra dentro do cgroup, e todo processo filho herda esses limites.

É a mesma coisa que acontece quando você roda docker run --cpus=0.5 --memory=100m nginx:1.27 o dockerd configura isso por baixo dos panos.

Alguns arquivos úteis pra acompanhar:

cat /sys/fs/cgroup/teste/memory.current
cat /sys/fs/cgroup/teste/memory.events
cat /sys/fs/cgroup/teste/cpu.stat

memory.events mostra se o kernel já matou algo por OOM. cpu.stat mostra throttling de CPU (nr_throttled) é a mesma fonte que o Kubernetes usa pra reportar isso.

E no Kubernetes?

Quando você define resources.requests/resources.limits num pod, o kubelet traduz isso pras mesmas primitivas: limits.cpu vira cpu.max, limits.memory vira memory.max, e requests.cpu vira cpu.weight prioridade relativa quando há disputa por CPU, não um teto.

Esse último ponto confunde bastante gente: requests não limita nada, só decide quem tem prioridade quando o nó está sob pressão. Se você já leu o post sobre requests vs limits e QoS, essa é a camada de baixo daquilo, o kubelet não inventa isolamento, ele configura cgroups via containerd/CRI.

Para debugar em produção:

systemd-cgls
systemd-cgtop
cat /proc/<pid>/cgroup
readlink /proc/<pid>/ns/*

systemd-cgls mostra a árvore de cgroups do sistema. systemd-cgtop é tipo um top, mas por cgroup. Os outros dois mostram a qual cgroup e a quais namespaces um processo pertence.

Simples assim! :)