Diagnóstico em Kubernetes
Quase todo diagnóstico difícil de Kubernetes é, no fundo, um problema de Linux, de rede ou de configuração — só que escondido atrás de uma camada de abstração. O que separa quem resolve em cinco minutos de quem passa a tarde é ter um roteiro, e não saber mais comandos.
O roteiro geral
Seção intitulada “O roteiro geral”Sempre nesta ordem. Ela vai do mais provável e mais barato para o mais raro e mais caro.
# 1. O que o Kubernetes acha que está acontecendo — a fonte mais subestimada.kubectl describe pod <pod> -n <ns>
# 2. Eventos do namespace, mais recentes por último.kubectl get events -n <ns> --sort-by=.lastTimestamp | tail -30
# 3. O que a aplicação disse antes de morrer.kubectl logs <pod> -n <ns> --previous
# 4. O estado real, em vez do resumido.kubectl get pod <pod> -n <ns> -o yamlPod em Pending
Seção intitulada “Pod em Pending”O Pod foi aceito mas nenhum nó o recebeu. O container nem tentou subir — não adianta olhar log.
kubectl describe pod <pod> -n <ns> | sed -n '/Events/,$p'As causas, por frequência:
Sem capacidade. A mensagem é 0/5 nodes are available: Insufficient cpu. Confira o
que você pediu contra o que existe:
kubectl describe nodes | grep -A6 "Allocated resources"Lembre que o agendamento usa requests, não uso real. Um cluster com CPU ociosa pode não ter espaço porque os requests já foram todos reservados.
Volume não vinculado. pod has unbound immediate PersistentVolumeClaims. Verifique o
PVC e a StorageClass:
kubectl get pvc -n <ns>kubectl describe pvc <pvc> -n <ns>Causa clássica: volume ReadWriteOnce já montado em outro nó, com o Pod novo sendo
agendado em um nó diferente.
Taint sem toleration. node(s) had untolerated taint. O nó está marcado para repelir
Pods e o seu não declara que tolera.
Regras de afinidade impossíveis. didn't match Pod's node affinity/selector. Um
nodeSelector apontando para um label que nenhum nó tem.
CrashLoopBackOff
Seção intitulada “CrashLoopBackOff”O container sobe e morre, repetidamente, e o kubelet espera cada vez mais entre as tentativas. Não é um erro em si: é o sintoma.
# O log da execução anterior — a atual pode nem ter chegado a escrever nada.kubectl logs <pod> -n <ns> --previous
# O código de saída conta metade da história.kubectl get pod <pod> -n <ns> -o jsonpath='{.status.containerStatuses[0].lastState.terminated}' | jq| Exit code | Significado |
|---|---|
0 |
Saiu com sucesso — o processo terminou em vez de continuar servindo |
1 |
Erro genérico da aplicação. Veja o log |
126 |
Comando encontrado mas não executável (falta bit de execução) |
127 |
Comando não encontrado — típico de sh ausente em imagem distroless |
137 |
SIGKILL (128+9). Quase sempre OOMKilled |
139 |
SIGSEGV (128+11). Falha de segmentação |
143 |
SIGTERM (128+15). Encerramento pedido — normal durante rollout |
As causas mais comuns, em ordem:
- Erro de configuração. Variável de ambiente ausente, segredo com chave errada,
arquivo de configuração inválido. O log costuma dizer, se você olhar o
--previous. - Dependência indisponível na partida. A aplicação tenta conectar no banco, falha e
sai. Trate com retry e uma
startupProbe, não comsleep. - OOMKilled. Veja a seção seguinte.
- Liveness probe reprovando. A aplicação sobe devagar, a liveness reprova antes de
ela ficar pronta e o kubelet reinicia — para sempre. Use
startupProbe. - PID 1 que não trata sinais. O processo não morre no
SIGTERM, levaSIGKILLe sai com 137, confundindo com falta de memória.
OOMKilled
Seção intitulada “OOMKilled”kubectl get pod <pod> -n <ns> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'# OOMKilledO container ultrapassou o limite de memória e o kernel o matou. Não é o nó que ficou sem memória — é o cgroup do container que estourou.
O que fazer, na ordem:
-
Confira o consumo real antes de aumentar o limite às cegas.
Janela do terminal kubectl top pod <pod> -n <ns> --containers -
Verifique se a aplicação enxerga o limite. Runtimes com heap gerenciado são o caso clássico: uma JVM que não conhece o limite do cgroup dimensiona o heap pela memória do nó, e estoura. Use
-XX:MaxRAMPercentageou o equivalente da sua linguagem. -
Diferencie pico de vazamento. Se o consumo cresce monotonicamente entre reinícios, aumentar o limite só adia o problema.
ImagePullBackOff
Seção intitulada “ImagePullBackOff”O kubelet não conseguiu baixar a imagem.
kubectl describe pod <pod> -n <ns> | grep -A5 "Failed"not found— tag errada, ou a imagem não foi publicada. Confirme se o pipeline realmente fez push.unauthorized/denied— faltaimagePullSecrets, ou o secret está no namespace errado. Secret é por namespace.toomanyrequests— limite de requisições do registry público. Use um mirror ou autentique.- Timeout — o nó não tem rota até o registry. Comum em sub-rede privada sem NAT ou sem endpoint privado.
Service que não responde
Seção intitulada “Service que não responde”Este é o caso com mais saltos possíveis. Vá do nome até o processo, sem pular etapas.
# 1. O Service tem endpoints? Vazio aqui já é a resposta.kubectl get endpointslices -n <ns> -l kubernetes.io/service-name=<svc>
# 2. Se estiver vazio: o selector bate com os labels dos Pods?kubectl get svc <svc> -n <ns> -o jsonpath='{.spec.selector}'kubectl get pods -n <ns> --show-labels
# 3. O Pod está Ready? Pod não-Ready é removido dos endpoints de propósito.kubectl get pods -n <ns> -o wide
# 4. A porta do Service bate com a containerPort?kubectl get svc <svc> -n <ns> -o yaml | grep -A4 ports
# 5. Teste direto no Pod, sem o Service no caminho.kubectl run diag --rm -it --image=nicolaka/netshoot --restart=Never -- \ curl -sv http://<ip-do-pod>:8080/healthz
# 6. Agora pelo nome do Service, para isolar o DNS.kubectl run diag --rm -it --image=nicolaka/netshoot --restart=Never -- \ curl -sv http://<svc>.<ns>.svc.cluster.local:80/Se o passo 5 funciona e o 6 não, o problema é DNS ou o próprio Service. Se ambos falham, é a aplicação ou uma NetworkPolicy.
Depurar imagem sem shell
Seção intitulada “Depurar imagem sem shell”Imagem distroless ou scratch não tem sh, então kubectl exec falha com exit 127. Use
um container efêmero, que injeta uma imagem com ferramentas no mesmo Pod,
compartilhando namespace de rede e processos:
kubectl debug -it <pod> -n <ns> \ --image=nicolaka/netshoot \ --target=<container> \ -- bashDentro dele, o namespace de rede é o mesmo do container original — curl localhost:8080
alcança a aplicação, e ss -tlnp mostra em que porta ela realmente está escutando.
Para investigar o nó em si:
kubectl debug node/<no> -it --image=ubuntu# O sistema de arquivos do nó fica montado em /hostNó em NotReady
Seção intitulada “Nó em NotReady”kubectl describe node <no> | grep -A12 ConditionsAs condições dizem quase tudo:
MemoryPressure/DiskPressure— o kubelet começa a despejar Pods. Em disco, a causa mais comum é acúmulo de imagens e logs.PIDPressure— processos demais, normalmente por um container que gera filhos sem colhê-los.Ready: Unknown— o kubelet parou de reportar. O nó pode estar de pé com o kubelet morto, ou ter perdido a rota até o control plane.
O comando que resolve o caso confuso
Seção intitulada “O comando que resolve o caso confuso”Quando nada faz sentido, compare o que você acha que aplicou com o que está de fato no cluster:
kubectl get <recurso> <nome> -n <ns> -o yaml \ | grep -v "^\s*\(creationTimestamp\|resourceVersion\|uid\|managedFields\)"Metade dos casos “impossíveis” é um manifest antigo que continua aplicado, um valor de Helm que não foi para onde você pensava, ou um controlador GitOps revertendo a sua alteração manual em silêncio.
Leituras relacionadas
Seção intitulada “Leituras relacionadas”- Resolver um Pod em CrashLoopBackOff — o passo a passo focado
- Service que não responde
- Sinais e exit codes — a tabela completa
- kubectl — referência de comandos
- Workloads — probes, requests e limits em detalhe