Otimize o Desempenho de Contêineres Docker com Limites de CPU e Memória

Aprenda a otimizar o desempenho de contêineres Docker definindo limites de CPU e memória. Este guia aborda opções essenciais de configuração como compartilhamentos de CPU, cotas, limites de memória e swap. Descubra como monitorar o uso de recursos do contêiner com `docker stats` e implementar práticas recomendadas para evitar escassez de recursos, melhorar a estabilidade da aplicação e aumentar a eficiência geral do sistema.

Otimize o Desempenho de Contêineres Docker com Limites de CPU e Memória

Os contêineres Docker não se comportam automaticamente como pequenas máquinas virtuais com recursos fixos. A menos que você informe ao Docker o contrário, um contêiner pode competir por CPU e memória do host como qualquer outro processo. Isso é conveniente em um laptop e arriscado em um servidor compartilhado.

Limites de CPU e memória não são impulsionadores mágicos de desempenho. Um limite muito baixo torna uma aplicação mais lenta ou instável. Um limite muito alto não protege o host. O objetivo é dar a cada contêiner espaço suficiente para o trabalho normal e de pico, evitando que um processo problemático derrube tudo o mais na máquina.

Por que os Limites Importam

O modo comum de falha é familiar: um contêiner inicia um trabalho descontrolado, a memória sobe, o host começa a fazer swap e serviços não relacionados ficam lentos. Ou uma tarefa em lote pesada em CPU usa todos os núcleos e o contêiner de API ao lado começa a perder metas de latência.

Os limites ajudam com três problemas práticos:

  • Eles protegem o host de um único contêiner usar toda a memória.
  • Eles tornam o teste de desempenho mais honesto porque o contêiner roda com restrições semelhantes às de produção.
  • Eles forçam você a notar quando uma aplicação precisa de escalonamento ou ajuste, em vez de silenciosamente pegar recursos emprestados de vizinhos.

Não comece com números aleatórios. Execute o serviço sob carga, observe o uso real e defina limites com margem de segurança. Um pequeno worker em segundo plano e um serviço JVM precisam de tratamentos muito diferentes.

Controles de CPU

O Docker expõe alguns controles de CPU. O mais útil no dia a dia é --cpus.

Limite um contêiner a aproximadamente uma CPU e meia:

docker run -d --name api --cpus="1.5" nginx

Isso é mais fácil de ler do que definir manualmente a cota e o período CFS. Internamente, o Docker usa os controles de agendamento de CPU do Linux.

--cpu-shares é diferente. É um peso relativo durante a contenção de CPU, não um limite rígido:

docker run -d --name important-api --cpu-shares 2048 nginx
docker run -d --name batch-worker --cpu-shares 512 worker-image

Quando o host tem CPU ociosa, ambos os contêineres podem usar mais. Quando competem, o primeiro contêiner tem mais peso de agendamento. Isso é útil para priorização, mas não impede um contêiner de usar CPU sobressalente.

Se você precisar de configurações CFS exatas, --cpu-period e --cpu-quota ainda estão disponíveis:

docker run -d --name limited-api \
  --cpu-period 100000 \
  --cpu-quota 50000 \
  nginx

Isso dá ao contêiner 50.000 microssegundos de tempo de CPU em cada período de 100.000 microssegundos, aproximadamente metade de uma CPU. Para a maioria das equipes, --cpus="0.5" comunica a mesma intenção de forma mais clara.

Controles de Memória

Os limites de memória são mais perigosos que os limites de CPU porque excedê-los pode matar o processo. Defina-os deliberadamente e teste o comportamento de pico.

A opção básica é --memory:

docker run -d --name web --memory 512m nginx

Se o contêiner exceder o limite, o kernel pode matar um processo dentro do contêiner. Na saída do Docker, você verá isso frequentemente como uma morte OOM:

docker inspect web --format '{{.State.OOMKilled}}'

O comportamento do swap é fácil de entender errado. Quando --memory está definido, --memory-swap é a permissão total de memória mais swap, não a permissão de swap por si só.

Isso permite 256 MB de RAM e até 256 MB de swap, totalizando 512 MB:

docker run -d --name worker --memory 256m --memory-swap 512m alpine

Definir --memory-swap igual a --memory desabilita swap adicional para esse contêiner em sistemas onde a contabilidade de swap está disponível:

docker run -d --name no-extra-swap --memory 256m --memory-swap 256m alpine

Definir --memory-swap -1 permite swap ilimitado até o swap disponível do host. Isso pode manter um processo vivo, mas pode tornar a latência terrível.

Monitore Antes e Depois

Use docker stats enquanto o serviço está sob carga realista:

docker stats
docker stats web worker

Observe CPU %, MEM USAGE / LIMIT, BLOCK I/O e PIDS. Um serviço sentado em 100 por cento do seu limite de memória não é "eficiente"; está a um pico de tráfego de ser morto. Um serviço vinculado a CPU que é constantemente limitado pode mostrar uma CPU média aceitável enquanto os usuários veem requisições lentas.

Para uma visualização rápida única:

docker stats --no-stream

Para pistas de OOM e reinicialização:

docker ps -a
docker inspect <container> --format 'OOM={{.State.OOMKilled}} Exit={{.State.ExitCode}}'
docker logs --tail 100 <container>

Para monitoramento contínuo, exporte métricas do contêiner para Prometheus, Grafana, um serviço de monitoramento em nuvem ou a plataforma que você já usa. docker stats é triagem, não um sistema de alerta de longo prazo.

Um Fluxo de Trabalho de Ajuste Sensato

Comece sem limites rígidos em um ambiente de teste e capture o uso ocioso, normal e de pico. Em seguida, defina a memória acima do pico observado com margem suficiente para coleta de lixo, crescimento de cache, handshakes TLS e rajadas curtas. Para CPU, decida se você precisa de um limite rígido ou apenas prioridade relativa.

Exemplo para uma API pequena:

docker run -d --name api \
  --cpus="2" \
  --memory 1g \
  --memory-swap 1g \
  -p 8080:80 \
  my-api:latest

Isso diz que a API obtém até duas CPUs, 1 GB de RAM e nenhuma permissão extra de swap. Não é universalmente correto. É um ponto de partida claro para um serviço que foi testado perto dessa faixa.

Para Docker Compose, o mecanismo Docker local suporta opções de recursos como:

services:
  api:
    image: my-api:latest
    mem_limit: 1g
    cpus: 2.0

As configurações de recursos do Compose e Swarm/Kubernetes não são idênticas, então verifique o destino da implantação antes de assumir que um campo tem o mesmo comportamento em todos os lugares.

Erros Comuns

Definir a memória muito baixa é o erro mais comum. A aplicação inicia, passa em um teste de fumaça e morre sob um padrão de requisição que aloca mais memória do que o teste cobriu.

Usar limites de CPU para esconder código ineficiente é outro. Se um serviço é lento porque está fazendo trabalho caro por requisição, um limite de CPU mais baixo torna os sintomas mais óbvios. Não corrige o caminho do código.

Ignorar runtimes de linguagem também prejudica. Aplicações JVM, Node.js, Go, Python e Ruby respondem de forma diferente à pressão de memória. Alguns runtimes precisam de configurações explícitas de heap para que suas suposições internas de memória correspondam ao limite do contêiner.

Finalmente, não ajuste apenas um contêiner. O host ainda precisa de CPU e memória para o kernel, Docker, logging, agentes de monitoramento e quaisquer processos sidecar. Deixe espaço.

Limites bons tornam a falha menor e o desempenho mais previsível. Eles devem vir da medição, não de suposições.