const express = require('express');
const app = express();
// Măsurăm simplu lag-ul din event loop
let eventLoopLag = 0;
setInterval(() => {
const start = Date.now();
setImmediate(() => {
eventLoopLag = Date.now() - start;
});
}, 2000).unref();
app.get('/healthz', (req, res) => {
const memoryUsage = process.memoryUsage().heapUsed / 1024 / 1024;
if (eventLoopLag > 250) {
return res.status(503).json({ status: 'unhealthy', reason: 'Event loop lag too high', lag: `${eventLoopLag}ms` });
}
res.json({
status: 'ok',
memory: `${Math.round(memoryUsage)}MB`
});
});Să fim sinceri: de câte ori nu ai dat pm2 status, ai văzut verdele ăla frumos cu „online”, dar în realitate clienții tăi primeau timeout? Am pățit-o acum vreo doi ani la un proiect cu vreo 15k useri zilnici. Ne-am trezit cu API-ul complet blocat din cauza unui memory leak masiv combinat cu un query de Postgres neindexat. PM2 arăta mândru că procesul rulează liniștit. Normal că rula, procesul din punct de vedere al sistemului de operare era activ, doar că event loop-ul Node.js era complet înghețat.
PM2 monitorizează procesul din exterior. Cât timp procesul are un PID și nu a crăpat cu un error code care să declanșeze restartul, PM2 consideră că treaba lui e făcută. Asta e marea lui limitare: nu înțelege ce se întâmplă în interiorul runtime-ului de Node.
Cum arată un healthcheck care nu te minte
Ca să nu mai fim prinși cu garda jos, trebuie să expunem un endpoint de /health în aplicație. Dar atenție, un simplu res.send('OK') nu e de ajuns. Dacă event loop-ul e blocat, nici măcar acel OK nu va fi trimis, ceea ce e un semnal bun pentru load balancer (care va da timeout), dar nu ne ajută să diagnosticăm fin problema înainte să pice tot.
Un healthcheck robust trebuie să verifice trei lucruri:
- Event Loop Lag: Cât durează până când o funcție asincronă simplă ajunge să fie executată. Dacă lag-ul trece de 200-300ms, aplicația deja gâfâie.
- Conexiunile externe critice: Baza de date (un simplu
SELECT 1) și eventual Redis. - Resursele sistemului: Memoria utilizată (dacă trece de 85% din limita setată prin
--max-old-space-size, e timpul de un restart controlat).
Trade-off-ul sincer: Deep vs. Shallow Healthchecks
Aici apare o problemă de design pe care mulți o ignoră. Dacă pui load balancer-ul (AWS ALB, Cloudflare, Nginx) să apeleze endpoint-ul de healthcheck din 5 în 5 secunde, iar în acel endpoint tu faci de fiecare dată interogări în baza de date, s-ar putea să îți pui singur baza de date în cap. Am pățit să avem 4 instanțe de API care generau cumulat sute de interogări de healthcheck pe minut, doar ca să ne asigurăm că „e totul verde”.
Recomandarea mea e să separi lucrurile. Faci un endpoint de tip "shallow" (rapid) la /healthz care doar verifică memoria și event loop-ul local. Pe ăsta îl rulezi des, la 5-10 secunde. Și faci un endpoint "deep" la /health/ready pe care îl rulezi mai rar, la 30-60 de secunde, unde verifici conexiunea la DB și alte servicii externe.
Dacă folosești Docker cu PM2, poți configura direct în Dockerfile sau în docker-compose o instrucțiune de HEALTHCHECK care apelează local acest endpoint folosind curl sau un script Node simplu. Dacă rulezi pe un VM clasic, poți folosi un utilitar de monitorizare extern sau chiar un script cron local care dă pm2 reload dacă endpoint-ul local dă timeout de 3 ori la rând.
Voi cum gestionați monitorizarea asta internă? Vă bazați doar pe uptime-ul raportat de PM2 sau aveți endpoint-uri dedicate pentru asta?