Docker & Kubernetes

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

17 min de leitura

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

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

<h2>Introdução: O Problema das Máquinas Virtuais</h2>

<p>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.</p>

<p>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: <strong>namespaces</strong> e <strong>cgroups</strong>. Entender esses dois conceitos é entender 90% do que o Docker realmente faz.</p>

<h2>Namespaces: O Isolamento de Recursos</h2>

<h3>O que é um Namespace?</h3>

<p>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 &quot;mundos paralelos&quot; onde cada um tem sua própria visão.</p>

<p>Existem sete tipos principais de namespaces no Linux moderno:</p>

<ol>

<li><strong>PID Namespace</strong> — isola a numeração de processos</li>

<li><strong>Network Namespace</strong> — isola interfaces de rede, portas e tabelas de roteamento</li>

<li><strong>Mount Namespace</strong> — isola o sistema de arquivos</li>

<li><strong>UTS Namespace</strong> — isola nome da máquina e domínio</li>

<li><strong>IPC Namespace</strong> — isola fila de mensagens e memória compartilhada</li>

<li><strong>User Namespace</strong> — isola UIDs e GIDs</li>

<li><strong>Cgroup Namespace</strong> — isola a visão de cgroups</li>

</ol>

<h3>PID Namespace na Prática</h3>

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

<pre><code class="language-bash">#!/bin/bash

Este script mostra como um namespace PID funciona

Primeiro, veja os processos do seu sistema

echo &quot;=== Processos do HOST (PID namespace padrão) ===&quot;

ps aux | head -5

Agora, vamos criar um novo namespace PID e rodar um shell dentro dele

echo &quot;&quot;

echo &quot;=== Processos DENTRO de um novo PID Namespace ===&quot;

O comando &#039;unshare&#039; 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 &quot;ps aux; sleep 10&quot;</code></pre>

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

<h3>Mount Namespace: Seu Próprio Filesystem</h3>

<p>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:</p>

<pre><code class="language-bash">#!/bin/bash

Demonstração de Mount Namespace

echo &quot;=== Mount points do HOST ===&quot;

mount | head -10

echo &quot;&quot;

echo &quot;=== Mount points DENTRO de um novo Mount Namespace ===&quot;

--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 &quot;

Dentro do namespace, fazemos um novo mount

mount -t tmpfs tmpfs /tmp/teste_namespace

mount | grep teste_namespace

echo &#039;Este mount é invisível do host&#039;

&quot;

echo &quot;&quot;

echo &quot;=== Tentando ver o mount do host ===&quot;

mount | grep teste_namespace || echo &quot;Mount não aparece aqui (esperado)&quot;</code></pre>

<p>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.</p>

<h3>Network Namespace: Rede Isolada</h3>

<p>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:</p>

<pre><code class="language-bash">#!/bin/bash

echo &quot;=== Interfaces de rede do HOST ===&quot;

ip link show | head -10

echo &quot;&quot;

echo &quot;=== Interfaces de rede DENTRO de um Network Namespace ===&quot;

--net: cria novo network namespace

unshare --net /bin/bash -c &quot;

echo &#039;Interfaces dentro do namespace:&#039;

ip link show

echo &#039;&#039;

echo &#039;Tabela de roteamento:&#039;

ip route show

&quot;

echo &quot;&quot;

echo &quot;=== Tentando pingar localhost do namespace ===&quot;

unshare --net /bin/bash -c &quot;

ping -c 1 127.0.0.1 &amp;&amp; echo &#039;Loopback funciona&#039;

&quot;</code></pre>

<p>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.</p>

<h2>Cgroups: Controle de Recursos</h2>

<h3>O que é um Cgroup?</h3>

<p>Enquanto namespaces tratam de <strong>isolamento de visão</strong>, cgroups tratam de <strong>limitação de recursos</strong>. 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.</p>

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

<h3>Memory Cgroup: Limitando RAM</h3>

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

<pre><code class="language-bash">#!/bin/bash

Precisamos de permissões root para este exemplo

if [ &quot;$EUID&quot; -ne 0 ]; then

echo &quot;Este script precisa ser executado como root&quot;

exit 1

fi

Para sistemas com cgroup v2

CGROUP_PATH=&quot;/sys/fs/cgroup/demo_container&quot;

Criar o cgroup

mkdir -p $CGROUP_PATH

Limitar a memória a 256 MB (256 1024 1024 bytes)

echo &quot;268435456&quot; &gt; $CGROUP_PATH/memory.max

echo &quot;Cgroup criado em $CGROUP_PATH com limite de 256 MB&quot;

Agora rodar um processo dentro deste cgroup

Este script Python vai tentar alocar muita memória

cat &gt; /tmp/memory_hog.py &lt;&lt; &#039;EOF&#039;

