Pular para o conteúdo

Resolver um Pod em CrashLoopBackOff

Intermediário14 min de leiturakubernetes

CrashLoopBackOff não é um erro: é o kubelet informando que o container morreu várias vezes e que ele está esperando cada vez mais entre as tentativas (10s, 20s, 40s… até 5 minutos). A causa real está no que aconteceu antes do crash.

Siga os passos na ordem. Cada um elimina uma classe de causa.

O container atual pode ter acabado de nascer e ainda não ter escrito nada. O que você quer é o log de quem morreu:

Janela do terminal
kubectl logs <pod> -n <ns> --previous

Com mais de um container no Pod, aponte qual:

Janela do terminal
kubectl logs <pod> -n <ns> -c <container> --previous

Se aparecer stack trace ou mensagem de erro de configuração, você já tem a resposta. Vá direto para o passo 6.

Se o log vier vazio, o processo morreu antes de conseguir escrever — o que aponta para os passos 2 e 3.

Janela do terminal
kubectl get pod <pod> -n <ns> \
-o jsonpath='{.status.containerStatuses[0].lastState.terminated}' | jq
Exit code O que significa Próximo passo
0 O processo terminou com sucesso e não voltou Passo 5
1 Erro da aplicação Passo 1, com mais atenção
126 Comando existe mas não é executável Passo 4
127 Comando não encontrado Passo 4
137 SIGKILL — quase sempre OOMKilled Passo 3
139 SIGSEGV — falha de segmentação Bug na aplicação ou biblioteca nativa
143 SIGTERM — encerramento solicitado Passo 5

O campo reason no mesmo objeto costuma dizer OOMKilled explicitamente quando é o caso.

Janela do terminal
kubectl get pod <pod> -n <ns> \
-o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'

Se voltar OOMKilled, veja o consumo real antes de mexer no limite:

Janela do terminal
kubectl top pod <pod> -n <ns> --containers

Duas verificações antes de simplesmente aumentar o número:

  • A aplicação enxerga o limite do container? Runtimes com heap gerenciado (JVM, .NET, Node com --max-old-space-size) dimensionam o heap pela memória visível. Sem configuração explícita, muitos olham para a memória do nó e estouram o limite do cgroup. Configure -XX:MaxRAMPercentage=75 ou o equivalente.
  • É pico ou vazamento? Se o consumo cresce sempre até morrer, aumentar o limite só aumenta o intervalo entre os crashes.

Exit 126 ou 127 significa que o container nem chegou a executar sua aplicação.

Janela do terminal
# O que a imagem realmente define como entrypoint e cmd?
kubectl get pod <pod> -n <ns> -o jsonpath='{.spec.containers[0].command} {.spec.containers[0].args}'
docker inspect <imagem> --format '{{.Config.Entrypoint}} {{.Config.Cmd}}'

Causas típicas:

  • command no manifest sobrescrevendo o entrypoint com um caminho que não existe naquela imagem.
  • Imagem distroless ou scratch: não existe sh, então qualquer command: ["sh", "-c", ...] falha com 127.
  • Arquitetura errada: imagem amd64 num nó arm64. A mensagem costuma ser exec format error.
  • Script copiado sem bit de execução — 126.

Se o exit code for 0 ou 143, provavelmente não foi a aplicação que decidiu morrer: ela foi encerrada.

Janela do terminal
kubectl describe pod <pod> -n <ns> | grep -A20 "Liveness\|Events"

Procure eventos como Liveness probe failed seguidos de Killing container.

O padrão clássico: a aplicação leva 40 segundos para subir, a livenessProbe começa a verificar aos 10 segundos e reprova três vezes. O kubelet reinicia. O ciclo se repete para sempre, e a aplicação nunca chega a ficar pronta.

A correção certa é uma startupProbe, que suspende a liveness até a aplicação subir:

startupProbe:
httpGet: { path: /healthz, port: http }
periodSeconds: 5
failureThreshold: 30 # tolera até 150s de partida
livenessProbe:
httpGet: { path: /healthz, port: http }
periodSeconds: 10
failureThreshold: 3

Antes de corrigir, valide. Rode a mesma imagem sem o ciclo de reinício atrapalhando:

Janela do terminal
kubectl run diagnostico -n <ns> --rm -it --restart=Never \
--image=<a-mesma-imagem> \
--overrides='{"spec":{"containers":[{"name":"diagnostico","image":"<a-mesma-imagem>","command":["sh"],"stdin":true,"tty":true}]}}' \
-- sh

Para imagem sem shell, use um container efêmero no Pod que está falhando — ele compartilha o namespace de rede e de processos:

Janela do terminal
kubectl debug -it <pod> -n <ns> --image=nicolaka/netshoot --target=<container> -- bash

Dentro dele, env mostra as variáveis reais, ls -la mostra o que foi montado e cat /proc/1/cmdline mostra o comando que de fato executou.

Se o crash constante está poluindo logs e alertas, congele o Pod substituindo o comando por algo que não morre:

Janela do terminal
kubectl patch deployment <deploy> -n <ns> --type=json -p='[
{"op":"replace","path":"/spec/template/spec/containers/0/command","value":["sleep","infinity"]}
]'

O Pod sobe, você entra nele e investiga com calma. Reverta antes de sair — um Deployment esquecido em sleep infinity é um serviço fora do ar que parece saudável.

Depois de muitos casos, a distribuição costuma ser esta:

  1. Configuração ausente ou errada — variável de ambiente, chave de Secret com nome diferente, ConfigMap não montado. De longe a maior fatia.
  2. Dependência indisponível na partida — a aplicação tenta conectar, falha e sai. Trate com retry e backoff, não com sleep no entrypoint.
  3. OOMKilled — limite baixo, ou runtime que não enxerga o cgroup.
  4. Probe mal configurada — liveness sem startup em aplicação de partida lenta.
  5. Imagem errada — arquitetura, tag ou entrypoint.