const express = require('express');
const { pool } = require('./db');
const app = express();
// Măsurăm event loop lag-ul în timp real
let eventLoopLag = 0;
setInterval(() => {
const start = Date.now();
setImmediate(() => {
eventLoopLag = Date.now() - start;
});
}, 1000);
app.get('/healthz', async (req, res) => {
// 1. Verificăm dacă event loop-ul e sugrumat
if (eventLoopLag > 300) {
return res.status(503).json({
status: 'unhealthy',
reason: `Event loop lag too high: ${eventLoopLag}ms`
});
}
// 2. Ping la DB cu timeout agresiv (max 1s)
try {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('DB Timeout')), 1000)
);
await Promise.race([
pool.query('SELECT 1'),
timeoutPromise
]);
return res.status(200).json({ status: 'healthy', lag: eventLoopLag });
} catch (err) {
return res.status(503).json({
status: 'unhealthy',
reason: err.message
});
}
});Toți am trecut prin asta: te uiți în consolă, pm2 status arată frumos cu verde aprins, uptime 40 de zile, memorie stabilă. Doar că utilizatorii se lovesc de timeout-uri și Nginx aruncă 502 Bad Gateway pe bandă rulantă.
PM2 e bun la ce știe să facă, dar are o definiție extrem de simplistă a vieții: dacă procesul are PID pe OS și nu a dat exit(1), el presupune că ești fericit. Dacă ai un event loop înghețat pe Node.js sau conexiunea la Postgres e într-o stare de zombie, PM2 n-are niciun habar.
Ce ratează PM2 când îți zice că procesul e „online”
Am pățit treaba asta anul trecut pe un serviciu de procesare plăți cu vreo 4.000 req/min. Un coleg a băgat un regex nesimțit pe un payload de webhook fără să-și dea seama de ReDoS. Event loop-ul a murit instantaneu la 100% CPU pe un core. Procesul nu a crăpat, nu a dat eroare neprinsă, deci PM2 n-a mișcat un deget timp de 20 de minute până când a sunat PagerDuty.
Pe scurt, PM2 nu prinde:
- Event loop lag extrem: procesul e ocupat cu ceva sincron și nu mai preia nimic din coadă.
- Pool-uri de conexiuni blocate: DB-ul s-a restartat, driverul tău nu s-a reconectat corect, iar noile query-uri stau la infinit într-un queue.
- Saturarea threadpool-ului libuv: dacă faci crypto sincron sau I/O blocat de fișiere mari, HTTP worker-ul nu mai răspunde.
Singurul lucru pe care PM2 îl știe nativ e max_memory_restart. Îl ții la 1GB, dacă trece de 1GB îi dă restart. Util, dar primitiv.
Cum construiești un healthcheck real
Dacă pui un endpoint /healthz care returnează doar res.json({ status: 'ok' }), te minți singur. Dacă event loop-ul are un lag de 800ms, răspunsul ăla vine, dar restul aplicației gâfâie.
Ai nevoie de două tipuri de verificări combinate într-un singur răspuns rapid: lag-ul intern și dependențele critice cu timeout strict. Codul de mai jos e fix abordarea pe care o punem noi în producție.
Câteva reguli de fier pentru endpoint-ul ăsta:
- Timeout pe query-ul de DB: Maximum 1000-1500ms. Dacă DB-ul nu răspunde într-o secundă la un
SELECT 1, nodul ăla nu e sănătos oricum. - Prag de event loop lag: Dacă o iterație simplă de
setImmediatedurează mai mult de 300ms, refuză traficul. - Fără cache pe răspuns: Vrei starea exactă din secunda aia.
Trade-off-ul: watchdog intern vs agresivitate
Dacă faci healthcheck-ul prea sensibil, ai belit-o în direcția opusă. O mică sughițătură de rețea între worker și DB de 200ms îți va omorî procesul când n-ar trebui.
Pentru un setup stabil, noi legăm endpoint-ul de un script separat de monitorizare (sau de dockcheck / Nginx upstream healthcheck) care cere un failure de 3 ori consecutiv înainte să ceară pm2 reload <id>. Nu lăsa aplicația să-și dea singură process.exit(1) la prima eroare tranzitorie de rețea, că riști o cascadă de restarturi unde pică toate instanțele deodată.
Voi cum monitorizați procesele Node în afara metricilor clasice de CPU/RAM?