#!/usr/bin/env python3

import sys

print(&quot;Tentando alocar 500 MB de RAM...&quot;)

try:

Tenta alocar 500 MB

big_list = [0] (500 1024 * 1024 // 8)

print(f&quot;Alocou {len(big_list) * 8 / 1024 / 1024} MB com sucesso&quot;)

except MemoryError:

print(&quot;MemoryError: Tentei alocar mais que o permitido pelo cgroup!&quot;)

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 &quot;Rodando script que tenta alocar 500 MB (limite é 256 MB)...&quot;

python3 /tmp/memory_hog.py

echo &quot;&quot;

echo &quot;=== Estatísticas do cgroup ===&quot;

cat $CGROUP_PATH/memory.stat | head -10

Limpeza

rm -rf $CGROUP_PATH</code></pre>

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

<h3>CPU Cgroup: Limitando Processamento</h3>

<p>Você também pode limitar quanto de CPU um container usa:</p>

<pre><code class="language-bash">#!/bin/bash

if [ &quot;$EUID&quot; -ne 0 ]; then

echo &quot;Este script precisa ser executado como root&quot;

exit 1

fi

CGROUP_PATH=&quot;/sys/fs/cgroup/cpu_limited&quot;

mkdir -p $CGROUP_PATH

cpu.max = &quot;max_usec period_usec&quot;

Exemplo: 50000 100000 = 50% de 1 CPU

echo &quot;50000 100000&quot; &gt; $CGROUP_PATH/cpu.max

echo &quot;Cgroup criado com limite de 50% de CPU&quot;

Script que usa muito CPU

cat &gt; /tmp/cpu_hog.py &lt;&lt; &#039;EOF&#039;

#!/usr/bin/env python3

import time

print(&quot;Rodando loop infinito (será limitado a 50% CPU)...&quot;)

start = time.time()

count = 0

while True:

count += 1

if count % 100000000 == 0:

elapsed = time.time() - start

print(f&quot;Execução: {elapsed:.1f}s, Iterações: {count}&quot;)

if elapsed &gt; 5:

break

EOF

chmod +x /tmp/cpu_hog.py

echo &quot;Rodando script (saia com Ctrl+C)...&quot;

Aqui você rodaria dentro do cgroup

python3 /tmp/cpu_hog.py

rm -rf $CGROUP_PATH</code></pre>

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

<h2>O Docker: Juntando Tudo</h2>

<h3>Como Docker Usa Namespaces e Cgroups</h3>

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

<ol>

<li>Cria namespaces (PID, network, mount, UTS, IPC, user)</li>

<li>Cria e configura cgroups com limites de recursos</li>

<li>Prepara um filesystem root (usando uma imagem)</li>

<li>Executa um comando dentro dessa combinação</li>

<li>Gerencia o ciclo de vida (start, stop, restart)</li>

</ol>

<p>Quando você roda <code>docker run nginx</code>, por baixo está acontecendo algo assim:</p>

<pre><code class="language-bash">#!/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 &quot;

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

&quot;</code></pre>

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

<h3>Inspecionando um Container Docker Real</h3>

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

<pre><code class="language-bash">#!/bin/bash

Inicie um container primeiro

docker run -d --name test_container nginx &gt; /dev/null

Obtenha o PID do container

CONTAINER_PID=$(docker inspect -f &#039;{{.State.Pid}}&#039; test_container)

echo &quot;=== Namespaces do container ===&quot;

ls -l /proc/$CONTAINER_PID/ns/

echo &quot;&quot;

echo &quot;=== Comparando com o host ===&quot;

ls -l /proc/self/ns/

echo &quot;&quot;

echo &quot;=== Cgroups do container ===&quot;

cat /proc/$CONTAINER_PID/cgroup | head -5

echo &quot;&quot;

echo &quot;Você pode ver que o container tem namespaces diferentes do host!&quot;

echo &quot;Mas ambos compartilham o MESMO KERNEL.&quot;

Limpeza

docker rm -f test_container &gt; /dev/null</code></pre>

<p>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.</p>

<h3>Diferenças Concretas: VM vs Container</h3>

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

<p><strong>VMs (máquinas virtuais):</strong></p>

<ul>

<li></li>

</ul>

<p>Isolamento completo (até kernel diferente)</p>

<ul>

<li></li>

</ul>

<p>Você pode rodar Windows dentro de Linux</p>

<ul>

<li>❌ Muito pesado (gigabytes de RAM cada)</li>

<li>❌ Startup lento (minutos)</li>

<li>❌ Difícil de escalar (não consegue 1000 VMs num servidor)</li>

</ul>

<p><strong>Containers (Docker):</strong></p>

<ul>

<li></li>

</ul>

<p>Leve (megabytes de RAM)</p>

<ul>

<li></li>

</ul>

<p>Startup rápido (milissegundos)</p>

<ul>

<li></li>

</ul>

<p>Escala bem (centenas/milhares por host)</p>

<ul>

<li>❌ Todos usam o mesmo kernel (Linux)</li>

<li>❌ Menos isolamento (um exploit no kernel afeta todos)</li>

</ul>

<pre><code class="language-bash">#!/bin/bash

Script que compara uso de recursos

echo &quot;=== VM Ubuntu padrão ===&quot;

echo &quot;RAM: ~2-4 GB (antes de rodar nada)&quot;

echo &quot;Boot: ~30-60 segundos&quot;

echo &quot;Imagem base: ~2-3 GB&quot;

echo &quot;&quot;

echo &quot;=== Container Ubuntu oficial ===&quot;

Baixar a imagem é instantâneo no primeiro run

docker run --rm -m 256m ubuntu:latest bash -c &quot;

echo &#039;RAM disponível: 256 MB&#039;

echo &#039;Tamanho da imagem: ~100 MB&#039;

echo &#039;Boot: &lt;1 segundo&#039;

free -h

&quot;</code></pre>

<h2>Limitações e Casos de Uso</h2>

<h3>Quando Usar VMs?</h3>

<p>Containers não são a solução para tudo. Use VMs quando:</p>

<ul>

<li>Você precisa rodar múltiplos kernels (Windows + Linux)</li>

<li>Você quer isolamento de kernel completo (multi-tenant muito severo)</li>

<li>Sua aplicação realmente precisa do kernel inteiro</li>

<li>Você quer suporte a sistemas operacionais diferentes</li>

</ul>

<h3>Quando Usar Containers?</h3>

<p>Use containers quando:</p>

<ul>

<li>Você precisa escalar muitas instâncias de uma aplicação</li>

<li>Você quer deploys rápidos e reproduzíveis</li>

<li>Recursos são limitados (você não pode pagar por 100 VMs)</li>

<li>Você trabalha em ambiente Linux (ou quer virtualização leve)</li>

</ul>

<h3>Segurança: A Questão Séria</h3>

<p>Uma coisa importante: containers compartilham o kernel. Se houver um exploit no kernel Linux, <strong>todos os containers são afetados</strong>. 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ê:</p>

<ul>

<li>Um kernel Linux vulnerável = todos os containers comprometidos</li>

<li>Um kernel de VM vulnerável = apenas aquela VM</li>

</ul>

<p>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.</p>

<h2>Conclusão</h2>

<p>Aprendemos três pontos fundamentais que você precisa levar com você:</p>

<ol>

<li><strong>Namespaces são isolamento de visão</strong> — 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.</li>

</ol>

<ol>

<li><strong>Cgroups são limitação de recursos</strong> — 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 &quot;você pode usar até aqui, não mais&quot;.</li>

</ol>

<ol>

<li><strong>Docker não é mágica, é engenharia</strong> — 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 &quot;usa Docker&quot; 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.</li>

</ol>

<h2>Referências</h2>

<ul>

<li><a href="https://man7.org/linux/man-pages/man7/namespaces.7.html" target="_blank" rel="noopener noreferrer">Linux Namespaces - Documentação Oficial</a></li>

<li><a href="https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html" target="_blank" rel="noopener noreferrer">Cgroups v2 - Kernel.org</a></li>

<li><a href="https://docs.docker.com/get-started/docker_concepts/the_basics/what_is_a_container/" target="_blank" rel="noopener noreferrer">Docker Internals - Docker Official Documentation</a></li>

<li><a href="https://www.linuxfoundation.org/" target="_blank" rel="noopener noreferrer">Linux Foundation - Container Fundamentals</a></li>

<li><a href="https://www.ibm.com/cloud/blog/containers-vs-vms" target="_blank" rel="noopener noreferrer">Boehm, J. - Containers vs VMs: What&#039;s the Difference?</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Docker & Kubernetes

Como Usar FinOps em Kubernetes: Kubecost, Right-sizing e Spot Instances em Produção
Como Usar FinOps em Kubernetes: Kubecost, Right-sizing e Spot Instances em Produção

FinOps em Kubernetes: Uma Introdução Prática FinOps é a disciplina que une fi...

Guia Completo de Grafana em Kubernetes: Dashboards, Alertas e Loki para Logs
Guia Completo de Grafana em Kubernetes: Dashboards, Alertas e Loki para Logs

Introdução ao Grafana em Kubernetes Grafana é uma plataforma de visualização...

Redes em Docker: bridge, host, overlay e macvlan na Prática na Prática
Redes em Docker: bridge, host, overlay e macvlan na Prática na Prática

Entendendo as Redes no Docker: Uma Perspectiva Prática Quando você começa a t...