Boas Práticas de Containers vs VMs: Namespaces, Cgroups e o que o Docker Realmente Faz para Times Ágeis Já leu

Introdução: O Problema das Máquinas Virtuais Quando começamos a trabalhar com infraestrutura, a primeira solução que encontramos são as máquinas virtuais (VMs). Uma VM simula um computador completo: você tem um hypervisor (como KVM, Xen ou VirtualBox) que executa sistemas operacionais inteiros, cada um com seu próprio kernel, bibliotecas do sistema e aplicações. O problema é óbvio quando você pensa em recursos: uma VM Ubuntu mínima consome cerca de 2-4 GB de RAM apenas para existir, mesmo antes de rodar qualquer aplicação real. Containers surgiram como uma alternativa radicalmente diferente. Em vez de virtualizar o hardware, containers compartilham o kernel do host (a máquina onde rodam). Isso significa que você pode ter 20, 50 ou até 100 containers rodando aplicações diferentes, consumindo apenas alguns megabytes cada um. Mas como isso é possível sem violar o isolamento entre aplicações? A resposta está em dois mecanismos do kernel Linux: namespaces e cgroups. Entender esses dois conceitos é entender 90% do que o

Introdução: O Problema das Máquinas Virtuais

Quando começamos a trabalhar com infraestrutura, a primeira solução que encontramos são as máquinas virtuais (VMs). Uma VM simula um computador completo: você tem um hypervisor (como KVM, Xen ou VirtualBox) que executa sistemas operacionais inteiros, cada um com seu próprio kernel, bibliotecas do sistema e aplicações. O problema é óbvio quando você pensa em recursos: uma VM Ubuntu mínima consome cerca de 2-4 GB de RAM apenas para existir, mesmo antes de rodar qualquer aplicação real.

Containers surgiram como uma alternativa radicalmente diferente. Em vez de virtualizar o hardware, containers compartilham o kernel do host (a máquina onde rodam). Isso significa que você pode ter 20, 50 ou até 100 containers rodando aplicações diferentes, consumindo apenas alguns megabytes cada um. Mas como isso é possível sem violar o isolamento entre aplicações? A resposta está em dois mecanismos do kernel Linux: namespaces e cgroups. Entender esses dois conceitos é entender 90% do que o Docker realmente faz.

Namespaces: O Isolamento de Recursos

O que é um Namespace?

Um namespace é um mecanismo de isolamento do kernel Linux que faz cada processo (ou grupo de processos) enxergar uma visão própria e isolada de certos recursos do sistema. Pense assim: sem namespaces, todos os processos no seu sistema compartilham a mesma árvore de diretórios, a mesma lista de processos, a mesma rede. Com namespaces, você cria "mundos paralelos" onde cada um tem sua própria visão.

Existem sete tipos principais de namespaces no Linux moderno:

  1. PID Namespace — isola a numeração de processos
  2. Network Namespace — isola interfaces de rede, portas e tabelas de roteamento
  3. Mount Namespace — isola o sistema de arquivos
  4. UTS Namespace — isola nome da máquina e domínio
  5. IPC Namespace — isola fila de mensagens e memória compartilhada
  6. User Namespace — isola UIDs e GIDs
  7. Cgroup Namespace — isola a visão de cgroups

PID Namespace na Prática

Vamos começar com algo concreto. Crie um script simples em Bash que demonstra como um container enxerga processos diferentes do host:

#!/bin/bash

# Este script mostra como um namespace PID funciona

# Primeiro, veja os processos do seu sistema
echo "=== Processos do HOST (PID namespace padrão) ==="
ps aux | head -5

# Agora, vamos criar um novo namespace PID e rodar um shell dentro dele
echo ""
echo "=== Processos DENTRO de um novo PID Namespace ==="

# O comando 'unshare' cria um novo namespace
# --pid: cria novo PID namespace
# --fork: força fork para que o shell seja PID 1
unshare --pid --fork /bin/bash -c "ps aux; sleep 10"

