Pular para o conteúdo
9 min de leitura

Como Containers Funcionam de Verdade: Namespaces, cgroups e o Padrão OCI

Por Equipe Nebular ·

Container não é máquina virtual leve. Entenda namespaces, cgroups, o filesystem em camadas e o padrão OCI que sustentam Docker, containerd e Kubernetes.

Neste artigo

Existe uma frase que atrapalha mais do que ajuda: "container é uma máquina virtual leve". A comparação parece útil no primeiro dia e passa a atrapalhar em todos os dias seguintes, porque um container não virtualiza hardware, não sobe um kernel próprio e não tem um sistema operacional convidado. Um container é apenas um processo comum do Linux rodando com uma visão restrita do sistema. Toda a mágica está em como o kernel isola essa visão. Quando você entende os mecanismos por trás disso, o comportamento do Docker, do containerd e do Kubernetes deixa de ser misterioso e passa a ser previsível.

Este artigo desce até a camada onde a coisa realmente acontece. Não é teoria acadêmica: entender namespaces, cgroups e o filesystem em camadas é o que separa quem debuga um container travado em minutos de quem fica adivinhando por horas.

O container é um processo, não uma máquina#

Comece por esta imagem mental. Quando você roda docker run nginx, não nasce nenhuma máquina nova. Nasce um processo nginx no seu próprio kernel Linux, o mesmo kernel que roda o resto do host. A diferença é que esse processo foi colocado dentro de um conjunto de restrições que fazem ele enxergar um mundo menor: uma árvore de processos que começa nele, um sistema de arquivos que parece ser a raiz mas não é, uma interface de rede isolada, e um teto de CPU e memória que ele não pode ultrapassar.

Nenhuma dessas restrições é invenção do Docker. Todas são recursos do kernel Linux, alguns com mais de uma década de estrada. O Docker, o containerd e o Podman são orquestradores que sabem chamar esses recursos na ordem certa. É por isso que containers Linux precisam de um kernel Linux: não há kernel convidado para emular, o processo usa o kernel do host diretamente. Essa é também a razão pela qual containers são tão mais leves e rápidos que máquinas virtuais: não há boot de sistema operacional, não há hipervisor no caminho, há apenas um processo começando a executar.

Namespaces: a ilusão do isolamento#

O primeiro pilar são os namespaces. Um namespace é um mecanismo do kernel que dá a um grupo de processos uma visão privada de um determinado recurso global do sistema. Existem vários tipos, e cada um isola uma dimensão diferente.

  • PID namespace: isola a árvore de processos. Dentro do container, o processo principal enxerga a si mesmo como PID 1, como se fosse o init do sistema. Ele não vê os processos do host nem de outros containers. Do lado de fora, no host, esse mesmo processo tem um PID comum, como 24815.
  • Mount namespace: isola a tabela de pontos de montagem. É o que permite ao container ter sua própria raiz de sistema de arquivos sem enxergar as montagens do host.
  • Network namespace: dá ao container sua própria pilha de rede, com interfaces, tabelas de roteamento e regras de firewall próprias. É por isso que dois containers podem escutar na porta 8080 sem colidir: cada um tem sua própria porta 8080 num namespace de rede separado.
  • UTS namespace: isola o hostname, permitindo que cada container tenha seu próprio nome de máquina.
  • IPC namespace: isola comunicação entre processos, como filas de mensagens e memória compartilhada.
  • User namespace: mapeia usuários e grupos, permitindo que o root dentro do container seja um usuário sem privilégio no host, um mecanismo de segurança valioso quando bem configurado.

O ponto central é que o isolamento de um container é a soma de vários namespaces aplicados a um processo. Não existe uma "coisa" chamada container no kernel; existe um processo participando de um PID namespace, de um mount namespace, de um network namespace, e assim por diante. Quando você ouve que um container "compartilha o kernel do host", é isto: os namespaces particionam a visão, mas o kernel executando as syscalls é sempre um só.

Você pode inspecionar isso à mão. Um processo no host expõe seus namespaces em /proc/<pid>/ns/. Dois processos que compartilham o mesmo network namespace apontam para o mesmo identificador ali. É esse mecanismo, e não nenhuma abstração exótica, que o Kubernetes usa para colocar todos os containers de um mesmo Pod no mesmo network namespace, o que os faz enxergarem localhost em comum.

cgroups: o teto de recursos#

Namespaces controlam o que o processo . Os control groups, ou cgroups, controlam o que o processo pode consumir. Sem cgroups, um container com vazamento de memória poderia consumir toda a RAM do host e derrubar os vizinhos. Com cgroups, você define limites e garantias.

