Pod reiniciando em loop na subida: a sonda de vida não esperou a aplicação
Configure startupProbe, livenessProbe e readinessProbe no Kubernetes com prazos calculados a partir do tempo real de subida, para parar o loop de reinício.
O deploy da API subiu às 14h e às 14h20 o pod estava com 11 reinícios. A aplicação leva uns 25 segundos para abrir o pool de conexões e aquecer o cache. A sonda de vida começava a perguntar aos 5 segundos e desistia depois de duas falhas. O Kubernetes matava o container antes de ele terminar de subir, e cada nova tentativa partia do zero. A resposta curta: durante a subida, quem manda é a startupProbe, com prazo folgado. A livenessProbe só começa depois que a subida terminou e serve para detectar travamento. Quem decide se o pod recebe tráfego é a readinessProbe, que tira o pod do balanceamento sem matá-lo.
Três sondas, três perguntas diferentes: já terminou de subir, está travado, pode receber pedido agora. Usar a de vida para responder às três é o que transforma uma subida lenta em loop infinito.
O que cada sonda faz quando falha
A liveness responde "o processo está vivo e funcionando?". Quando ela falha o número de vezes configurado em failureThreshold, o kubelet reinicia o container. É a única das três que mata.
A readiness responde "pode receber tráfego agora?". Quando falha, o pod sai dos endpoints do Service e para de receber pedidos novos, mas continua rodando. Quando volta a passar, o tráfego volta.
A startup responde "já terminou de iniciar?". Enquanto ela não passa, as outras duas ficam desligadas. Se ela estourar o prazo total, o container é reiniciado. Depois que ela passa uma vez, não roda mais naquele container.
Sem sonda de startup, a de vida começa a contar logo depois do initialDelaySeconds. Os padrões são periodSeconds: 10, timeoutSeconds: 1 e failureThreshold: 3. Com valores apertados como no exemplo, cinco segundos de espera e duas falhas a cada cinco segundos, o container morre por volta dos 10 segundos. Uma aplicação que precisa de 25 nunca chega lá.
O initialDelaySeconds maior parece resolver, mas é um número fixo para uma duração que varia. No nó frio, com a imagem recém-baixada e o banco ocupado, a mesma aplicação pode levar 50 segundos. E um atraso inicial alto na liveness também atrasa a detecção de travamento logo depois da subida.
Como diagnosticar o loop
kubectl get pods -l app=loja-api
kubectl describe pod loja-api-7d9f8c6b5-x2k4q
kubectl logs loja-api-7d9f8c6b5-x2k4q --previous
No get pods, a coluna RESTARTS cresce e o estado pode alternar com CrashLoopBackOff. No describe, a seção de eventos mostra falhas de liveness seguidas do aviso de que o container será reiniciado. O --previous mostra o log do container que morreu. Se o último registro é sempre uma etapa da inicialização, como "conectando ao banco" ou "carregando catálogo", e nunca "servidor pronto", o prazo está curto, não a aplicação quebrada.
Como configurar as três sondas
- Meça a subida real. Registre no log o instante em que o processo começa e o instante em que fica pronto. Olhe os piores casos, não a média: nó novo, imagem baixando, banco sob carga.
- Dê à startup um prazo de duas a três vezes o pior caso. O prazo é
failureThresholdvezesperiodSeconds. Com pior caso de 25 segundos, 30 tentativas a cada 2 segundos dão 60 segundos. - Faça a liveness verificar só o próprio processo. O endpoint responde se o servidor HTTP atende. Nada de banco, fila ou API externa.
- Faça a readiness verificar o que o pod precisa para atender: pool aberto, migração aplicada, cache carregado.
- Dê folga à liveness: timeout de alguns segundos e três falhas seguidas antes de matar. Uma pausa de coleta de lixo não pode virar reinício.
- Registre no log quando a aplicação fica pronta e quando a readiness muda de estado, para correlacionar com os eventos do pod.
O manifesto
containers:
- name: loja-api
image: registry.exemplo.com.br/loja-api:1.42.0
ports:
- containerPort: 8080
startupProbe:
httpGet:
path: /livez
port: 8080
periodSeconds: 2
failureThreshold: 30
livenessProbe:
httpGet:
path: /livez
port: 8080
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: 8080
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
A startup e a liveness usam o mesmo endpoint, e isso é intencional: a pergunta é a mesma, o que muda é a paciência. Durante a subida, até 60 segundos. Depois, três falhas seguidas, cerca de 30 segundos de travamento, antes de reiniciar.
Os endpoints na aplicação
const express = require('express');
const app = express();
let pronto = false;
let encerrando = false;
app.get('/livez', (req, res) => res.send('ok'));
app.get('/readyz', (req, res) => {
if (pronto && !encerrando) return res.send('pronto');
res.status(503).send('aguarde');
});
app.listen(8080, () => console.log('http ouvindo na 8080'));
iniciar()
.then(() => {
pronto = true;
console.log('aplicação pronta');
})
.catch((err) => {
console.error('falha ao iniciar', err);
process.exit(1);
});
process.on('SIGTERM', () => {
encerrando = true;
});
O servidor HTTP abre a porta antes da inicialização pesada, então /livez responde desde o primeiro segundo e /readyz diz "aguarde" até iniciar() terminar. Se a inicialização falhar, o processo sai com erro, em vez de ficar vivo sem nunca ficar pronto. No SIGTERM, a readiness passa a falhar para o pod sair do balanceamento; o resto do desligamento está em ao desligar, termine o pedido que já entrou.
Por que a liveness não deve olhar o banco
É tentador fazer a sonda de vida testar tudo: banco, Redis, API de pagamento. Pense no que acontece quando o banco fica lento por dois minutos. Todos os pods falham a liveness ao mesmo tempo, todos são reiniciados, todos voltam juntos tentando abrir conexões no banco que já estava sofrendo. Um problema de dependência vira uma queda total, e reiniciar a API não conserta o banco.
A dependência fora do ar é assunto da readiness, e mesmo nela vale cuidado: se todos os pods saírem do balanceamento juntos, a pessoa recebe erro do balanceador em vez de uma resposta da aplicação dizendo o que está fora. Sobre o que uma verificação deve de fato testar, veja healthcheck que só responde 200.
Erros comuns
- Usar
initialDelaySecondscomo prazo de subida. Um número fixo para uma duração que varia: curto no nó frio, longo demais para detectar travamento. - Liveness que depende do banco. Uma lentidão no banco reinicia todos os pods de uma vez e piora a situação.
- Timeout de 1 segundo na liveness. Uma pausa de coleta de lixo ou um pico de CPU vira reinício.
- Readiness que passa antes de o pool abrir. O pod recebe tráfego e os primeiros pedidos falham.
- Mesmo endpoint e mesma lógica para readiness e liveness. Quando a dependência cai, o pod é morto em vez de só sair do balanceamento.
- Não olhar o log do container anterior. Sem o
--previous, você vê só a subida nova e nunca o ponto onde a anterior morreu.
Checklist
- Você sabe o tempo de subida no pior caso, medido em produção.
- A
startupProbetem prazo de duas a três vezes esse pior caso. - A
livenessProbetesta só o próprio processo. - A
readinessProbesó passa depois de a aplicação estar pronta para atender. - Timeout e número de falhas da liveness toleram pausas curtas.
- O log registra "aplicação pronta" e mudanças de readiness.
- No
SIGTERM, a readiness passa a falhar antes do encerramento.
Rode kubectl describe pod no seu pod com mais reinícios: as falhas de liveness acontecem antes do primeiro pedido atendido com sucesso? Se sim, o problema é paciência, não código.
Referência: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/