Execute este script. O que você verá é impressionante: dentro do unshare, o /bin/bash aparece como PID 1 (o primeiro processo), enquanto no host ele tem um PID completamente diferente. Isso é um PID namespace em ação.

Mount Namespace: Seu Próprio Filesystem

Agora vamos para o Mount Namespace, que isola a visão do sistema de arquivos. Cada container tem seu próprio root filesystem, suas próprias montagens:

#!/bin/bash

# Demonstração de Mount Namespace

echo "=== Mount points do HOST ==="
mount | head -10

echo ""
echo "=== Mount points DENTRO de um novo Mount Namespace ==="

# --mount: cria novo mount namespace
# Vamos criar um diretório de teste
mkdir -p /tmp/container_root

unshare --mount --mount-propagation=rprivate /bin/bash -c "
    # Dentro do namespace, fazemos um novo mount
    mount -t tmpfs tmpfs /tmp/teste_namespace
    mount | grep teste_namespace
    echo 'Este mount é invisível do host'
"

echo ""
echo "=== Tentando ver o mount do host ==="
mount | grep teste_namespace || echo "Mount não aparece aqui (esperado)"

Este exemplo mostra um princípio fundamental: montagens feitas dentro de um mount namespace são invisíveis para o host e para outros containers. Cada container tem sua própria visão do filesystem.

Network Namespace: Rede Isolada

O Network Namespace isola interfaces de rede, portas, tabelas de roteamento. Um container pode ter sua própria interface loopback, sua própria configuração de rede:

#!/bin/bash

echo "=== Interfaces de rede do HOST ==="
ip link show | head -10

echo ""
echo "=== Interfaces de rede DENTRO de um Network Namespace ==="

# --net: cria novo network namespace
unshare --net /bin/bash -c "
    echo 'Interfaces dentro do namespace:'
    ip link show
    echo ''
    echo 'Tabela de roteamento:'
    ip route show
"

echo ""
echo "=== Tentando pingar localhost do namespace ==="
unshare --net /bin/bash -c "
    ping -c 1 127.0.0.1 && echo 'Loopback funciona'
"

Dentro de um network namespace novo, você vê apenas a interface loopback. Nenhuma outra interface de rede. Isso é perfeito para containers porque você pode atribuir IPs privados a cada container sem conflito.

Cgroups: Controle de Recursos

O que é um Cgroup?

Enquanto namespaces tratam de isolamento de visão, cgroups tratam de limitação de recursos. Um cgroup (control group) permite que você especifique limites no que um processo pode usar: quantos CPUs, quanto de memória RAM, quanto de I/O de disco, etc.

Sem cgroups, um container poderia alocar toda a RAM do servidor, derrubando tudo mais. Com cgroups, você diz: "este container pode usar no máximo 512 MB de RAM, não mais que 25% de CPU". Se tentar ultrapassar, é automaticamente limitado ou morto.

Memory Cgroup: Limitando RAM

Vamos começar de forma prática. No Linux moderno (especialmente com cgroup v2), você controla cgroups através de arquivos no /sys/fs/cgroup. Aqui está um exemplo que cria um cgroup e limita memória:

#!/bin/bash

# Precisamos de permissões root para este exemplo
if [ "$EUID" -ne 0 ]; then 
    echo "Este script precisa ser executado como root"
    exit 1
fi

# Para sistemas com cgroup v2
CGROUP_PATH="/sys/fs/cgroup/demo_container"

# Criar o cgroup
mkdir -p $CGROUP_PATH

# Limitar a memória a 256 MB (256 * 1024 * 1024 bytes)
echo "268435456" > $CGROUP_PATH/memory.max

echo "Cgroup criado em $CGROUP_PATH com limite de 256 MB"

# Agora rodar um processo dentro deste cgroup
# Este script Python vai tentar alocar muita memória
cat > /tmp/memory_hog.py << 'EOF'
#!/usr/bin/env python3
import sys

