Resolvendo Containers Docker Lentos: Um Guia de Performance Passo a Passo
Descubra por que os containers Docker estão lentos verificando CPU, memória, I/O de disco, rede, limites, montagens e logs.
Resolvendo Containers Docker Lentos: Um Guia de Performance Passo a Passo
Quando um container Docker parece lento, não comece reconstruindo a imagem ou alterando flags de runtime aleatórias. Primeiro, decida o que "lento" significa. O tempo de resposta da API está alto? Um worker está ficando para trás? A inicialização está lenta? As builds estão lentas? O host está sobrecarregado? Cada um aponta para uma correção diferente.
Um container não é uma mágica de isolamento da física. Ele ainda usa CPU do host, memória do host, armazenamento do host, rede do host e o código da aplicação que você enviou. O Docker adiciona controles e namespaces em torno desses recursos, mas não torna uma consulta lenta rápida ou um disco saturado ocioso.
Comece com uma visão ao vivo rápida:
docker stats
Observe o container enquanto reproduz a lentidão. Uma única captura é menos útil do que ver o que muda sob carga. Se a CPU saltar e permanecer alta, você tem um problema de CPU. Se a memória subir até o container morrer, siga o caminho da memória. Se BLOCK I/O se mover intensamente enquanto as requisições travam, o armazenamento merece atenção. Se o container parecer calmo, mas os usuários ainda perceberem latência, olhe para o aplicativo, chamadas de rede, banco de dados ou serviços upstream.
Primeiro, compare a saúde do container e do host
Um container lento pode simplesmente estar vivendo em um host lento. Verifique ambos os níveis.
docker stats <container>
top
free -h
df -h
No Linux, iostat -xz 1 é útil se disponível. Alta utilização de disco ou longos tempos de espera podem explicar bancos de dados lentos, instalações de pacotes e serviços com muitos logs. No Docker Desktop, verifique também a CPU e a memória atribuídas à VM do Docker. Um Mac com bastante memória ainda pode privar containers se o Docker Desktop estiver limitado muito baixo.
Se todo container está lento, o host é o suspeito. Se um container está lento enquanto os vizinhos estão bem, foque nessa carga de trabalho, seus limites, montagens e dependências.
Gargalos de CPU
No docker stats, a CPU pode exceder 100% porque o Docker relata o uso entre núcleos. Um container usando 200% está usando aproximadamente dois núcleos. A questão importante é se isso é esperado para a carga de trabalho.
Verifique os limites de runtime:
docker inspect <container> --format 'NanoCPUs={{.HostConfig.NanoCpus}} CpuQuota={{.HostConfig.CpuQuota}} CpuPeriod={{.HostConfig.CpuPeriod}} Cpuset={{.HostConfig.CpusetCpus}}'
Se um serviço foi iniciado com --cpus=0.5, ele pode estar sendo limitado sob tráfego normal. No Kubernetes ou Compose, o mesmo problema pode se esconder nos limites de CPU. Um worker que processava trabalhos rapidamente em um laptop pode ficar lento no CI porque só recebe meio CPU.
Para CPU no nível da aplicação, perfile o processo em vez de adivinhar. Para Node, use perfilamento de CPU embutido ou ferramentas estilo clinic. Para Python, amostre com py-spy onde permitido. Para Java, use JFR ou async-profiler. Se você não pode instalar ferramentas dentro de uma imagem de produção, execute a mesma imagem em um ambiente de staging ou use um padrão de container de depuração.
Causas comuns de CPU incluem loops de polling apertados, serialização JSON cara, backtracking de regex, processamento de imagem, compressão e muitas threads de worker disputando poucos núcleos. Aumentar a CPU ajuda apenas se o aplicativo puder usá-la e o host tiver capacidade.
Pressão de memória e mortes OOM
Problemas de memória aparecem como uso crescente de memória, coleta de lixo frequente, atividade de swap no host ou saídas repentinas. Confirme o status OOM:
docker inspect <container> --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} memory={{.HostConfig.Memory}}'
Se OOMKilled=true, o container excedeu sua situação de memória. Isso pode ser um limite explícito --memory, um limite da VM do Docker Desktop ou pressão em todo o host.
Use docker stats enquanto envia tráfego realista. Se a memória crescer sem se estabilizar, suspeite de vazamento, cache sem limites, acúmulo de fila ou uma carga de trabalho que carrega muitos dados de uma vez. Se a memória saltar durante a inicialização e depois se estabilizar, o limite pode ser simplesmente muito baixo para o runtime.
Os padrões da linguagem importam. Java, Node e alguns servidores de aplicação podem reservar ou usar memória de forma diferente dentro de containers dependendo da versão e configuração. Defina opções explícitas de heap ou memória quando precisar de comportamento previsível. Por exemplo, um serviço Java pode precisar de porcentagens de heap cientes de container; um serviço Node pode precisar de --max-old-space-size; um banco de dados precisa de configurações de cache que deixem espaço para o processo e o sistema de arquivos.
Não defina limites de memória tão apertados que o aplicativo gaste todo o tempo coletando lixo. Um container que nunca trava, mas pausa constantemente, ainda está quebrado.
I/O de disco e montagens bind lentas
Problemas de armazenamento são fáceis de perder porque os gráficos de CPU e memória parecem normais. No Docker, a lentidão do disco geralmente vem de um de quatro lugares: I/O pesado da aplicação, logs excessivos, driver de armazenamento ou montagens bind no Docker Desktop.
Verifique a visão do Docker:
docker stats <container>
docker logs --tail 20 <container>
Se os logs são extremamente ruidosos, o driver de logging tem trabalho a fazer. Logs JSON-file podem crescer rapidamente a menos que a rotação seja configurada. Em um serviço ocupado, registrar cada corpo de requisição ou linha de depuração pode se tornar um problema real de performance.
Inspecione as configurações de logging:
docker inspect <container> --format '{{json .HostConfig.LogConfig}}'
Para configurações locais e de servidores pequenos, considere a rotação de logs na configuração do daemon ou no arquivo Compose. Para plataformas de produção, envie logs para o sistema de logging da plataforma e mantenha o volume de log da aplicação intencional.
Montagens bind merecem atenção especial no macOS e Windows. Uma árvore de origem montada do host em um container Linux atravessa uma camada de virtualização. Isso é conveniente para desenvolvimento, mas pode ser muito mais lento do que um volume nomeado para pastas de dependências, bancos de dados ou diretórios com muita escrita.
Por exemplo, um container de desenvolvimento Node pode ser lento se node_modules estiver em uma montagem bind. Um padrão melhor é montar o código fonte, mas manter as dependências em um volume nomeado:
services:
app:
volumes:
- .:/app
- node_modules:/app/node_modules
volumes:
node_modules:
Para bancos de dados, prefira volumes nomeados em vez de montagens bind, a menos que você tenha um fluxo de trabalho específico de backup ou inspeção que exija caminhos do host.
Latência de rede e lentidão de dependências
Um container pode estar "lento" porque está esperando por outro serviço. O processo local pode estar saudável enquanto DNS, um banco de dados, Redis, uma API ou um proxy está lento.
Teste de dentro do container:
docker exec -it <container> sh
curl -w '
lookup:%{time_namelookup} connect:%{time_connect} start:%{time_starttransfer} total:%{time_total}
' -o /dev/null -s http://service:8080/health
A saída curl -w separa a consulta DNS, conexão TCP, primeiro byte e tempo total. Se a consulta DNS estiver lenta, inspecione /etc/resolv.conf e as configurações de DNS do daemon Docker. Se a conexão estiver lenta ou falhar, verifique redes, firewalls e vinculação de serviço. Se o tempo até o primeiro byte estiver lento, o serviço upstream aceitou a conexão, mas demorou para responder.
Para tráfego container-para-container, use uma rede bridge definida pelo usuário para que os containers possam se resolver pelo nome:
docker network create appnet
docker run -d --name api --network appnet my-api
docker run --rm --network appnet curlimages/curl http://api:8080/health
Não faça benchmark através de portas publicadas do host quando o tráfego real é container-para-container. Teste o caminho que a produção usa.
Performance de inicialização é um problema separado
Inicialização lenta geralmente vem do tempo de pull da imagem, instalação de dependências na inicialização do container, migrações de banco de dados ou aquecimento da aplicação.
Um container não deve instalar pacotes toda vez que inicia. Se seu entrypoint executa npm install, pip install, apt-get ou baixa binários em cada boot, mova esse trabalho para a construção da imagem, a menos que haja uma forte razão para não fazer isso.
Verifique os logs de inicialização com timestamps se seu aplicativo os fornecer. Caso contrário, adicione timestamps simples em torno das etapas do entrypoint durante a depuração:
date; echo 'starting migrations'
# migration command
date; echo 'starting server'
# server command
Para imagens puxadas através de uma rede, o tamanho da imagem importa. Builds multi-estágio, .dockerignore e bases de runtime menores melhoram a velocidade de inicialização a frio e implantação. Mas uma vez que a imagem já está presente e o container está em execução, o tamanho da imagem geralmente importa menos do que CPU, memória, I/O e comportamento da aplicação.
Performance de build não é performance de runtime
Builds Docker lentas são frustrantes, mas são uma classe diferente de problema. Se mudanças de código forçam a instalação de dependências em cada build, corrija a ordenação das camadas:
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
Não copie todo o repositório antes de instalar as dependências, a menos que você queira que cada mudança no código fonte invalide a camada de dependências.
Mantenha também o contexto de build pequeno:
.git
node_modules
coverage
dist
*.log
Os cache mounts do BuildKit podem ajudar em downloads repetidos de dependências, mas primeiro certifique-se de que o Dockerfile está ordenado corretamente. Um cache mount não pode salvar completamente um Dockerfile que invalida o cache muito cedo.
Limites de recursos podem proteger o host e prejudicar o aplicativo
Limites de CPU e memória são úteis porque um container não deve derrubar um host. Eles também podem criar lentidão artificial se copiados de um exemplo sem medir a carga de trabalho.
Inspecione os limites:
docker inspect <container> --format '{{json .HostConfig}}' | jq '{Memory, NanoCpus, CpuQuota, CpuPeriod, BlkioWeight}'
Se jq não estiver disponível, inspecione o container normalmente e procure por HostConfig.
Para Compose, verifique a configuração real renderizada:
docker compose config
Isso captura limites herdados de arquivos de override ou variáveis de ambiente. Uma surpresa comum é um arquivo de override de desenvolvimento que define limites baixos e acidentalmente é usado em um ambiente de teste.
Um fluxo de diagnóstico prático
Use este fluxo quando a reclamação for simplesmente "o container está lento":
- Reproduza o comportamento lento e execute
docker statsdurante a reprodução. - Verifique CPU, memória, disco e limites da VM do Docker Desktop do host.
- Inspecione os limites de CPU e memória do container.
- Leia os logs em busca de retentativas, timeouts de conexão, migrações, logging de depuração ou dicas de OOM.
- Teste as dependências de dentro do container com
curl,digou uma imagem de depuração criada para esse fim. - Verifique as montagens: mova caminhos com muita escrita para volumes nomeados quando apropriado.
- Perfile a aplicação se os gráficos de recursos apontarem de volta para o código.
As melhores correções tendem a ser específicas: aumentar um limite de memória muito baixo, parar de registrar payloads enormes, mover dados de banco de dados de uma montagem bind, corrigir um caminho DNS lento, reordenar camadas do Dockerfile ou ajustar o runtime da aplicação. Conselhos genéricos de "otimizar Docker" são menos úteis do que provar qual recurso está realmente lento.