Programação Por Equipe DevBras

Capa do artigo: Healthcheck que só responde 200 não diz se o sistema trabalha

Healthcheck que só responde 200 não diz se o sistema trabalha

Separe a checagem de vida da checagem de prontidão, teste a dependência crítica com prazo curto e configure Docker, Compose ou Kubernetes para usar cada uma no lugar certo.

O balanceador manda clientes para a instância porque a rota de health devolve 200. As requisições falham: o banco não conecta desde a troca de senha da madrugada. O processo está de pé e inútil, e o painel de monitoramento está todo verde. A resposta curta: você mediu o processo, não o serviço. Use duas checagens. A de vida (liveness) responde se o processo está funcionando e não depende de nada externo; ela decide reinício. A de prontidão (readiness) confirma, com prazo curto, que as dependências críticas respondem; ela decide se a instância recebe tráfego. Se o banco cair, a instância sai do rodízio e o erro que o cliente vê é honesto.

Por que um 200 sozinho mente

A rota mais comum de health é algo como app.get('/health', (req, res) => res.send('ok')). Ela prova três coisas: o processo existe, a porta está aberta e o roteador HTTP responde. Não prova que o banco aceita conexão, que o pool não está esgotado, que a fila está acessível ou que a configuração carregou certo.

O exagero oposto também quebra. Se a checagem que decide reinício consulta o banco, uma queda do banco faz o orquestrador reiniciar todas as instâncias ao mesmo tempo, sem resolver nada. Quando o banco volta, todas sobem juntas e disputam conexão. Você transformou uma falha de dependência numa tempestade de reinícios.

Por isso as duas perguntas são diferentes:

  • Vida: este processo travou de um jeito que só reiniciar resolve? Exemplo: laço infinito, deadlock, event loop parado.
  • Prontidão: esta instância consegue atender um pedido agora? Exemplo: banco responde, cache carregado, migração concluída.

Como implementar as duas checagens

As rotas na aplicação

Exemplo em Node.js com Express e PostgreSQL. A prontidão faz uma consulta trivial e desiste em um segundo:

const express = require('express');
const { Pool } = require('pg');

const app = express();
const pool = new Pool();

function comPrazo(promessa, ms) {
  let timer;
  const limite = new Promise((_, rejeita) => {
    timer = setTimeout(() => rejeita(new Error('prazo esgotado')), ms);
  });
  return Promise.race([promessa, limite]).finally(() => clearTimeout(timer));
}

app.get('/livez', (req, res) => {
  res.status(200).json({ status: 'vivo' });
});

app.get('/readyz', async (req, res) => {
  try {
    await comPrazo(pool.query('SELECT 1'), 1000);
    res.status(200).json({ status: 'pronto' });
  } catch (err) {
    console.error('readyz falhou:', err.message);
    res.status(503).json({ status: 'indisponivel' });
  }
});

app.listen(3000);

Algumas regras para a rota de prontidão:

  1. Teste só o que é crítico. Sem banco, a aplicação não trabalha: entra. Sem o provedor de e-mail, ela ainda vende: não entra. Dependência opcional que derruba a prontidão tira do ar um sistema que funcionaria pela metade.
  2. Prazo curto e explícito. Uma checagem que espera 30 segundos pelo banco segura conexões e chega depois de o balanceador já ter desistido.
  3. Nada pesado. SELECT 1 a cada poucos segundos é barato. Relatório, contagem de tabela grande ou chamada paga a terceiro, não.
  4. Status HTTP correto. 503 quando não está pronto. Corpo com 200 e texto "erro" não é entendido por nenhum balanceador.
  5. Sem detalhes sensíveis na resposta. Mensagem de erro do banco com host e usuário vai para o log, não para quem chamou a rota.

No Docker e no Compose

O HEALTHCHECK do Dockerfile, ou o bloco healthcheck do Compose, marca o container como starting, healthy ou unhealthy. Imagens slim nem sempre têm curl, então o exemplo usa o próprio Node, que tem fetch nativo a partir da versão 18:

services:
  api:
    image: registry.exemplo.com.br/loja/api:2.3.0
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/readyz').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
      interval: 15s
      timeout: 3s
      retries: 3
      start_period: 20s

  worker:
    image: registry.exemplo.com.br/loja/worker:2.3.0
    depends_on:
      api:
        condition: service_healthy

Um detalhe que pega muita gente: o Docker sozinho, fora do Swarm, só marca o container como unhealthy. Ele não reinicia nem tira de lugar nenhum. A política de reinício reage à saída do processo, não ao healthcheck. O estado serve para depends_on com service_healthy, para o seu monitoramento e para proxies que leem esse estado, como o Traefik. Para ver o histórico das últimas checagens:

docker inspect --format '{{json .State.Health}}' $(docker compose ps -q api)

No Kubernetes

Aqui as duas checagens têm nomes próprios e efeitos diferentes. A liveness reinicia o container; a readiness tira o pod dos endpoints do Service:

livenessProbe:
  httpGet:
    path: /livez
    port: 3000
  periodSeconds: 10
  failureThreshold: 3
readinessProbe:
  httpGet:
    path: /readyz
    port: 3000
  periodSeconds: 5
  timeoutSeconds: 2
  failureThreshold: 2

Se a aplicação demora para subir, existe ainda a startupProbe, que segura a liveness até a partida terminar.

No balanceador

Aponte a checagem ativa do balanceador para /readyz. Repare que o Nginx open source só faz checagem passiva (marca o servidor como falho depois de erros em requisições reais); checagem ativa é recurso do NGINX Plus. HAProxy, Traefik e os balanceadores das nuvens fazem checagem ativa.

A prontidão também é a peça que permite desligar sem derrubar pedido: ao receber o sinal de parada, a aplicação passa a responder 503 e o balanceador para de mandar tráfego antes de ela fechar. O detalhe está em terminar o pedido que já entrou.

Erros comuns

  • Uma rota só para tudo. Se ela checa o banco, a queda do banco reinicia todas as instâncias. Se não checa, o balanceador manda tráfego para quem não trabalha.
  • Checagem sem prazo. O banco lento segura a rota, as checagens se acumulam e o próprio health piora a situação.
  • Dependência opcional na prontidão. O serviço de envio de SMS cai e o site inteiro sai do ar junto.
  • Esperar que o Docker reinicie o unhealthy. Fora do Swarm e do Kubernetes, ninguém age sobre esse estado, a menos que você configure.
  • Health exposto com detalhes. Versão de banco, hostname interno e mensagem de erro vão para qualquer um que chamar a rota.
  • Health pesado com intervalo curto. Dez instâncias consultando uma tabela grande a cada segundo viram carga real no banco.

Checklist

  • Existem duas rotas: uma de vida, sem dependências, e uma de prontidão.
  • A prontidão testa só dependências críticas, cada uma com prazo de um ou dois segundos.
  • A prontidão devolve 503 quando falha.
  • O balanceador ou o orquestrador usa a prontidão para decidir tráfego.
  • A checagem de reinício usa a rota de vida, nunca a de prontidão.
  • As rotas não expõem detalhes internos na resposta.
  • Você já derrubou o banco em homologação e viu a instância sair do rodízio.

Desligue o banco em homologação e chame a sua rota de health. Ela ainda diz que está tudo bem?

Avalie este artigo

Referência: https://docs.docker.com/engine/reference/builder/#healthcheck