const express = require('express');
const mongoose = require('mongoose');
const app = express();
app.get('/health', async (req, res) => {
const healthcheck = {
uptime: process.uptime(),
message: 'OK',
timestamp: Date.now()
};
try {
// Verificăm dacă DB-ul e chiar legat
if (mongoose.connection.readyState !== 1) {
throw new Error('Database not connected');
}
res.send(healthcheck);
} catch (error) {
healthcheck.message = error.message;
res.status(503).send(healthcheck);
}
});Dacă te bazezi pe pm2 status ca să știi dacă aplicația ta e în picioare, am o veste proastă pentru tine: trăiești periculos. Am pățit-o acum vreo 3 ani la un proiect cu vreo 12.000 de useri activi, unde totul apărea cu verde în consolă, dar în realitate API-ul dădea timeout pe bandă rulantă.
Problema e simplă. PM2 monitorizează procesul la nivel de sistem de operare. Dacă PID-ul există și procesul n-a crăpat cu un exit code fatal, PM2 o să-ți raporteze fericit statusul „online”. Dar ce se întâmplă când Event Loop-ul tău e blocat de o funcție sincronă nenorocită sau când conexiunea la baza de date a murit, dar procesul Node.js încă respiră? PM2 n-are nicio idee.
Capcana statusului "online"
Node.js e „single-threaded” (mă rog, știm noi cum e, dar pentru Event Loop contează asta). Am avut cazul unui script care făcea un procesat masiv de JSON-uri într-o buclă. Procesul era „online”, CPU-ul stătea în 100%, dar orice request HTTP nou stătea la coadă până când expira timeout-ul din Nginx. Din perspectiva PM2, totul era perfect.
Altă fază clasică: baza de date pică. Aplicația ta pornește, dar nu se poate conecta. Dacă n-ai pus un process.exit(1) în catch-ul de la conexiune, aplicația rămâne într-un stadiu de „zombie”. E vie, dar inutilă. PM2 o vede online, load balancer-ul îi trimite trafic, userul vede erori.
Cum construiești un healthcheck care chiar zice ceva
Soluția nu e să te uiți la proces, ci să întrebi aplicația dacă „se simte bine”. Eu folosesc mereu un endpoint dedicat, de obicei /health sau /_status. Dar atenție, nu pune doar un res.send('ok'). Asta testează doar dacă serverul HTTP răspunde, ceea ce e un început, dar nu e destul.
Un healthcheck serios trebuie să verifice:
- Conexiunea la baza de date (un simplu
SELECT 1saudb.command({ ping: 1 })). - Conexiunea la Redis sau cozi de mesaje (RabbitMQ/Kafka).
- Timpul de răspuns (dacă durează 5 secunde să-mi zică că e OK, clar avem o problemă de lag).
La proiectul menționat mai sus, am introdus un check care verifica și heap memory-ul. Dacă săream de 80% din memoria alocată constant, forțam un restart înainte să dea V8-ul Out of Memory. Am redus incidentele de tip „silent crash” cu aproape 40% în prima lună.
Trade-off: Deep vs Shallow healthcheck
Aici e o discuție întreagă. Dacă faci un healthcheck prea „deep” (verifici 10 servicii externe), riști să-ți omori singur aplicația. Dacă pică un API terț de care nu depinde funcționarea de bază, nu vrei ca PM2 să-ți dea restart la proces în buclă.
Trade-off-ul meu: în healthcheck-ul pentru PM2/Load Balancer verific doar resursele critice fără de care aplicația e complet inutilă (DB local, Redis pentru sesiuni). Restul le pun într-un alt endpoint de monitorizare (ex. /health/details) pe care îl verifică sistemul de alerting (Grafana/Prometheus), dar care nu declanșează restarturi automate.
Ce facem cu PM2?
PM2 are un modul numit pm2-server-monit sau poți folosi pm2 plus (varianta plătită), dar sincer, cea mai robustă metodă rămâne un script extern sau configurarea corectă a max_memory_restart și un cron care dă un ping în endpoint-ul de sănătate.
Dacă ești pe Kubernetes, PM2 e oricum cam redundant, dar pe un VPS clasic, asigură-te că ai măcar graceful reload configurat. Când PM2 dă restart, trimite SIGINT. Dacă aplicația ta nu ascultă de semnalul ăsta ca să închidă conexiunile curat, o să ai downtime la fiecare deploy.
Voi ce verificați în endpoint-ul de health înainte să lăsați traficul să intre în instanță?