Node.js Prozess Exit Strategien: Signale, Fehler, und Ordnungsgemäße Herunterfahrten
Emily Parker
Product Engineer · Leapcell

Hintergrund Einführung
Nachdem unser Dienst bereitgestellt wurde, ist es unvermeidlich, dass er von der Laufzeitumgebung (wie Containern, PM2 usw.) geplant wird, Service-Upgrades durchläuft, die Neustarts verursachen, oder verschiedene Ausnahmen auftreten, die zu Prozessabstürzen führen. Im Allgemeinen verfügen Laufzeitumgebungen über Gesundheitsüberwachungsmechanismen für Serviceprozesse. Wenn ein Prozess abstürzt, startet die Laufzeit ihn neu. Während Upgrades werden in der Regel Rolling-Upgrade-Strategien verwendet. Die Planungsstrategien von Laufzeitumgebungen behandeln unsere Serviceprozesse jedoch als Black Boxes, ohne ihren internen Zustand zu berücksichtigen. Daher muss unser Serviceprozess proaktiv Planungsaktionen aus der Laufzeitumgebung erkennen und vor dem Beenden notwendige Bereinigungsaktionen durchführen.
Heute werden wir verschiedene Szenarien zusammenfassen, die zum Beenden eines Node.js-Prozesses führen können, und besprechen, was wir tun können, indem wir auf diese Prozessbeendigungsereignisse hören.
Prinzipien
Ein Prozess wird auf eine von zwei Arten beendet:
- Der Prozess wird freiwillig beendet.
- Der Prozess empfängt ein Systemsignal, das ihn anweist, sich zu beenden.
Beenden über System Signale
Die offizielle Node.js-Dokumentation listet gängige System Signale auf. Wir konzentrieren uns auf die folgenden:
- SIGHUP: Wird ausgelöst, wenn das Terminal direkt geschlossen wird, anstatt
Ctrl + C
zu verwenden, um den Prozess zu stoppen. - SIGINT: Wird ausgelöst, wenn
Ctrl + C
gedrückt wird, um den Prozess zu stoppen. PM2 sendet dieses Signal auch an Kindprozesse, wenn er sie neu startet oder stoppt. - SIGTERM: Wird typischerweise verwendet, um einen Prozess ordnungsgemäß zu beenden. Wenn Kubernetes beispielsweise einen Pod löscht, sendet er ein SIGTERM-Signal, damit der Pod innerhalb eines Zeitlimits (Standard: 30 Sekunden) Bereinigungsaktionen durchführen kann.
- SIGBREAK: Wird auf Windows-Systemen ausgelöst, wenn
Ctrl + Break
gedrückt wird. - SIGKILL: Erzwingt das sofortige Beenden des Prozesses und verhindert jegliche Bereinigungsaktionen. Wenn
kill -9 pid
ausgeführt wird, empfängt der Prozess dieses Signal. Wenn ein Pod in Kubernetes nicht innerhalb des 30-Sekunden-Timeouts beendet wird, sendet Kubernetes ein SIGKILL-Signal, um ihn sofort zu beenden. In ähnlicher Weise sendet PM2 SIGKILL, wenn ein Prozess während des Neustarts oder der Beendigung nicht innerhalb von 1,6 Sekunden beendet wird.
Für nicht-erzwungene Beendigungssignale kann ein Node.js-Prozess auf diese Signale hören und benutzerdefinierte Beendigungsverhalten definieren. Wenn wir beispielsweise ein CLI-Tool haben, das lange zum Ausführen von Aufgaben benötigt, können wir den Benutzer vor dem Beenden auffordern, wenn Ctrl + C
gedrückt wird:
const readline = require('readline'); process.on('SIGINT', () => { // Simple command-line interaction using readline const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); rl.question('Die Aufgabe ist noch nicht abgeschlossen. Möchten Sie den Vorgang wirklich beenden? ', (answer) => { if (answer === 'yes') { console.log('Aufgabe unterbrochen. Prozess wird beendet.'); process.exit(0); } else { console.log('Aufgabe wird fortgesetzt...'); } rl.close(); }); }); // Simulieren Sie eine Aufgabe, die eine Minute zum Abschließen benötigt const longTimeTask = () => { console.log('Aufgabe gestartet...'); setTimeout(() => { console.log('Aufgabe abgeschlossen.'); }, 1000 * 60); }; longTimeTask();
Dieses Skript zeigt jedes Mal eine Eingabeaufforderung an, wenn Ctrl + C
gedrückt wird:
Die Aufgabe ist noch nicht abgeschlossen. Möchten Sie den Vorgang wirklich beenden? no
Aufgabe wird fortgesetzt...
Die Aufgabe ist noch nicht abgeschlossen. Möchten Sie den Vorgang wirklich beenden? no
Aufgabe wird fortgesetzt...
Die Aufgabe ist noch nicht abgeschlossen. Möchten Sie den Vorgang wirklich beenden? yes
Aufgabe unterbrochen. Prozess wird beendet.
Freiwilliger Prozessausstieg
Ein Node.js-Prozess kann sich aufgrund der folgenden Szenarien freiwillig beenden:
- Während der Ausführung tritt ein nicht abgefangener Fehler auf (kann mit
process.on('uncaughtException')
abgefangen werden). - Es tritt eine nicht behandelte Promise-Ablehnung auf (ab Node.js v16 führen nicht behandelte Ablehnungen zum Beenden des Prozesses; verwenden Sie
process.on('unhandledRejection')
, um sie zu behandeln). - Ein
error
-Ereignis wird von einemEventEmitter
ausgegeben, wird aber nicht behandelt. - Der Prozess ruft explizit
process.exit()
auf. - Die Node.js-Ereignisschleife ist leer (d. h. es gibt keine ausstehenden Aufgaben), was mit
process.on('exit')
erkannt werden kann.
PM2 verfügt über einen Daemon-Prozess, der den Dienst neu startet, wenn er abstürzt. Wir können einen ähnlichen Selbstheilungsmechanismus in Node.js mithilfe des Cluster-Moduls implementieren, bei dem Worker-Prozesse automatisch neu gestartet werden, wenn sie abstürzen:
const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; const process = require('process'); if (cluster.isMaster) { console.log(`Master-Prozess gestartet: ${process.pid}`); // Erstellen Sie Worker-Prozesse basierend auf der Anzahl der CPU-Kerne for (let i = 0; i < numCPUs; i++) { cluster.fork(); } // Auf Worker-Exit-Ereignisse hören cluster.on('exit', (worker, code, signal) => { console.log(`Worker ${worker.process.pid} wurde mit Code: ${code || signal} beendet. Neustart...`); cluster.fork(); }); } if (cluster.isWorker) { process.on('uncaughtException', (error) => { console.log(`Worker ${process.pid} hat einen Fehler festgestellt`, error); process.emit('disconnect'); process.exit(1); }); // Erstellen Sie einen HTTP-Server http .createServer((req, res) => { res.writeHead(200); res.end('Hallo Welt\n'); }) .listen(8000); console.log(`Worker-Prozess gestartet: ${process.pid}`); }
Praktische Umsetzung
Nachdem wir nun verschiedene Szenarien analysiert haben, in denen ein Node.js-Prozess beendet werden kann, implementieren wir einen Prozess-Exit-Listener, mit dem Benutzer benutzerdefinierte Exit-Verhaltensweisen definieren können.
// exit-hook.js const tasks = []; const addExitTask = (fn) => tasks.push(fn); const handleExit = (code, error) => { // Implementierungsdetails werden unten erläutert }; process.on('exit', (code) => handleExit(code)); process.on('SIGHUP', () => handleExit(128 + 1)); process.on('SIGINT', () => handleExit(128 + 2)); process.on('SIGTERM', () => handleExit(128 + 15)); process.on('SIGBREAK', () => handleExit(128 + 21)); process.on('uncaughtException', (error) => handleExit(1, error)); process.on('unhandledRejection', (error) => handleExit(1, error));
Für handleExit
stellen wir sicher, dass sowohl synchrone als auch asynchrone Aufgaben mithilfe von process.nextTick()
ordnungsgemäß behandelt werden:
let isExiting = false; const handleExit = (code, error) => { if (isExiting) return; isExiting = true; let hasDoExit = false; const doExit = () => { if (hasDoExit) return; hasDoExit = true; process.nextTick(() => process.exit(code)); }; let asyncTaskCount = 0; let asyncTaskCallback = () => { process.nextTick(() => { asyncTaskCount--; if (asyncTaskCount === 0) doExit(); }); }; tasks.forEach((taskFn) => { if (taskFn.length > 1) { asyncTaskCount++; taskFn(error, asyncTaskCallback); } else { taskFn(error); } }); if (asyncTaskCount > 0) { setTimeout(() => doExit(), 10 * 1000); } else { doExit(); } };
Ordungsgemäßes Beenden des Prozesses
Beim Neustart eines Webservers oder bei der Behandlung der Laufzeitcontainerplanung (PM2, Docker usw.) möchten wir:
- Laufende Anfragen abschließen.
- Datenbankverbindungen bereinigen.
- Fehler protokollieren und Warnungen auslösen.
- Andere notwendige Herunterfahrvorgänge durchführen.
Verwenden des Exit-Hook-Tools:
const http = require('http'); const server = http .createServer((req, res) => { res.writeHead(200); res.end('Hallo Welt\n'); }) .listen(8000); addExitTask((error, callback) => { console.log('Prozess wird aufgrund eines Fehlers beendet:', error); server.close(() => { console.log('Annahme neuer Anfragen wurde gestoppt.'); setTimeout(callback, 5000); }); });
Fazit
Indem wir die verschiedenen Szenarien verstehen, die zum Beenden von Node.js-Prozessen führen, können wir abnormale oder geplante Beendigungen proaktiv erkennen und behandeln. Während Tools wie Kubernetes und PM2 abgestürzte Prozesse neu starten können, können wir durch die Implementierung der Überwachung im Code Probleme früher erkennen und beheben.
Wir sind Leapcell, Ihre erste Wahl für das Hosten von Node.js-Projekten.
Leapcell ist die Next-Gen Serverless-Plattform für Webhosting, asynchrone Aufgaben und Redis:
Multi-Sprachen-Unterstützung
- Entwickeln Sie mit Node.js, Python, Go oder Rust.
Stellen Sie unbegrenzt viele Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Erkenntnisse.
Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Null Betriebsaufwand – konzentrieren Sie sich einfach auf den Aufbau.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