こんにちは、ヒロです。
世の中には色々な言語やフレームワークで作成されたサービスが数多くありますが、初期化処理に気を使っていても終了処理には気を使っていないサービスが割と多く見受けられます。
もちろんフレームワークが勝手にやってくれるため気を使う必要がないものや、運用で回避しているケースもあるかと思いますが、今回は特にnode.jsでの終了処理に焦点を当ててみました。
そもそもサービスの安全な停止とは何なのか?
サービス停止指示後に、残った処理中の接続が完了するのを待ってから、プロセスを安全に停止する方法のことを一般的にGraceful Shutdownと呼びます。
例えばExcelを編集中にPCの電源を引っこ抜くと、折角編集したデータが未保存分は全て失われてしまいます。
最悪のケースは保存中の強制終了でデータが破損してファイルが開けなくなってしまいます。
サービスも同様で、処理中に強制終了されるとデータの整合性が取れなくなったり、ファイルが破損してしまう可能性があります。
これらを解決するためにLinuxのデーモンやWindowsのサービスには、OSからサービスへサービスの停止を知らせる仕組みがあります。
node.jsで動くサービスを安全に停止する
Linuxでデーモンを停止する際に使用されるのがSIGNALという仕組みです。
デーモンで動くサービスに対してプロセスが終了しますよ、プロセスを強制終了しますよ、という信号が送られます。
以下は、サービスの終了・停止に関するSIGNALの一例です。
SIGNAL | 動作 | コメント |
---|---|---|
SIGINT | Term | キーボードからの割り込み(CTRL+C) |
SIGTERM | Term | 終了 (termination) シグナル |
SIGKILL | Term | Kill シグナル |
SIGSTOP | Stop | プロセスの一時停止 (stop) |
※シグナル SIGKILL と SIGSTOP はキャッチ、ブロック、無視できない。
Linuxでnode.jsのサービスを実装する場合は、このSIGNALの仕組みを考慮して終了処理を実装すればGraceful Shutdownを実現可能です。
但し、SIGTERM送信後にプロセスが終了せず30秒のタイムアウトが発生するとSIGKILLが送信されるので、いつまでも終了処理を続けることができない点には注意が必要です。
LinuxでのGraceful Shutdown実装例
index.ts
process.on('SIGTERM', () => {
// Socketのcloseやループ処理の中断用メソッド呼び出しなどサービスの終了処理
});
ちなみにCTR+Cで終了されるようなコンソールアプリの場合は、Linux・Windows共通で以下の処理でGraceful Shutdownが行えます
process.stdin.resume();
process.on('SIGINT', () => {
// Socketのcloseやループ処理の中断用メソッド呼び出しなどサービスの終了処理
});
※process.stdin.resume()は標準入力の取得処理です。
WindowsでのGraceful Shutdown
問題はWindwosで動くnode.jsサービスのGraceful Shutdownです。
node.jsで作成したプログラムをWindowsのサービスとして登録するのはそんなに難しくありませんが、単純にWindowsのサービスとして登録してもサービス停止のためのSIGNALは送られてきません。
ですから前述のロジックでは、Windowsがサービスを停止させる際に終了処理を通すことができません。
解決方法はいくつか存在します。
- .NetなどのWindows用言語でラッパーサービスを作成し、そこからnode.jsのコントロールを行う
- Windowsのタスクスケジューラーで起動し、Windsows終了時にタスクスケジューラーで起動したサービスにCTRL+Cなどのコマンドを送る
- Windows上でDockerを起動し、コンテナ内でnode.jsのプログラムをデーモン起動する
- Windowsのサービス利用をあきらめて別のサービスを使用する
PM2とは
今回はできるだけWindowsやDockerに頼らずに別のサービスを利用する方法を取ることにしました。
使用するのはPM2という割とメジャーなアプリケーションです。PM2はNode.jsのプロセス管理を行うためのオープンソースで、Node.jsのアプリケーションごとの起動停止管理やログ管理、リーソースのモニタリングができます。
LinuxだけでなくWindowsにも対応しており、且つPM2から起動したアプリケーションに対してGracefull Shutdownを行う仕組みを持っています。
PM2のインストールとnode.jsアプリケーションの登録
PM2は以下のコマンドでnpmからインストールが可能です。
npm install -g pm2
PM2にnode.jsで作成したアプリケーションを登録するには、pm2コマンドで管理したいアプリケーションを直接指定するか、ECOSYSTEM FILEを利用するかを選択できます。
基本的にやっていることは同じですが、複数のアプリケーションを一括して管理する場合などはECOSYSTEM FILEを利用した方が管理がしやすいと思います。
ECOSYSTEM FILEを利用する場合はecosystem.config.jsというファイルを利用してPM2にアプリケーション情報を伝えます。
ecosystem.config.js
module.exports = {
apps : [
{
name: 'アプリケーション名',
script: './main.js',
kill_timeout : 30000,
shutdown_with_message: true
}
]
};
※WindowsでGracefull Shutdownを利用する場合はshutdown_with_message: true
の設定が必要になります。
ECOSYSTEM FILEを利用してPM2にアプリケーションを登録するには以下のコマンドを実行します。
pm2 start ecosystem.config.js
このままではpm2を再起動するとアプリケーション情報が保存されずにクリアされてしまいますので、以下のコマンドを実行してアプリケーション情報を保存します。
pm2 save
これでPM2にnode.jsのアプリケーション登録が完了したのですが、サービスとして動かすにはもう少し手順が必要となります。
PM2はWindows環境では自動起動してくれませんので、PM2をサービスとして自動起動してくれるpm2-windows-startupをインストールして登録します。
npm install -g pm2-windows-startup & pm2-startup install
これでPM2の自動起動及びPM2からのnode.jsアプリケーションの自動起動登録が完了しました。
PM2で管理されるnode.jsアプリケーションのGracefull Shutdown設定
PM2のGracefull Shutdownの仕組みもLinuxとWindowsでは若干動きが異なります。
Linuxであれば前述のSIGNALの仕組みが利用されSIGINT(CTRL+C)がアプリケーションに対して送信されて来ますが、Windowsの場合はnode.jsのプログラムに少しロジックを追加する必要があります。
PM2の公式ドキュメントにはprocess.on('message', (msg) => {
を使用するように記述があります。
ドキュメントを読んで最初に追加したロジックが以下となります。
process.on('message', () => {
server.close(() => {
logger.info('Server has been shutdown.');
process.exit(0);
});
});
ところがこのロジックで何度終了処理を行ってみてもServer has been shutdown.
のログが出力されません。
小一時間悩んだ結果、もう一度公式ドキュメントを見てみると
console.log('Closing all connections...')
setTimeout(() => {
console.log('Finished closing connections')
process.exit(0)
}, 1500)
1500msec待ってからprocess.exit(0)
しています。
そうです、process.exit(0)
は処理を待たずにプログラムを終了してしまうため、必要な場合はプログラム側でwaitを掛けて待ってあげないとプログラムは即時終了してしまいます。
非同期処理のlogger.infoではファイルへの書き込みが終了する前にプログラム自体が終了してしまうのです。
最終的に以下のようにプログラムを修正することでアプリケーション終了のログも正常に出力されるようになりました。
process.on('message', () => {
server.close(() => {
logger.info('Server has been shutdown.');
setTimeout(() => {
process.exit(0);
}, 1000);
});
});
Windowsでnode.jsのGracefull Shutdownを実現するのは、なかなか骨が折れる作業でした。