Programação Por Equipe DevBras

Capa do artigo: Pod reiniciando em loop na subida: a sonda de vida não esperou a aplicação

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

  1. 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.
  2. Dê à startup um prazo de duas a três vezes o pior caso. O prazo é failureThreshold vezes periodSeconds. Com pior caso de 25 segundos, 30 tentativas a cada 2 segundos dão 60 segundos.
  3. 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.
  4. Faça a readiness verificar o que o pod precisa para atender: pool aberto, migração aplicada, cache carregado.
  5. 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.
  6. 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 initialDelaySeconds como 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 startupProbe tem prazo de duas a três vezes esse pior caso.
  • A livenessProbe testa só o próprio processo.
  • A readinessProbe só 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.

Avalie este artigo

Referência: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/