Node.js プロセスの終了戦略:シグナル、エラー、およびグレースフルシャットダウン
Emily Parker
Product Engineer · Leapcell

背景紹介
サービスをデプロイした後、ランタイム環境(コンテナ、PM2など)によるスケジュール、サービスアップグレードによる再起動、または様々な例外によるプロセスのクラッシュは避けられません。一般的に、ランタイム環境にはサービスプロセス向けのヘルスモニタリングメカニズムがあります。プロセスがクラッシュすると、ランタイムはそれを再起動します。アップグレード中には、通常、ローリングアップグレード戦略が使用されます。ただし、ランタイム環境のスケジューリング戦略は、サービスプロセスをブラックボックスとして扱い、その内部状態を考慮しません。したがって、サービスプロセスは、ランタイム環境からのスケジューリングアクションを積極的に検出し、終了前に必要なクリーンアップアクションを実行する必要があります。
今回は、Node.jsプロセスが終了する可能性のあるさまざまなシナリオをまとめ、これらのプロセス終了イベントをリッスンすることで何ができるかについて説明します。
原則
プロセスは、次の2つの方法のいずれかで終了します。
- プロセスが自発的に終了する。
- プロセスが終了するように指示するシステムシグナルを受信する。
システムシグナルによる終了
Node.jsの公式ドキュメントには、一般的なシステムシグナルがリストされています。ここでは、次のものに焦点を当てます。
- SIGHUP: プロセスを停止するために
Ctrl + C
を使用する代わりに、ターミナルが直接閉じられたときにトリガーされます。 - SIGINT:
Ctrl + C
を押してプロセスを停止するときにトリガーされます。PM2は、再起動または停止時にこのシグナルを子プロセスにも送信します。 - SIGTERM: 通常、プロセスを正常に終了するために使用されます。たとえば、Kubernetesがポッドを削除すると、SIGTERMシグナルを送信して、ポッドがタイムアウト期間内(デフォルト:30秒)にクリーンアップアクションを実行できるようにします。
- SIGBREAK: Windowsシステムで
Ctrl + Break
が押されたときにトリガーされます。 - SIGKILL: プロセスを強制的に直ちに終了させ、クリーンアップアクションを防ぎます。
kill -9 pid
を実行すると、プロセスはこのシグナルを受信します。Kubernetesでは、ポッドが30秒のタイムアウト以内に終了しない場合、KubernetesはSIGKILLシグナルを送信して、それを直ちに終了させます。同様に、PM2は、再起動または終了中にプロセスが1.6秒以内に終了しない場合、SIGKILLを送信します。
強制的な終了シグナル以外の場合、Node.jsプロセスはこれらのシグナルをリッスンして、カスタムの終了動作を定義できます。たとえば、タスクの実行に時間がかかるCLIツールがある場合、Ctrl + C
が押されたときに終了する前にユーザーにプロンプトを表示できます。
const readline = require('readline'); process.on('SIGINT', () => { // readlineを使用したシンプルなコマンドラインインタラクション const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); rl.question('タスクはまだ完了していません。本当に終了しますか?', (answer) => { if (answer === 'yes') { console.log('タスクが中断されました。プロセスを終了します。'); process.exit(0); } else { console.log('タスクを続行します...'); } rl.close(); }); }); // 完了までに1分かかるタスクをシミュレート const longTimeTask = () => { console.log('タスクが開始されました...'); setTimeout(() => { console.log('タスクが完了しました。'); }, 1000 * 60); }; longTimeTask();
このスクリプトは、Ctrl + C
が押されるたびにプロンプトを表示します。
タスクはまだ完了していません。本当に終了しますか? no
タスクを続行します...
タスクはまだ完了していません。本当に終了しますか? no
タスクを続行します...
タスクはまだ完了していません。本当に終了しますか? yes
タスクが中断されました。プロセスを終了します。
自発的なプロセス終了
Node.jsプロセスは、次のシナリオにより自発的に終了する可能性があります。
- 実行中にキャッチされないエラーが発生した場合(
process.on('uncaughtException')
を使用してキャプチャ可能)。 - 未処理のPromiseリジェクションが発生した場合(Node.js v16以降、未処理のリジェクションはプロセスを終了させます。
process.on('unhandledRejection')
を使用して処理します)。 EventEmitter
によってerror
イベントが発行されたが、処理されない場合。- プロセスが明示的に
process.exit()
を呼び出す場合。 - Node.jsイベントループが空の場合(つまり、保留中のタスクがない場合。
process.on('exit')
を使用して検出可能)。
PM2には、サービスがクラッシュした場合に再起動するデーモンプロセスがあります。clusterモジュールを使用して、Node.jsで同様の自己修復メカニズムを実装できます。ワーカープロセスがクラッシュした場合に自動的に再起動されます。
const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; const process = require('process'); if (cluster.isMaster) { console.log(`マスタープロセスが開始されました:${process.pid}`); // CPUコアの数に基づいてワーカープロセスを作成 for (let i = 0; i < numCPUs; i++) { cluster.fork(); } // ワーカーの終了イベントをリッスン cluster.on('exit', (worker, code, signal) => { console.log(`ワーカー ${worker.process.pid} がコード:${code || signal} で終了しました。再起動中...`); cluster.fork(); }); } if (cluster.isWorker) { process.on('uncaughtException', (error) => { console.log(`ワーカー ${process.pid} でエラーが発生しました`, error); process.emit('disconnect'); process.exit(1); }); // HTTPサーバーを作成 http .createServer((req, res) => { res.writeHead(200); res.end('Hello world\n'); }) .listen(8000); console.log(`ワーカープロセスが開始されました:${process.pid}`); }
実践的な実装
Node.jsプロセスが終了する可能性のあるさまざまなシナリオを分析したので、ユーザーがカスタムの終了動作を定義できるプロセス終了リスナーを実装しましょう。
// exit-hook.js const tasks = []; const addExitTask = (fn) => tasks.push(fn); const handleExit = (code, error) => { // 実装の詳細については、以下で説明します }; 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));
handleExit
の場合、process.nextTick()
を使用して、同期タスクと非同期タスクの両方が適切に処理されるようにします。
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(); } };
グレースフルなプロセス終了
Webサーバーを再起動したり、ランタイムコンテナのスケジューリング(PM2、Dockerなど)を処理したりする場合、次のことを行いたいと考えます。
- 進行中のリクエストを完了します。
- データベース接続をクリーンアップします。
- エラーをログに記録し、アラートをトリガーします。
- その他の必要なシャットダウンアクションを実行します。
exit-hookツールを使用します。
const http = require('http'); const server = http .createServer((req, res) => { res.writeHead(200); res.end('Hello world\n'); }) .listen(8000); addExitTask((error, callback) => { console.log('エラーによるプロセスの終了:', error); server.close(() => { console.log('新しいリクエストの受付を停止しました。'); setTimeout(callback, 5000); }); });
結論
Node.jsプロセスが終了するさまざまなシナリオを理解することで、異常な終了またはスケジュールされた終了を事前に検出し、処理できます。KubernetesやPM2などのツールは、クラッシュしたプロセスを再起動できますが、コード内にモニタリングを実装することで、問題をより早く検出して解決できます。
私たちはLeapcellです。Node.jsプロジェクトをホストするための最適な選択肢です。
Leapcellは、Webホスティング、非同期タスク、およびRedis向けの次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、または Rust で開発します。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い - リクエストも料金もありません。
比類のないコスト効率
- アイドル料金なしの従量課金制。
- 例: $25 で平均応答時間 60 ミリ秒で 694 万件のリクエストをサポート。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的な UI。
- 完全に自動化された CI/CD パイプラインと GitOps の統合。
- 実用的な洞察を得るためのリアルタイムのメトリクスとログ。
簡単なスケーラビリティと高性能
- 高い同時実行性を容易に処理するための自動スケーリング。
- 運用上のオーバーヘッドはゼロ - 構築に集中するだけです。
詳細については、ドキュメントをご覧ください。
X でフォローしてください: @LeapcellHQ