print("Tentando alocar 500 MB de RAM...")
try:
    # Tenta alocar 500 MB
    big_list = [0] * (500 * 1024 * 1024 // 8)
    print(f"Alocou {len(big_list) * 8 / 1024 / 1024} MB com sucesso")
except MemoryError:
    print("MemoryError: Tentei alocar mais que o permitido pelo cgroup!")
    sys.exit(1)
EOF

chmod +x /tmp/memory_hog.py

# Rodar o script dentro do cgroup
# (A sintaxe exata pode variar; estou mostrando o conceito)
echo "Rodando script que tenta alocar 500 MB (limite é 256 MB)..."
python3 /tmp/memory_hog.py

echo ""
echo "=== Estatísticas do cgroup ==="
cat $CGROUP_PATH/memory.stat | head -10

# Limpeza
rm -rf $CGROUP_PATH

Este exemplo demonstra o ponto-chave: você define um limite (memory.max) e qualquer processo dentro desse cgroup não consegue exceder. Se tentar, receberá um erro de memória.

CPU Cgroup: Limitando Processamento

Você também pode limitar quanto de CPU um container usa:

#!/bin/bash

if [ "$EUID" -ne 0 ]; then 
    echo "Este script precisa ser executado como root"
    exit 1
fi

CGROUP_PATH="/sys/fs/cgroup/cpu_limited"
mkdir -p $CGROUP_PATH

# cpu.max = "max_usec period_usec"
# Exemplo: 50000 100000 = 50% de 1 CPU
echo "50000 100000" > $CGROUP_PATH/cpu.max

echo "Cgroup criado com limite de 50% de CPU"

# Script que usa muito CPU
cat > /tmp/cpu_hog.py << 'EOF'
#!/usr/bin/env python3
import time

print("Rodando loop infinito (será limitado a 50% CPU)...")
start = time.time()
count = 0

while True:
    count += 1
    if count % 100000000 == 0:
        elapsed = time.time() - start
        print(f"Execução: {elapsed:.1f}s, Iterações: {count}")
        if elapsed > 5:
            break
EOF

chmod +x /tmp/cpu_hog.py

echo "Rodando script (saia com Ctrl+C)..."
# Aqui você rodaria dentro do cgroup
# python3 /tmp/cpu_hog.py

rm -rf $CGROUP_PATH

Este controle de CPU é crítico em ambientes compartilhados. Um container não consegue monopolizar a máquina inteira.

O Docker: Juntando Tudo

Como Docker Usa Namespaces e Cgroups

Agora que você entende namespaces e cgroups isoladamente, fica fácil entender o Docker. Docker é essencialmente uma ferramenta que:

  1. Cria namespaces (PID, network, mount, UTS, IPC, user)
  2. Cria e configura cgroups com limites de recursos
  3. Prepara um filesystem root (usando uma imagem)
  4. Executa um comando dentro dessa combinação
  5. Gerencia o ciclo de vida (start, stop, restart)

Quando você roda docker run nginx, por baixo está acontecendo algo assim:

#!/bin/bash

# Simulando (simplificadamente) o que Docker faz

# 1. Docker cria uma imagem do filesystem (usando camadas)
# 2. Docker cria namespaces
unshare --pid --net --mount --uts --ipc --fork /bin/bash -c "
    # 3. Docker muda para o root da imagem
    chroot /var/lib/docker/containers/xyz/rootfs

    # 4. Docker configura cgroups (limitando memória, CPU, etc)
    # (isto aconteceria antes via manipulação de /sys/fs/cgroup)

    # 5. Docker executa o comando principal
    /usr/sbin/nginx
"

Claro, Docker faz muito mais que isso (networking sofisticado, volumes, logs, health checks), mas essa é a essência.

Inspecionando um Container Docker Real

Se você tem Docker instalado, pode inspecionar um container em execução e ver seus namespaces:

#!/bin/bash

# Inicie um container primeiro
docker run -d --name test_container nginx > /dev/null

# Obtenha o PID do container
CONTAINER_PID=$(docker inspect -f '{{.State.Pid}}' test_container)

echo "=== Namespaces do container ==="
ls -l /proc/$CONTAINER_PID/ns/

echo ""
echo "=== Comparando com o host ==="
ls -l /proc/self/ns/

echo ""
echo "=== Cgroups do container ==="
cat /proc/$CONTAINER_PID/cgroup | head -5

echo ""
echo "Você pode ver que o container tem namespaces diferentes do host!"
echo "Mas ambos compartilham o MESMO KERNEL."

# Limpeza
docker rm -f test_container > /dev/null

Quando você roda isso, verá que cada namespace (pid, net, mnt, etc) tem um inode diferente. Isso prova que o container tem sua própria visão isolada.

Diferenças Concretas: VM vs Container

Deixe-me ser muito claro sobre o que você está ganhando e perdendo:

VMs (máquinas virtuais):
- ✅ Isolamento completo (até kernel diferente)
- ✅ Você pode rodar Windows dentro de Linux
- ❌ Muito pesado (gigabytes de RAM cada)
- ❌ Startup lento (minutos)
- ❌ Difícil de escalar (não consegue 1000 VMs num servidor)

Containers (Docker):
- ✅ Leve (megabytes de RAM)
- ✅ Startup rápido (milissegundos)
- ✅ Escala bem (centenas/milhares por host)
- ❌ Todos usam o mesmo kernel (Linux)
- ❌ Menos isolamento (um exploit no kernel afeta todos)

#!/bin/bash

# Script que compara uso de recursos

echo "=== VM Ubuntu padrão ==="
echo "RAM: ~2-4 GB (antes de rodar nada)"
echo "Boot: ~30-60 segundos"
echo "Imagem base: ~2-3 GB"

echo ""
echo "=== Container Ubuntu oficial ==="
# Baixar a imagem é instantâneo no primeiro run
docker run --rm -m 256m ubuntu:latest bash -c "
    echo 'RAM disponível: 256 MB'
    echo 'Tamanho da imagem: ~100 MB'
    echo 'Boot: <1 segundo'
    free -h
"

Limitações e Casos de Uso

Quando Usar VMs?

Containers não são a solução para tudo. Use VMs quando:

  • Você precisa rodar múltiplos kernels (Windows + Linux)
  • Você quer isolamento de kernel completo (multi-tenant muito severo)
  • Sua aplicação realmente precisa do kernel inteiro
  • Você quer suporte a sistemas operacionais diferentes

Quando Usar Containers?

Use containers quando:

  • Você precisa escalar muitas instâncias de uma aplicação
  • Você quer deploys rápidos e reproduzíveis
  • Recursos são limitados (você não pode pagar por 100 VMs)
  • Você trabalha em ambiente Linux (ou quer virtualização leve)

Segurança: A Questão Séria

Uma coisa importante: containers compartilham o kernel. Se houver um exploit no kernel Linux, todos os containers são afetados. Com VMs, cada uma tem seu kernel isolado. Por isso, em ambientes onde segurança multi-tenant é crítica (cloud pública), você ainda vê:

  • Um kernel Linux vulnerável = todos os containers comprometidos
  • Um kernel de VM vulnerável = apenas aquela VM

Mas na prática, para a maioria dos casos (microsserviços, aplicações internas), containers são o padrão porque o ganho em eficiência compensa o risco levemente maior.

Conclusão

Aprendemos três pontos fundamentais que você precisa levar com você:

  1. Namespaces são isolamento de visão — cada container vê seu próprio PID 1, sua própria rede, seu próprio filesystem. Mas todos compartilham o mesmo kernel Linux. Isso é fundamentalmente diferente de uma VM, onde cada máquina tem seu kernel isolado.

  2. Cgroups são limitação de recursos — sem eles, um container poderia derrotar o propósito inteiro consumindo toda a RAM ou CPU. Cgroups garantem que cada container respeite seus limites. É como dizer "você pode usar até aqui, não mais".

  3. Docker não é mágica, é engenharia — Docker é uma ferramenta bem-desenhada que orquestra namespaces, cgroups e filesystems em camadas. Entender esses mecanismos baixo-nível é o que separa alguém que "usa Docker" de alguém que realmente entende o que está acontecendo. Quando seu container não inicia, quando você precisa debugar problemas de rede ou memória, esse conhecimento é ouro.

Referências


Artigos relacionados