コンテンツにスキップ

Node.jsのグレースフルシャットダウン#

Node.jsには統合されたWebサーバーの機能があります。さらにExpressを使用すると、これらの機能を拡張することができます。

残念ながら、Node.jsは自身のシャットダウンをうまく処理できません。これはコンテナ化されたシステムで多くの問題を引き起こします。最大の問題は、Node.jsのコンテナがシャットダウンするように指示されると、すべてのアクティブな接続を即座にを強制終了し適切に停止させることができないことです。

このパートでは、アクティブなリクエストを処理し終えてから、適切にシャットダウンするようにNode.jsを設定する方法を説明します。

例として、Expressを使用したシンプルなNode.jsサーバーを使用します:

app.js
const express = require('express');
const app = express();

// 全てのリクエストに5秒のディレイを追加。
app.use((req, res, next) => setTimeout(next, 5000));

app.get('/', function (req, res) {
  res.send("Hello World");
})

const server = app.listen(3000, function () {
  console.log('Example app listening on port 3000!');
})

これは、Webサーバーが localhost:3000 にアクセスされたときに "Hello World" を表示します。計算処理に時間がかかるリクエストをシミュレートするため、レスポンスに5秒の遅延があることに注意してください。

パートA: リクエストの完了を許可する#

上記の例を実行し、リクエストが処理されている間(5秒以内)にNode.jsプロセスを停止すると、Node.jsサーバーはすぐに接続を切断し、ブラウザはエラーを表示することになります。

Node.jsサーバーに、実際に自身を停止する前にすべてのリクエストが完了するのを待つべきだと伝えるために、以下のコードを追加します:

Graceful Shutdown
const startGracefulShutdown = () => {
  console.log('Starting shutdown of express...');
  server.close(function () {
    console.log('Express shut down.');
  });
}

process.on('SIGTERM', startGracefulShutdown);
process.on('SIGINT', startGracefulShutdown);

これは基本的に server.close() を呼び出し、これによりNode.js HTTPサーバーに対して以下の指示が出されます:

  1. これ以上のリクエストを受け付けない。
  2. すべての実行中のリクエストを完了する。

これは SIGINT ( CTRL + C を押したとき)または SIGTERM (プロセスを終了するための標準信号)で行います。

この小さな追加により、Node.jsはすべてのリクエストが完了するのを待つようになり、その後自身を停止します。

もし私たちがNode.jsをコンテナ化された環境で実行していないならば、おそらく 数秒後に実際にNode.jsサーバーを終了する追加のコードを含めたいと考えています。なぜなら、一部のリクエストが非常に長くかかるか、あるいは全く停止しない可能性があるからです。これはコンテナ化されたシステム内で実行されているため、コンテナが停止しない場合、DockerとKubernetesは数秒後(通常は30秒後)にSIGKILLを実行します。これはプロセス自体では処理できないため、私たちは気にする必用がありません。

パートB:YarnとNPMの子生成問題#

もし私たちがパートAだけを実装した場合、良い経験を得られるでしょう。現実の世界では、多くのNode.jsシステムがYarnやNPMで構築されており、これらはNode.jsにパッケージ管理システムだけでなく、スクリプト管理も提供します。

これらのスクリプト機能を使うと、アプリケーションの起動を簡単にすることができます。多くのpackage.jsonファイルが以下のようになっていることが分かります。

package.json
{
  "name": "node",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "express": "^4.15.3"
  },
  "scripts": {
    "start": "node index.js"
  }
}

そして、定義されたscriptsセクションを使って、以下のようにアプリケーションを起動するだけで済みます:

アプリケーションの起動
yarn start

または

アプリケーションの起動
npm start

これは便利で、開発者を楽にします。だから、私たちはDockerfilesの中でも同じものを使用しています。

.dockerfile
CMD ["yarn", "start"]

しかし、これには大きな問題があります:

yarnnpmSIGINTSIGTERM シグナルを受け取ると、それらは正確にシグナルを生成した子プロセスに転送します。(この場合は node index.js)しかし、子プロセスが停止するのを待つことはありません。代わりに、yarn/npm は即座に自分自身を停止します。これにより、Docker/Kubernetesに対してコンテナが完了したというシグナルが送られ、Docker/Kubernetesはすぐにすべての子プロセスを強制終了します。YarnNPMにはオープンな問題がありますが、残念ながらまだ解決されていません。

この問題の解決策は、YarnやNPMを使用してアプリケーションを開始するのではなく、直接 node を使用することです。

.dockerfile
CMD ["node", "index.js"]

これにより、Node.jsは適切に終了し、Docker/KubernetesはNode.jsが終了するのを待つことができます。