Implementando healthcheck endpoints ricos com dependências externas
1. Fundamentos do Healthcheck em Sistemas Distribuídos
Um healthcheck endpoint é um ponto de verificação que vai muito além de um simples 200 OK. Em sistemas distribuídos, ele é a principal ferramenta para orquestradores como Kubernetes e Docker Swarm tomarem decisões sobre o estado dos serviços. Um healthcheck pobre pode causar falsos positivos, onde um serviço aparentemente saudável está na verdade degradado, ou falsos negativos, onde um serviço funcional é derrubado desnecessariamente.
A diferença entre os tipos de probes é crucial:
- Liveness probe: Indica se o container deve ser reiniciado. Se falha, o pod é morto e recriado.
- Readiness probe: Indica se o serviço está pronto para receber tráfego. Se falha, o pod é removido dos endpoints do Service.
- Startup probe: Usado para serviços que demoram a inicializar, desativando as outras probes até que esteja pronto.
Orquestradores que recebem healthchecks pobres podem causar efeitos cascata, derrubando serviços inteiros por falhas temporárias em dependências não críticas.
2. Projetando a Estrutura do Endpoint de Healthcheck
A resposta JSON deve ser rica e padronizada. Um modelo eficiente inclui:
{
"status": "healthy",
"version": "1.0.0",
"uptime": 123456,
"dependencies": {
"database": {
"status": "healthy",
"latency_ms": 2,
"last_check": "2024-01-15T10:30:00Z"
},
"redis": {
"status": "degraded",
"latency_ms": 150,
"error": "High latency detected"
},
"external_api": {
"status": "unhealthy",
"error": "Connection refused"
}
},
"metadata": {
"hostname": "web-01",
"environment": "production"
}
}
Versionamento do endpoint é prática recomendada:
- /healthz para liveness probe
- /ready para readiness probe
- /health para healthcheck completo
A estratégia de agregação deve considerar degradação parcial. Um status degraded indica que o serviço funciona, mas com performance reduzida, enquanto unhealthy significa falha total.
3. Verificando Dependências Externas de Forma Robusta
Cada tipo de dependência requer uma abordagem específica:
Banco de dados: Além do ping, execute uma consulta leve como SELECT 1 para validar a capacidade de resposta real.
async function checkDatabase() {
try {
const start = Date.now();
await db.raw('SELECT 1');
return {
status: 'healthy',
latency_ms: Date.now() - start
};
} catch (error) {
return {
status: 'unhealthy',
error: error.message
};
}
}
Filas e caches: Redis, RabbitMQ e Kafka devem ser verificados com comandos específicos. Para Redis, use PING; para RabbitMQ, verifique a conexão com o canal.
APIs externas: Implemente timeouts rigorosos e fallbacks. Uma API externa falhando não deve derrubar todo o serviço.
async function checkExternalAPI() {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 2000);
const response = await fetch('https://api.exemplo.com/health', {
signal: controller.signal
});
clearTimeout(timeout);
return {
status: response.ok ? 'healthy' : 'unhealthy',
status_code: response.status
};
} catch (error) {
return {
status: 'degraded',
error: 'API externa indisponível'
};
}
}
4. Tratamento de Timeouts, Erros e Degradação Parcial
Cada dependência deve ter seu próprio timeout para evitar bloqueio do healthcheck geral. Use Promise.race ou AbortController para garantir que verificações lentas não travem todo o processo.
Classificação de erros:
- Conexão recusada: Serviço não está rodando ou porta errada
- Timeout: Serviço lento ou sobrecarregado
- Resposta inesperada: Dados corrompidos ou protocolo incompatível
A lógica de degradação deve ser inteligente. Se um componente não crítico falha, o serviço pode continuar operando com funcionalidade reduzida, mas o healthcheck deve reportar degraded em vez de unhealthy.
5. Cache e Otimização de Performance no Healthcheck
Healthchecks não devem sobrecarregar as dependências. Implemente cache local com TTL configurável:
const healthCache = {
data: null,
lastUpdate: 0,
ttl: 5000 // 5 segundos
};
async function getHealthStatus() {
const now = Date.now();
if (healthCache.data && (now - healthCache.lastUpdate) < healthCache.ttl) {
return healthCache.data;
}
healthCache.data = await runHealthChecks();
healthCache.lastUpdate = now;
return healthCache.data;
}
Verificações paralelas reduzem drasticamente a latência total. Use Promise.all para executar verificações simultâneas, mas com cuidado para não sobrecarregar recursos.
Rate limiting é essencial para evitar que healthchecks frequentes derrubem dependências já sobrecarregadas.
6. Segurança e Observabilidade do Endpoint
Restrinja o acesso ao healthcheck:
- Rede interna apenas (firewall rules)
- Autenticação básica ou token
- Rate limiting por IP
Logging estruturado deve incluir:
- Trace ID para correlação
- Timestamp preciso
- Latência por dependência
- Status code retornado
Métricas Prometheus essenciais:
healthcheck_latency_seconds{dependency="database"} 0.002
healthcheck_failures_total{dependency="redis"} 5
healthcheck_status{dependency="external_api"} 0
7. Exemplo Prático: Implementação em Node.js/Express
const express = require('express');
const app = express();
async function checkDependency(name, checkFn) {
const start = Date.now();
try {
const result = await checkFn();
return {
name,
status: result.status || 'healthy',
latency_ms: Date.now() - start,
...(result.error && { error: result.error })
};
} catch (error) {
return {
name,
status: 'unhealthy',
latency_ms: Date.now() - start,
error: error.message
};
}
}
async function runHealthChecks() {
const checks = await Promise.all([
checkDependency('database', () => db.raw('SELECT 1')),
checkDependency('redis', () => redis.ping()),
checkDependency('external_api', () =>
fetch('https://api.exemplo.com/health', { signal: AbortSignal.timeout(2000) })
)
]);
const dependencies = {};
let overallStatus = 'healthy';
for (const check of checks) {
dependencies[check.name] = {
status: check.status,
latency_ms: check.latency_ms,
...(check.error && { error: check.error })
};
if (check.status === 'unhealthy') {
overallStatus = 'unhealthy';
} else if (check.status === 'degraded' && overallStatus !== 'unhealthy') {
overallStatus = 'degraded';
}
}
return {
status: overallStatus,
version: '1.0.0',
uptime: process.uptime(),
dependencies
};
}
app.get('/health', async (req, res) => {
const health = await runHealthChecks();
const statusCode = health.status === 'healthy' ? 200 :
health.status === 'degraded' ? 200 : 503;
res.status(statusCode).json(health);
});
app.listen(3000);
8. Boas Práticas e Integração com Orquestradores
Para Kubernetes, configure probes adequadamente:
livenessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
Evite efeito cascata: healthcheck não deve depender de si mesmo. Não faça chamadas recursivas ou loops infinitos.
Documente o formato da resposta e o SLA esperado para cada dependência. Isso facilita troubleshooting e mantém expectativas claras entre equipes.
Referências
- Kubernetes Documentation: Configure Liveness, Readiness and Startup Probes — Guia oficial da Kubernetes sobre configuração de probes com exemplos práticos
- Microsoft Azure: Health Endpoint Monitoring Pattern — Padrão de arquitetura para implementação de healthchecks em microsserviços
- Prometheus Documentation: Best Practices for Instrumentation — Diretrizes para métricas de healthcheck e observabilidade
- Node.js Official: HTTP Clients and Timeouts — Documentação sobre gerenciamento de timeouts em requisições HTTP
- Redis Documentation: PING Command — Referência oficial para verificação de conectividade com Redis
- Express.js: Writing Middleware — Guia para implementação de endpoints com Express.js
- 12factor.net: Logs — Princípios de logging estruturado para aplicações modernas