O avaliador abaixo tem quatro propriedades. Cada checagem tem prazo, então uma conexão que nunca chega vira falha em vez de espera infinita. As checagens rodam em paralelo, então o tempo total é o da mais lenta, e não a soma. O resultado vale por dois segundos e é compartilhado entre requisições concorrentes, para que uma rajada de sondagens não vire uma rajada de SELECT 1 contra o banco. E a resposta separa o que bloqueia a instância, com 503, do que só degrada, com 200 e o detalhe no corpo.
import http from 'node:http';
import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
const CACHE_MS = 2000; // o resultado vale por 2 s: sondagens concorrentes reaproveitam a mesma checagem
const PRAZO_PADRAO_MS = 1000; // abaixo do timeoutSeconds da sondagem, com folga
function comPrazo(promessa, ms, nome) {
let timer;
const prazo = new Promise((_, rejeitar) => {
timer = setTimeout(() => rejeitar(new Error(nome + ': sem resposta em ' + ms + ' ms')), ms);
});
return Promise.race([promessa, prazo]).finally(() => clearTimeout(timer));
}
export function criarAvaliador({ checagens, criticas = [], prazoMs = PRAZO_PADRAO_MS }) {
let cache = null;
let emCurso = null;
async function executar() {
const resultados = {};
await Promise.all(
Object.entries(checagens).map(async ([nome, checar]) => {
const inicio = performance.now();
try {
await comPrazo(checar(), prazoMs, nome);
resultados[nome] = { ok: true, ms: Math.round(performance.now() - inicio) };
} catch (erro) {
resultados[nome] = { ok: false, erro: erro.message };
}
}),
);
const bloqueada = criticas.some((nome) => !resultados[nome].ok);
const degradada = Object.values(resultados).some((r) => !r.ok);
const resposta = {
status: bloqueada ? 503 : 200,
corpo: {
estado: bloqueada ? 'indisponivel' : degradada ? 'degradado' : 'ok',
checagens: resultados,
},
};
cache = { em: Date.now(), resposta };
return resposta;
}
return function avaliar() {
if (cache && Date.now() - cache.em < CACHE_MS) return Promise.resolve(cache.resposta);
emCurso ??= executar().finally(() => {
emCurso = null;
});
return emCurso;
};
}
export function criarServidorSaude(avaliar) {
return http.createServer(async (req, res) => {
if (req.url === '/health/live') {
res.writeHead(200).end('ok'); // o processo responde; nenhuma dependencia entra aqui
return;
}
if (req.url === '/health/ready') {
const { status, corpo } = await avaliar();
res.writeHead(status, { 'Content-Type': 'application/json' }).end(JSON.stringify(corpo));
return;
}
res.writeHead(404).end();
});
}
- O prazo interno precisa ficar abaixo do timeoutSeconds da sondagem, com folga. Se o avaliador leva 1,5 s e o kubelet desiste em 1 s, a instância é marcada como falha mesmo saudável, e o corpo da resposta nunca chega a quem precisava dele.
- O corpo expõe o nome das checagens e as mensagens de erro. Num endpoint público isso revela topologia e detalhes internos. Restrinja o endpoint à rede interna ou remova o campo erro da resposta e mantenha a mensagem no log.
- O event loop entra como degradação, não como bloqueio. Uma instância lenta por carga ainda produz; tirá-la da rotação concentra o tráfego nas outras, que ficam mais lentas, e o efeito se espalha.
Na inicialização, o avaliador é ligado ao pool e ao monitor de event loop:
import { Pool } from 'pg';
import { criarAvaliador, criarServidorSaude } from './saude.js';
const pool = new Pool({ max: 10 });
const loop = monitorEventLoopDelay({ resolution: 20 });
loop.enable();
const avaliar = criarAvaliador({
checagens: {
banco: () => pool.query('SELECT 1'), // se nao ha conexao livre, a espera e limitada pelo prazo
event_loop: async () => {
const p99Ms = loop.percentile(99) / 1e6; // o histograma guarda nanossegundos
loop.reset();
if (p99Ms > 200) throw new Error('p99 de ' + Math.round(p99Ms) + ' ms');
},
},
criticas: ['banco'], // so o banco local bloqueia a instancia; o event loop apenas degrada
});
criarServidorSaude(avaliar).listen(3000);