【解説付き】Node.js + ExpressのAPIサーバをgraceful shutdownする【実装例付き】

## はじめに

こんにちは！Gopherくん大好きマンの[@o_ga09](https://x.com/o_ga09)です！

## TL;DR

- この記事を読むと
    - Graceful Shutdownを理解できる
    - Graceful Shutdownを実装しないと起き得るエラーを理解できる
    - Graceful Shutdownを実装できるようになる

## 対象読者

- バックエンドエンジニア
- コンピュータにおけるプロセス・スレッドが理解している
- 何かしらの言語でAPIサーバの実装経験がある
- Dockerを使用したことがある

## 免責事項

本記事における実装はサンプルです。本記事を参考にして実装されたコードで起こる障害に関して筆者はいかなる責任も負いかねます。

## 目次

- [事象](#事象)
- [原因](#原因)
- [アーキテクチャ](#アーキテクチャ)
- [1. Graceful Shutdownとは](#1-graceful-shutdownとは)
- [2.OSのシグナルの種類](#2-osのシグナルの種類)
- [3.ローカルでGraceful Shutdownする](#3-ローカルでgraceful-shutdownする)
- [4.Dockerコンテナ環境でGraceful Shutdownする](#4-dockerコンテナ環境でgraceful-shutdownする)
- [5. Node.js ✖️ Dockerコンテナにおける問題点](#5-nodejs-%EF%B8%8F-dockerコンテナにおける問題点)
- [6. Graceful Shutdownを考慮しないと起き得るエラー](#6-graceful-shutdownを考慮しないと起き得るエラー)
- [7. 実装例](#7-実装例)
- [8. 動作確認](#8-動作確認)
- [まとめ](#まとめ)
- [さいごに](#さいごに)

## 事象

リリース時のECSタスクが置き換わったタイミングで以下のエラーが毎回発生する。

```bash:Auroraエラーログ
[Warning] Aborted connection xxx to db: '<database name>' user: '<user name>' host: '<host IP>' (Got an error reading communication packets)
```

実害はないが、リリース後に毎回発生するのはドキドキする。

## 原因

ECS on Fargate のローリングアップデート時、コンテナがスケールインする際にDB切断処理が行われていないため。

以下公式の回答

https://repost.aws/ja/knowledge-center/rds-mysql-communication-packet-error

Dockerなどのコンテナ仮想化エンジンは、通常、コンテナを停止させる際に`SIGTERM`というプロセスシグナルを送信して停止しますが、それでも停止できない場合に`SIGKILL`というプロセスシグナルを送信することで強制的にコンテナを停止させます。

大体の場合は、`SIGTERM`というプロセスシグナルで停止可能なので問題ないのですが、
今回のように**Node.js**の場合は、`PID:1`問題([後述](#5-nodejs-%EF%B8%8F-dockerコンテナにおける問題点))があり、`SIGTERM`というプロセスシグナルで停止できないのです。

なので、今回は明示的にサーバ終了処理をいれて適切にコンテナを停止できるようにしましたというお話です。

## アーキテクチャ

ユーザーの窓口に、<font color="red">**APIGateway**</font>
コンピューティングに、<font color="orange">**ECS on Fargate**</font>
DBに、<font color="blue">**AmazonAurora for MySQL**</font>

を使用したごくシンプルなサーバレス構成

![achitecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512616/66df9868-6fd7-603a-adce-29f79c679d92.png)

## 1. Graceful Shutdownとは？

そもそも、APIサーバをシャットダウンさせる方法は、大き分けて2つあります。

 - グレースフルシャットダウン (Graceful Shutdown)
 - 即時/強制シャットダウン (Immediate/forceful Shutdown)

 これら2つにはどのような違いあるのでしょうか？また、それぞれにはどのようなメリット／デメリットがあるのでしょうか？

**グレースフルシャットダウン (Graceful Shutdown)**

- 進行中のリクエストを完了させてから、サーバーを停止します
- 新しいリクエストは受け付けません
- リソースを適切に解放し、データの整合性を維持します

**即時/強制シャットダウン (Immediate/forceful Shutdown)**

- サーバーを直ちに停止します
- 進行中のリクエストは中断されます
- データの整合性が損なわれる可能性

**メリット／デメリット**

|比較項目|グレースフルシャットダウン|即時/強制シャットダウン|
|---|---|---|
|DB接続|セッション正規の手順で切る|セッションが残る可能性が高い|
|処理中のリクエスト|処理を必ず完了させる|処理が完了する保証はない|
|新規のリクエスト|受けつない|受け付ける|
|プロセスの扱い|正常終了|強制終了|

ただし、上記は一例であり、すべてこの通りというわけではなく、実装によるところもあります。ただ、あるべきとしてのメリット／デメリットを記載しております。

**図で言うとこんな感じ**

- グレースフルシャットダウン

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512616/b04fc566-878d-8dbf-ac13-5b74efad09f9.png)

- 即時/強制シャットダウン

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512616/f5cb3097-7c4a-0c9e-d46f-029bbd1369ee.png)


## 2. OSのシグナルの種類

`kill -l`をLinuxもしくはMacのターミナル上で実行すると、使用可能なシグナルの一覧が表示される。

Mac上のターミナルで実行

```bash
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL
 5) SIGTRAP	 6) SIGABRT	 7) SIGEMT	 8) SIGFPE
 9) SIGKILL	10) SIGBUS	11) SIGSEGV	12) SIGSYS
13) SIGPIPE	14) SIGALRM	15) SIGTERM	16) SIGURG
17) SIGSTOP	18) SIGTSTP	19) SIGCONT	20) SIGCHLD
21) SIGTTIN	22) SIGTTOU	23) SIGIO	24) SIGXCPU
25) SIGXFSZ	26) SIGVTALRM	27) SIGPROF	28) SIGWINCH
29) SIGINFO	30) SIGUSR1	31) SIGUSR2
```

いろいろ、種類はありますがよく使用されるのはせいぜい３〜４種類くらい（体感です）

|No|シグナル|用途|
|---|---|---|
|15|SIGTERM|プロセスに停止命令を送る|
|9|SIGKILL|プロセスを強制終了する|
|2|SIGINT|プロセスへの割り込み命令を送る|
|1|SIGHUP|プロセスをハングアップさせる|

ちなみに、Dockerコンテナを停止させる`docker stop`コマンドを実行するとDockerから内部で動作しているアプリケーションのプロセスにまず、SIGTERMが送信されて停止されます。また、SIGTERM命令で一定時間後に停止できなかった場合、SIGKILL命令で強制終了されます。

そのため、アプリケーションをDockerコンテナで動作させる際は、なるべくグレースフルシャットダウンさせないと、もし、なんらかの原因でSIGTERMによる停止ができなかった場合、強制終了させられるので不都合が起き得る可能性が高まります。

## 3. ローカルでGraceful Shutdownする

実際にグレースフルシャットダウンを実装したサーバを起動してみましょう。

![スクリーンショット 2024-07-03 20.36.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512616/a453571e-d3c7-8af9-881e-a5dc2ac7454c.png)

この通り、ローカルでサーバを立てて`kill`コマンドで`15`(SIGTERM)のシグナルを送ってみるとサーバが`{"time":"2024-07-03T11:36:18.176Z","severity":"Info","message":"SIGTERM signal received."}`というログを出してシャットダウンしてくていることが分かります。

また、連続サーバを起動してもポートの解放などの処理が行われているので正常に起動できます。

![スクリーンショット 2024-07-03 20.40.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512616/e92e70fb-3402-d5a6-c803-98603649345d.png)

ちなみに、`kill -9`で強制終了した場合は、ポートの解放が行われずにサーバが異常終了するため連続での起動はできません。（ローカルの場合）

![スクリーンショット 2024-07-03 20.43.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512616/16e1096b-1500-fff3-3020-c269da2ac22f.png)

## 4. Dockerコンテナ環境でGraceful Shutdownする

今度は、Docker環境でグレースフルシャットダウンを試してみましょう。

![スクリーンショット 2024-07-03 21.16.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512616/bd40932a-79d3-3071-3bc4-f8aa33717437.png)

このように、正常にグレースフルシャットダウンができました。

## 5. Node.js ✖️ Dockerコンテナにおける問題点

ただし、Node.jsのアプリケーションをDocker化する際には気をつけなければならないことがあります。
それは、Node.jsのアプリケーションのPIDが1になってしまう問題です。

**なぜ、PIDが1だといけないのか？**

多くの場合、dockerコンテナのベースイメージとしてLinuxを選択するかと思います。Linuxにおいて`PID 1`というのはinitプロセスであり、カーネルから起動される親プロセスのようなものです。そのため、rootでシグナルを送っても`kill`することができないのです。

また、node.js公式のリポジトリで言及されている通り、node.jsは`PID 1`で動作するように設計されていないためシグナルを送っても応答がないのです。

![スクリーンショット 2024-07-03 21.30.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512616/75269114-b899-52a9-08b2-3ceac8852b54.png)

`kill`コマンドで`SIGTERM`のシグナルを送ってもサーバが起動している。

### 参考

https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md

https://ngzm.hateblo.jp/entry/2017/08/22/185224

## 6. Graceful Shutdownを考慮しないと起き得るエラー

一例ですが、簡単に挙げてみたいと思います。

- DBのセッションを接続したままにしてしまうことによる、DBからの`Aborted connection`エラー
- ポートが正常に解放されないことによるエラー
- データ更新リクエストの途中で、終了したことによるデータの不整合

こんな感じで、上げ出したらきりがないほど影響はあるかなと思います。
まぁ、ただし、起きる起きないは運次第みたいなところが、あるのも事実な気がするので、実装しておいた方が無難かなくらいに捉えていただければと思います。

## 7. 実装例

ここからは、簡単に実装例を紹介します。

```javascript:app.ts
import express from 'express';
import { HealthCheckController } from './controller/system';
import { usecase } from '../DI/container';
import { TaskController } from './controller/task';
import { logger } from '../middleware/logger';

export class Server {
  readonly app = express();
  readonly HealthCheckHandler = express.Router();
  readonly TaskHandler = express.Router();
  readonly apiRouter = express.Router();
  readonly r = new HealthCheckController();
  readonly t = new TaskController(usecase);
  
  constructor() {
    this.apiRouter.use('/', this.HealthCheckHandler);
    this.apiRouter.use('/tasks', this.TaskHandler);
  }
  
  Run() {
    
    // GET リクエスト
    this.HealthCheckHandler.get('/', this.r.healthCheck);
    this.TaskHandler.get('/', this.t.getAllTasks.bind(this.t));

    // パスパラメータを取得する
    this.TaskHandler.get('/:id', this.t.getById.bind(this.t));

    // POST リクエスト
    this.TaskHandler.post('/', this.t.CreateTask.bind(this.t));

    // PUT リクエスト
    this.TaskHandler.put('/:id', this.t.UpdateTask.bind(this.t));

    // DELETE リクエスト
    this.TaskHandler.delete('/:id', this.t.DeleteTask.bind(this.t));

    this.app.use(express.json());
    this.app.use('/api/v1', this.apiRouter);
    // サーバ起動時にapp.listenの戻り値を格納しておく
    const server = this.app.listen(8080, () => {
      logger.info('starting server :8080');
    });

    // ここ重要！！！
    process.on('SIGTERM', () => {
    　// サーバをシャットダウンする
      server.close(() => {
        // シャットダウン時の処理を実装する
        db.close();
        logger.info('SIGTERM signal received.');
      });
    });
  }
}

```

実装例は、以上！！！

簡単ですよね！！！

ほんの少しだけ解説すると、

```javascript
process.on('SIGTERM', () => {
```

で、シグナルの受信をして、そのコールバック関数内で、

```javascript
server.close(() => {
    // シャットダウン時の処理を実装する
    db.close();
    logger.info('SIGTERM signal received.');
});
```

また、あらかじめサーバ起動時に、サーバの起動時のデータを`server`変数に持っておきます

```javascript
const server = this.app.listen(8080, () => {
    logger.info('starting server :8080');
});
```

こうすることで、後からサーバのシャットダウン処理をすることができます。

サーバのシャットダウンやDBの接続の終了処理やロギングを行います。

## 8. 動作確認

この実装をすることで、以下のように`SIGTERM signal received.`というログを出力して、「`SIGTERM`のシグナルを受け取りましたサーバをシャットダウンします。」となるのです。

```json
{"time":"2024-07-03T11:36:18.176Z","severity":"Info","message":"SIGTERM signal received."}
```

## まとめ

このNode.jsのアプリケーションをDockerコンテナ上で起動すると`PID 1`となってしまう事象はかなり有名ですが今回、改めて自分への戒めも含めで記事にしてみました。よろしければ、いいねをよろしくお願いいたします。

## さいごに

 弊社では、エンジニア積極採用中です！
SES、請負、受託、Saleforce やりたい方はご興味を持っていただけたら幸いです。

ワクトでは、以下の Mission・Vision・Value を掲げております。

Mission : 「IT× ワクワク」で、社会の発展に貢献する
Vision : 大切な人に心から薦めたい会社であり続ける
Value :
One team for customers
熱意 × 人格 × 能力
まずやってみる
ワクトでは、もう一つ重要なものとして「 マインドマップ 」というものがあります。
個人的にですが、こちらに共感していただけた方はワクトに合うかなと思っております。

![MindMap](https://camo.qiitausercontent.com/d4bc1bad02e4881409ad684cdf3e1fa4b25b2a6c/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f333531323631362f37636461653334372d626230632d393532372d656430382d3164393463376630333337652e706e67)


https://wakuto.net/

https://www.wantedly.com/companies/wakuto0910

https://twitter.com/wakuwaku_toyou

また、本記事の内容は個人の考えであり、会社を代表するものではございません。