Os cgroups organizam processos em uma hierarquia e aplicam controladores de recurso a cada nó dessa hierarquia. Os mais usados no dia a dia são:

  • memory: define o limite máximo de memória. Ao ultrapassar o limite, o processo é candidato ao OOM killer do kernel, que o encerra. É a origem clássica do container que "morre sozinho" com exit code 137: ele bateu no teto de memória e foi morto.
  • cpu: define quanto de tempo de CPU o grupo recebe, seja por peso relativo (shares) ou por cota absoluta em uma janela de tempo (quota e period). É por isso que um container pode ser limitado a "meio núcleo" sem que exista meio núcleo físico: o kernel simplesmente reduz o tempo de execução na janela.
  • pids: limita o número de processos, defesa direta contra fork bombs.
  • io: controla a banda de entrada e saída de disco.

Na segunda geração, o cgroups v2, a hierarquia foi unificada e o modelo ficou mais coerente, o que a maioria das distribuições modernas já adota por padrão. Entender cgroups é o que permite dimensionar requests e limits no Kubernetes com propriedade, porque esses campos do manifesto são traduzidos diretamente em configuração de cgroup no nó. Definir um limite de memória apertado demais gera OOM kills silenciosos; definir um limite de CPU baixo demais gera throttling que aparece como latência inexplicável. Nos dois casos, a causa raiz está no cgroup, não no código da aplicação.

O filesystem em camadas e o padrão de imagens#

Falta o terceiro pilar: de onde vem o sistema de arquivos que o container enxerga. Aqui entra o filesystem em camadas, geralmente implementado com o driver overlay2 no Linux.

Uma imagem de container é composta por camadas empilhadas e imutáveis. Cada instrução do processo de build que altera o filesystem gera uma nova camada, que registra apenas a diferença em relação à anterior. Quando o container roda, o runtime empilha essas camadas somente-leitura e adiciona no topo uma camada gravável, fina, exclusiva daquele container. Toda escrita vai para essa camada de topo por meio de um mecanismo de copy-on-write: modificar um arquivo que vem de uma camada inferior copia esse arquivo para a camada gravável e altera a cópia.

Esse desenho explica várias características práticas. Camadas de imagem são compartilhadas entre containers e entre imagens: se dez imagens partem da mesma base, essa base ocupa espaço uma única vez em disco. É também por isso que a ordem das instruções no build importa tanto para cache; e é por isso que a camada gravável some quando o container é removido, o que torna o armazenamento de dados persistentes um problema à parte, resolvido por volumes. A disciplina de construir imagens enxutas e bem ordenadas é um tema por si só, que exploramos em detalhe no guia de boas práticas de Dockerfile.

OCI: por que Docker não é a única opção#

Nos primeiros anos, "container" e "Docker" eram quase sinônimos. Isso mudou com a criação da Open Container Initiative, a OCI, que padronizou dois contratos fundamentais e desacoplou o ecossistema de qualquer fornecedor.

A image specification define como uma imagem de container é estruturada: o formato do manifesto, das camadas e da configuração. Qualquer ferramenta que produza uma imagem no formato OCI gera algo que qualquer runtime compatível consegue rodar. A runtime specification define como um container em execução deve ser configurado e iniciado a partir de um bundle no disco.

A implementação de referência do runtime é o runc, um binário pequeno que recebe uma configuração e faz o trabalho bruto de criar namespaces, aplicar cgroups e executar o processo. Acima dele vive o containerd, um daemon que gerencia o ciclo de vida completo: baixar imagens, gerenciar armazenamento, chamar o runc. E acima do containerd vivem interfaces como o Docker e o próprio Kubernetes, que fala com o containerd por meio de uma interface padronizada, a CRI.

A consequência dessa estratificação é libertadora. O Kubernetes moderno não depende do Docker: ele conversa com o containerd, ou com outro runtime compatível com a CRI, e roda imagens no formato OCI que podem ter sido construídas por Docker, BuildKit, Buildah, Kaniko ou qualquer outra ferramenta aderente ao padrão. A remoção do suporte direto ao Docker como runtime do Kubernetes, anunciada em 2020 e concretizada nas versões seguintes, assustou muita gente na época mas foi apenas a formalização dessa arquitetura em camadas. As imagens continuaram funcionando porque o que importava era o padrão OCI, não a marca.

Por que isso muda como você opera#

Amarrando os três pilares: um container é um processo isolado por namespaces, limitado por cgroups, enxergando um filesystem em camadas montado a partir de uma imagem no padrão OCI. Não há mágica, há mecanismos do kernel orquestrados na ordem certa.

Esse modelo mental paga dividendos constantes. Um container que morre com exit code 137 bateu no limite de memória do cgroup, não teve um bug aleatório. Uma aplicação com latência estranha sob carga pode estar sofrendo CPU throttling por uma quota de cgroup apertada. Dois serviços que não se enxergam podem estar em network namespaces separados quando você esperava que estivessem juntos. Uma imagem que ocupa gigabytes a mais que o necessário provavelmente tem camadas mal ordenadas carregando artefatos de build.

Nada disso é sorte ou azar. É comportamento determinístico de mecanismos que você pode inspecionar, medir e ajustar. O dia em que "container é uma VM leve" some do seu vocabulário e dá lugar a "container é um processo com namespaces, cgroups e camadas" é o dia em que você para de tratar a plataforma como caixa-preta e começa a operá-la com intenção.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly