はじめに
こんにちは!Gopherくん大好きマンの@o_ga09です!
TL;DR
- この記事を読むと
- Graceful Shutdownを理解できる
- Graceful Shutdownを実装しないと起き得るエラーを理解できる
- Graceful Shutdownを実装できるようになる
対象読者
- バックエンドエンジニア
- コンピュータにおけるプロセス・スレッドが理解している
- 何かしらの言語でAPIサーバの実装経験がある
- Dockerを使用したことがある
免責事項
本記事における実装はサンプルです。本記事を参考にして実装されたコードで起こる障害に関して筆者はいかなる責任も負いかねます。
目次
- 事象
- 原因
- アーキテクチャ
- 1. Graceful Shutdownとは
- 2.OSのシグナルの種類
- 3.ローカルでGraceful Shutdownする
- 4.Dockerコンテナ環境でGraceful Shutdownする
- 5. Node.js ✖️ Dockerコンテナにおける問題点
- 6. Graceful Shutdownを考慮しないと起き得るエラー
- 7. 実装例
- 8. 動作確認
- まとめ
- さいごに
事象
リリース時のECSタスクが置き換わったタイミングで以下のエラーが毎回発生する。
[Warning] Aborted connection xxx to db: '<database name>' user: '<user name>' host: '<host IP>' (Got an error reading communication packets)
実害はないが、リリース後に毎回発生するのはドキドキする。
原因
ECS on Fargate のローリングアップデート時、コンテナがスケールインする際にDB切断処理が行われていないため。
以下公式の回答
Dockerなどのコンテナ仮想化エンジンは、通常、コンテナを停止させる際にSIGTERM
というプロセスシグナルを送信して停止しますが、それでも停止できない場合にSIGKILL
というプロセスシグナルを送信することで強制的にコンテナを停止させます。
大体の場合は、SIGTERM
というプロセスシグナルで停止可能なので問題ないのですが、
今回のようにNode.jsの場合は、PID:1
問題(後述)があり、SIGTERM
というプロセスシグナルで停止できないのです。
なので、今回は明示的にサーバ終了処理をいれて適切にコンテナを停止できるようにしましたというお話です。
アーキテクチャ
ユーザーの窓口に、APIGateway
コンピューティングに、ECS on Fargate
DBに、AmazonAurora for MySQL
を使用したごくシンプルなサーバレス構成
1. Graceful Shutdownとは?
そもそも、APIサーバをシャットダウンさせる方法は、大き分けて2つあります。
- グレースフルシャットダウン (Graceful Shutdown)
- 即時/強制シャットダウン (Immediate/forceful Shutdown)
これら2つにはどのような違いあるのでしょうか?また、それぞれにはどのようなメリット/デメリットがあるのでしょうか?
グレースフルシャットダウン (Graceful Shutdown)
- 進行中のリクエストを完了させてから、サーバーを停止します
- 新しいリクエストは受け付けません
- リソースを適切に解放し、データの整合性を維持します
即時/強制シャットダウン (Immediate/forceful Shutdown)
- サーバーを直ちに停止します
- 進行中のリクエストは中断されます
- データの整合性が損なわれる可能性
メリット/デメリット
比較項目 | グレースフルシャットダウン | 即時/強制シャットダウン |
---|---|---|
DB接続 | セッション正規の手順で切る | セッションが残る可能性が高い |
処理中のリクエスト | 処理を必ず完了させる | 処理が完了する保証はない |
新規のリクエスト | 受けつない | 受け付ける |
プロセスの扱い | 正常終了 | 強制終了 |
ただし、上記は一例であり、すべてこの通りというわけではなく、実装によるところもあります。ただ、あるべきとしてのメリット/デメリットを記載しております。
図で言うとこんな感じ
- グレースフルシャットダウン
- 即時/強制シャットダウン
2. OSのシグナルの種類
kill -l
をLinuxもしくはMacのターミナル上で実行すると、使用可能なシグナルの一覧が表示される。
Mac上のターミナルで実行
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
いろいろ、種類はありますがよく使用されるのはせいぜい3〜4種類くらい(体感です)
No | シグナル | 用途 |
---|---|---|
15 | SIGTERM | プロセスに停止命令を送る |
9 | SIGKILL | プロセスを強制終了する |
2 | SIGINT | プロセスへの割り込み命令を送る |
1 | SIGHUP | プロセスをハングアップさせる |
ちなみに、Dockerコンテナを停止させるdocker stop
コマンドを実行するとDockerから内部で動作しているアプリケーションのプロセスにまず、SIGTERMが送信されて停止されます。また、SIGTERM命令で一定時間後に停止できなかった場合、SIGKILL命令で強制終了されます。
そのため、アプリケーションをDockerコンテナで動作させる際は、なるべくグレースフルシャットダウンさせないと、もし、なんらかの原因でSIGTERMによる停止ができなかった場合、強制終了させられるので不都合が起き得る可能性が高まります。
3. ローカルでGraceful Shutdownする
実際にグレースフルシャットダウンを実装したサーバを起動してみましょう。
この通り、ローカルでサーバを立ててkill
コマンドで15
(SIGTERM)のシグナルを送ってみるとサーバが{"time":"2024-07-03T11:36:18.176Z","severity":"Info","message":"SIGTERM signal received."}
というログを出してシャットダウンしてくていることが分かります。
また、連続サーバを起動してもポートの解放などの処理が行われているので正常に起動できます。
ちなみに、kill -9
で強制終了した場合は、ポートの解放が行われずにサーバが異常終了するため連続での起動はできません。(ローカルの場合)
4. Dockerコンテナ環境でGraceful Shutdownする
今度は、Docker環境でグレースフルシャットダウンを試してみましょう。
このように、正常にグレースフルシャットダウンができました。
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
で動作するように設計されていないためシグナルを送っても応答がないのです。
kill
でシグナルを送ってもサーバが起動している。
参考
6. Graceful Shutdownを考慮しないと起き得るエラー
一例ですが、簡単に挙げてみたいと思います。
- DBのセッションを接続したままにしてしまうことによる、DBからの
Aborted connection
エラー - ポートが正常に解放されないことによるエラー
- データ更新リクエストの途中で、終了したことによるデータの不整合
こんな感じで、上げ出したらきりがないほど影響はあるかなと思います。
まぁ、ただし、起きる起きないは運次第みたいなところが、あるのも事実な気がするので、実装しておいた方が無難かなくらいに捉えていただければと思います。
7. 実装例
ここからは、簡単に実装例を紹介します。
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.');
});
});
}
}
実装例は、以上!!!
簡単ですよね!!!
ほんの少しだけ解説すると、
process.on('SIGTERM', () => {
で、シグナルの受信をして、そのコールバック関数内で、
server.close(() => {
// シャットダウン時の処理を実装する
db.close();
logger.info('SIGTERM signal received.');
});
また、あらかじめサーバ起動時に、サーバの起動時のデータをserver
変数に持っておきます
const server = this.app.listen(8080, () => {
logger.info('starting server :8080');
});
こうすることで、後からサーバのシャットダウン処理をすることができます。
サーバのシャットダウンやDBの接続の終了処理やロギングを行います。
8. 動作確認
この実装をすることで、以下のようにSIGTERM signal received.
というログを出力して、「SIGTERM
のシグナルを受け取りましたサーバをシャットダウンします。」となるのです。
{"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
熱意 × 人格 × 能力
まずやってみる
ワクトでは、もう一つ重要なものとして「 マインドマップ 」というものがあります。
個人的にですが、こちらに共感していただけた方はワクトに合うかなと思っております。
また、本記事の内容は個人の考えであり、会社を代表するものではございません。