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:
- 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.
- 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.
- Nada pesado.
SELECT 1a cada poucos segundos é barato. Relatório, contagem de tabela grande ou chamada paga a terceiro, não. - Status HTTP correto. 503 quando não está pronto. Corpo com 200 e texto "erro" não é entendido por nenhum balanceador.
- 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?
Referência: https://docs.docker.com/engine/reference/builder/#healthcheck