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, portasmnt: pontos de montagemuts: hostname e domainnameipc: memória compartilhada, semáforos, filas de mensagensuser: mapeamento de UID/GIDcgroup: 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! :)