- グレースフルシャットダウン (graceful shutdown) は、アプリケーションが終了シグナルを受け取った後に、新規リクエストの受付停止、現在処理中のリクエストの完了、リソースのクリーンアップを行う手順で構成される
- Goでは
os/signal パッケージを使って SIGINT、SIGTERM などの終了シグナルを直接処理でき、signal.NotifyContext を使えば context ベースの終了制御も可能
- HTTPサーバー終了時には、
Server.Shutdown() を呼ぶ前に readiness probe を失敗させてトラフィックを遮断し、数秒待ってから shutdown を実行するのが安定的
- すべてのハンドラーは context の終了シグナルを検知して終了できる必要があり、
BaseContext または middleware を通じてこれを統合的に処理できる
- 終了シグナル受信後は データベース、メッセージブローカー、キャッシュなどの外部リソースを意図的にクリーンアップする必要があり、
defer に登録しておくと終了順序の管理がしやすい
Graceful Shutdownとは?
- グレースフルシャットダウンは、アプリケーション終了時に 新規リクエストの遮断、進行中リクエストの完了待機、リソースのクリーンアップを行うプロセス
- この記事は主に HTTPサーバーとコンテナ環境を扱うが、すべてのアプリケーションに適用できる概念
1. 終了シグナルの処理
- Unix系システムでは SIGTERM、SIGINT、SIGHUP などが終了シグナルとして使われる
- Goランタイムは
SIGTERM、SIGINT を受信するとデフォルトでアプリケーションを終了するが、os/signal.Notify で直接処理できる
- バッファ付きチャネル(容量 1) を使うと、初期化中のシグナル取りこぼしを防げる
- Go 1.16 以降では
signal.NotifyContext を使うことで、context ベースのシグナル制御が簡単になる
2. 終了時間の認識
- Kubernetes ではデフォルトで 30秒の終了猶予期間 が与えられる (
terminationGracePeriodSeconds)
- 安全に終了するには、20% の余裕を持たせて 25 秒以内に終了処理を完了させるのが望ましい
3. 新規リクエストの受付停止
http.Server.Shutdown() は 新しい接続を遮断し、既存リクエストが完了するまで待機する
- Kubernetes 環境では readiness probe を先に失敗させて トラフィック流入を遮断してから少し待ち、その後 shutdown を実行する
- readiness ハンドラーではグローバル変数で終了状態を判定し、HTTP 503 を返すように構成できる
4. リクエスト処理の完了
- 終了のための context には 適切な timeout の設定 が必要 (
context.WithTimeout)
- shutdown context が期限切れになると、残っている接続は強制終了される
- すべてのハンドラーは
context.Context を活用して 終了シグナルを検知し、中断できるよう設計する必要がある
- そのために middleware や
BaseContext を通じて すべてのリクエストに終了 context を注入できる
5. リソースのクリーンアップ
- 終了シグナルを受けたからといってすぐにリソースを閉じると、処理中のハンドラーで問題が発生する可能性がある
- shutdown 完了後に データベース接続、メッセージブローカー、キャッシュなどをクリーンアップすべき
- Go の
defer を活用すると、初期化の逆順で終了ルーチンを実行できるため依存関係の管理がしやすい
- メモリやファイルディスクリプタなど OS が自動的にクリーンアップするリソース以外にも、データの flush、トランザクションの rollback など明示的な終了が必要なリソースが存在する
全体例の要約
signal.NotifyContext で終了シグナルを受信
/healthz readiness エンドポイントを実装
BaseContext ですべてのリクエストに終了 context を注入
- readiness を 5 秒待ってから shutdown を実行
server.Shutdown の呼び出し失敗時には強制終了のフォールバックも含む
参考文献と関連リソース
1件のコメント
Hacker Newsの意見
Kubernetesでは、ロードバランサーのターゲットIP更新に時間がかかることがある。問題の90%は、トラフィックが実際にドレインされているかを確認することにある
log.Fatalを使うと、defer内の内容は実行されないlog.Fatalはos.Exitを呼び出して即座に終了するpanicを使えばdeferの内容は実行されるPrometheus の
/metricsエンドポイントが定期的にスクレイプされる場合、最後のスクレイプとプロセス終了の間に記録されたメトリクスが伝播されない可能性がある分散システムがクライアントの正常終了に依存すると、システムが深刻に故障する可能性がある
新しいサービスインスタンスが以前のインスタンスからソケットを受け取ることで、接続を切らずにアプリケーションを再起動する方法についての説明が不足している
liveness に関する議論が不足している
プログラムが ctrl c のようなコマンドをきれいに処理できないなら、適切に書かれていないということだ
Elixir はプロセスを小さな VM プロセスとして設計しており、正常終了ルーチンを意図的に作る必要がないようにしている
プロジェクトで正常終了を扱うための小さなライブラリを作った
readiness probe を更新した後、数秒待ってシステムが新しいリクエストを送らないようにする