Pular para o conteúdo

Diagnóstico em Kubernetes

Intermediário24 min de leiturakubernetes

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.

Sempre nesta ordem. Ela vai do mais provável e mais barato para o mais raro e mais caro.

Janela do terminal
# 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 yaml

O Pod foi aceito mas nenhum nó o recebeu. O container nem tentou subir — não adianta olhar log.

Janela do terminal
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:

Janela do terminal
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:

Janela do terminal
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.

O container sobe e morre, repetidamente, e o kubelet espera cada vez mais entre as tentativas. Não é um erro em si: é o sintoma.

Janela do terminal
# 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:

  1. 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.
  2. Dependência indisponível na partida. A aplicação tenta conectar no banco, falha e sai. Trate com retry e uma startupProbe, não com sleep.
  3. OOMKilled. Veja a seção seguinte.
  4. Liveness probe reprovando. A aplicação sobe devagar, a liveness reprova antes de ela ficar pronta e o kubelet reinicia — para sempre. Use startupProbe.
  5. PID 1 que não trata sinais. O processo não morre no SIGTERM, leva SIGKILL e sai com 137, confundindo com falta de memória.
Janela do terminal
kubectl get pod <pod> -n <ns> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'
# OOMKilled

O 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:

  1. Confira o consumo real antes de aumentar o limite às cegas.

    Janela do terminal
    kubectl top pod <pod> -n <ns> --containers
  2. 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:MaxRAMPercentage ou o equivalente da sua linguagem.

  3. Diferencie pico de vazamento. Se o consumo cresce monotonicamente entre reinícios, aumentar o limite só adia o problema.

O kubelet não conseguiu baixar a imagem.

Janela do terminal
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 — falta imagePullSecrets, 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.

Este é o caso com mais saltos possíveis. Vá do nome até o processo, sem pular etapas.

Janela do terminal
# 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.

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:

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

Dentro 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:

Janela do terminal
kubectl debug node/<no> -it --image=ubuntu
# O sistema de arquivos do nó fica montado em /host
Janela do terminal
kubectl describe node <no> | grep -A12 Conditions

As 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.

Quando nada faz sentido, compare o que você acha que aplicou com o que está de fato no cluster:

Janela do terminal
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.