const { monitorEventLoopDelay } = require('perf_hooks');
const h = monitorEventLoopDelay({ resolution: 20 });
h.enable();
let consecutiveFails = 0;
async function checkHealth(db) {
const loopLag = Math.round(h.mean / 1e6); // nanosecunde în ms
let dbHealthy = false;
try {
await db.$queryRaw`SELECT 1`;
dbHealthy = true;
} catch (err) {
dbHealthy = false;
}
// Dacă loop-ul e blocat peste 2 secunde sau DB e picat
if (loopLag > 2000 || !dbHealthy) {
consecutiveFails++;
if (consecutiveFails >= 3) {
console.error('[FATAL] Healthcheck eșuat repetat. Forțez restart...');
process.exit(1); // PM2 va reporni instanța
}
return { status: 'DEGRADED', loopLag, dbHealthy };
}
consecutiveFails = 0;
return { status: 'OK', loopLag, dbHealthy };
}Toți am fost acolo: deschizi consola, dai pm2 status, toate cele 8 instanțe sunt verzi, memorie decentă, CPU 0%. Doar că în Slack explodează alertele din Sentry, iar clienții primesc 504 de la Nginx.
Am pățit asta acum vreo doi ani pe un API de plăți cu vreo 12.000 de cereri pe minut. Pentru PM2, procesul era viu pentru că pid-ul exista în sistem și nu crăpase cu exit code non-zero. În realitate, un leak într-un pool de Prisma blocase toate conexiunile către PostgreSQL, iar Event Loop-ul avea un lag de 14 secunde. Aplicația era practic în comă profundă, dar PM2 o considera sănătoasă.
Ce prinde PM2 nativ (și ce ratează complet)
PM2 e un process manager bun pentru ce a fost gândit la bază: repornește procesul dacă aruncă o eroare nesupravegheată (uncaughtException) sau dacă depășește limita de RAM via max_memory_restart. Atât.
Ce ratează cu brio:
- Event loop înghețat: Un apel sincron nesimțit sau un JSON.parse uriaș blochează loop-ul. Procesul nu primește semnale, dar OS-ul îl ține deschis.
- Pool-ul de conexiuni epuizat: Serverul HTTP acceptă handshake-ul de TCP, Nginx trimite cererea, dar handler-ul tău stă agățat la infinit așteptând un socket liber de bază de date.
- Stuck background tasks: Procesul răspunde greu, consumă sloturi de worker, iar timpul de răspuns sare de la 40ms la 30 de secunde.
Separă Liveness de Readiness
Dacă ai lucrat cu Kubernetes știi deja distincția, dar pe un VPS clasic cu PM2 văd rar abordarea asta. Ai nevoie de două rute separate:
/live(Liveness): Verifică doar dacă serverul HTTP poate procesa un request banal. Aici măsori în principal lag-ul din Event Loop. Dacă lag-ul trece de 2000ms, ceva e grav putred și procesul merită împușcat./ready(Readiness): Verifică dacă dependențele critice sunt în picioare (conexiunea la baza de date, Redis). Dacă DB-ul e jos, endpoint-ul întoarce 503.
Un trade-off important aici: nu pune query-uri grele în /ready. Am văzut echipe care făceau SELECT COUNT(*) FROM users la fiecare 5 secunde din scriptul de monitorizare. Dacă rulezi 16 instanțe de Node în cluster, tocmai ai adăugat o tonă de încărcare inutilă pe DB. Un simplu SELECT 1 sau un ping de Redis e arhisuficient.
Cum îl obligi pe PM2 să omoare procesul zombie
PM2 nu are un healthcheck HTTP nativ ca Docker sau Kubelet. Soluția pragmatică fără să complici infrastructura e să lași aplicația să se sinucidă controlat când simte că nu mai e viabilă.
Dacă verificarea de sănătate eșuează de 3 ori consecutiv (nu la primul bump), apelezi process.exit(1). PM2 va vedea ieșirea anormală și va reporni instanța curat.
Pentru cei care au Nginx în față: e mult mai sănătos să scoți din upstream un proces zombie prin fail_timeout și max_fails decât să aștepți ca memoria să ajungă la plafon.
Voi cum monitorizați procesele de Node pe bare-metal sau VPS? Vă bazați doar pe PM2 sau rulați un watchdog extern cu curl?