グレースフルシャットダウンとは?SIGTERM受信後の停止順序と猶予時間の設計を実装者目線で解説
デプロイのたびに数件だけ502が記録される、停止したはずのワーカーが処理途中のメッセージを取りこぼす。こうした症状の多くは、プロセスが停止要求を受け取ってから終了するまでの数十秒を設計していないことから起きます。グレースフルシャットダウンは、その数十秒を「受付停止・処理完了待ち・依存先切断」という順序に分解し、実行基盤が待ってくれる時間の内側に収める実装です。この記事では、停止処理の骨格、猶予時間の逆算、言語とコンテナ基盤ごとの設定、そして作り込みをどこで打ち切るかまでを扱います。
まとめ:グレースフルシャットダウンの成立条件と実装を進める順序
グレースフルシャットダウンとは、プロセスが停止要求を受け取った後、処理中の仕事を終わらせ、資源を解放してから自分で終了する仕組みを指します。成立の条件は3つです。停止要求のシグナルを受け取れること、新規の受付を止めても既存の処理を続けられること、そして一連の後始末が実行基盤の与える猶予時間内に終わること。どれか1つでも欠ければ、外から見た挙動は強制終了と変わりません。
実装の順序は逆算で決めます。まずロードバランサーが接続を引き剥がすまでの待ち時間を外枠に置き、その内側にプロセスの猶予時間、さらに内側に停止直前の待機を収める。Kubernetesの猶予は既定30秒、ECSのstopTimeoutは既定30秒で上限120秒という制約があるため、後始末に必要な時間の実測が先で、設定値はその実測から決める形になります。
判断を先に置きます。猶予時間を60秒より長く伸ばしたくなった時点で、それは停止処理の問題ではなく設計の問題です。長時間の処理を要求と同じプロセスで抱えている、外部への書き込みが冪等でない、といった構造を直す方が安く済みます。停止処理の作り込みは、検証で失敗要求がゼロになった時点で止めてください。
グレースフルシャットダウンの定義と強制終了との違いを整理する
言葉の指す範囲を先に固定します。ここを曖昧にしたまま設定値だけを触ると、直したつもりで症状が残ります。
SIGTERMとSIGKILLの役割分担と猶予が尽きた後に起きること
プロセスの停止は二段構えです。まずSIGTERMが送られ、プロセス自身が終了処理を実行する機会を与えられる。決められた猶予時間を過ぎても終了しない場合に、SIGKILLが送られて強制的に停止させられます。
両者の差は、プロセス側が受け取って処理できるかどうかにあります。SIGTERMはハンドラを登録して挙動を差し替えられる一方、SIGKILLはカーネルが直接プロセスを消すためハンドラを書けません。つまりグレースフルシャットダウンとは、SIGTERMを受け取ってからSIGKILLが来るまでの時間を使い切って後始末を終える設計そのものを指します。
停止処理を書いていないプロセスが処理中の要求を捨てる仕組みと原因
ハンドラを登録していないプロセスは、SIGTERMの既定動作である即時終了に従います。このとき、応答を生成している途中のリクエストは送信されず、クライアント側にはコネクションの切断として観測される。ロードバランサーを挟んでいれば502や504として記録されます。
取りこぼしはHTTPだけではありません。データベースのトランザクションはコミット前に接続が切れてロールバックされ、キューから取り出したメッセージは可視性タイムアウトの満了まで宙に浮きます。ログ上は「正常に停止しました」と見えるにもかかわらず、利用者側だけがエラーを踏む状態になるため、発見が遅れる型の不具合です。
無停止デプロイとの担当範囲の違いと本記事が扱う層の位置づけの整理
停止時のエラーはプロセスだけが原因とは限りません。経路(ロードバランサー)、プロセス、判定(ヘルスチェック)、状態(スキーマ)の4層のどこでも発生します。層ごとの切り分けと設定の全体像はゼロダウンタイムデプロイの発生源と設定順序で扱っているため、本記事は4層のうちプロセス層だけを実装の解像度で掘ります。
担当範囲を分けて考える理由は、対処の主体が違うからです。経路の待ち時間はインフラ設定、判定はマニフェストの記述で済むのに対し、プロセスの終了処理はアプリケーションのコードを書き換えないと入りません。基盤側の設定をいくら丁寧にしても、コードに停止処理がなければ症状は残る。ここが実装者の責任範囲になります。
停止要求を受けてから終了するまでの処理順序と待ち時間の設計指針
実装の骨格はどの言語でも同じです。順序を1つ間違えるだけで、待っているつもりの時間が無駄になります。
受付停止・処理完了待ち・依存先切断の3段階に分ける実装の骨格
第1段階は新規受付の停止です。リスナーを閉じて新しい接続を受け付けなくする。この時点ではまだ処理中の要求が残っているため、プロセスは終了しません。
第2段階が処理完了待ちで、実行中のリクエストやジョブが終わるのを待ちます。第3段階でデータベース接続プール、キャッシュ接続、ファイルハンドルを順に閉じ、最後にプロセスを終了する。順序を逆にして先に接続プールを閉じると、処理中のリクエストがデータベースへ到達できず、待っている間に500系の応答を返してしまいます。後始末は「外側から内側へ」が原則です。
ヘルスチェックを先に落としてから受付を止める順序が必要な理由
リスナーを閉じるだけでは足りません。ロードバランサーは自分の周期でヘルスチェックを行っており、停止を検知して振り分け先から外すまでに数秒から数十秒かかります。この秒数はヘルスチェックの既定値と最悪検知時間の逆算で求められます。この間に届いた新規接続は、閉じたばかりのポートに対して接続拒否となる。
そのため、シグナルを受けたらまず準備完了の判定を失敗に切り替え、ロードバランサーが振り分けを止めるまで待ってからリスナーを閉じます。準備完了と生存確認の判定を分ける考え方はLiveness ProbeとReadiness Probeの設定で詳しく整理しており、停止処理では準備完了の判定だけを落とし、生存確認は通したままにするのが基本形です。生存確認まで落とすと、終了処理の途中で再起動をかけられます。
猶予時間の入れ子をロードバランサーの待ち時間から逆算する手順
設定値は3つの時間の大小関係で決まります。外側から順に、ロードバランサーが接続を引き剥がすまでの待ち時間、実行基盤がプロセスへ与える猶予時間、アプリケーションが処理完了を待つ時間です。この順で長さが短くなる入れ子になっていれば、どの層でも打ち切りは起きません。
逆算の手順は決まっています。まず要求処理時間の99パーセンタイルを実測し、それを処理完了待ちの上限に置く。次にヘルスチェックの間隔と失敗回数から振り分け停止までの秒数を求め、両者の合計より少し長い値を猶予時間に設定します。ロードバランサーの振り分けとヘルスチェックの仕組みを踏まえて周期を確認しないと、この合計が実態より短く見積もられる点に注意してください。
言語とフレームワークごとのグレースフルシャットダウン実装の要点
骨格が同じでも、標準機能でどこまで賄えるかは処理系ごとに違います。手を入れる箇所を見極めます。
Goで停止用の文脈に期限を持たせて待ち時間を打ち切る書き方の実例
Goの標準HTTPサーバーは、停止用のメソッドを呼ぶと新規接続の受付を止め、処理中の要求が終わるまで待ちます。待ち時間の上限は、渡す文脈に期限を設定して与える形です。
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("graceful shutdown timeout: %v", err)
}
close(dbPool)
実装で漏れやすいのが、サーバー以外の停止です。バックグラウンドで動かしている定期処理や、goroutineで起動したワーカーは停止メソッドの対象外のため、待機グループで完了を待つ処理を自分で書く必要があります。ここを忘れると、サーバーは丁寧に閉じたのにワーカーだけが道連れで消えます。
Node.jsでサーバーの終了処理が完了せず猶予を使い切る原因
Node.jsでは、サーバーの終了処理を呼んでも待機中のkeep-alive接続が残っていると完了通知が返りません。ブラウザやHTTPクライアントが再利用のために接続を保持したままだと、処理中の要求がゼロでも待ち続け、猶予時間を丸ごと使い切ってから強制終了される挙動になる。
対処は2つです。1つは待機中の接続を明示的に閉じる仕組みを併用し、処理中の要求だけを待つ形に絞ること。もう1つは、停止時に応答ヘッダーで接続を閉じる指示を返し、クライアント側に再利用をやめさせる方法です。どちらも入れずに秒数だけ延ばすと、デプロイ1回あたりの所要時間が台数分だけ膨らみます。
Spring BootとPython製サーバーで設定だけで賄える範囲の見極め
Spring Bootはserver.shutdownの既定がgracefulで、Tomcat・Jetty・Reactor Nettyのいずれでも新規受付を止めて処理中の要求を待ちます。待ち時間はspring.lifecycle.timeout-per-shutdown-phaseで明示指定する形です(2026年8月時点の3.x系リファレンス)。設定だけでWeb層は賄えるものの、非同期実行器やスケジューラの停止は個別に指定が要ります。
PythonのASGIサーバーも同様に、停止シグナルを受けて処理完了を待つ実装が入っています。注意点はワーカーを複数持つ構成で、親プロセスがシグナルを受けてから子へ転送するまでの時間が加算されること。設定で賄える範囲は「HTTPの入口」までで、キュー消費やバッチはアプリケーション側で書く、と線を引くと判断が早くなります。
コンテナ基盤で猶予時間とpreStopを設定するときの実装判断
コードが正しくても、基盤の設定値が小さければ途中で切られます。基盤ごとの前提を押さえます。
Kubernetesの猶予時間とpreStopが合算される入れ子の注意点
Kubernetesでは、Podの猶予秒数を過ぎるとコンテナが強制終了されます。既定は30秒です。振り分け停止の待ち時間を稼ぐためにpreStopフックを併用する構成が一般的ですが、公式ドキュメントはpreStopが非同期ではなく、停止シグナルの送信前に完了する必要があると明記しています。
terminationGracePeriodSeconds: 45
lifecycle:
preStop:
exec:
command: ["sleep", "10"]
猶予秒数は、preStopの実行時間とコンテナ停止時間の合計に対して適用されます。公式ドキュメントは猶予60秒・preStop 55秒・停止10秒の例を挙げ、合計65秒が猶予を超えるため強制終了されると説明しています。上の例なら、preStopの10秒を引いた35秒がアプリケーションの後始末に使える時間です。安定版は1.36系(1.36.2・2026年6月9日時点)で、ハンドラはExec・HTTP・Sleepの3種が用意されています。
ECSのstopTimeoutが既定30秒・上限120秒である前提の設計
Amazon ECSでは、コンテナ定義のstopTimeoutが猶予時間にあたります。未指定なら30秒、指定できる最大値は120秒です(プラットフォームバージョン1.3.0以降)。上限が固定されている点がKubernetesとの大きな違いになります。
| 実行基盤 | 猶予時間の設定 | 既定値 | 上限 |
|---|---|---|---|
| Kubernetes 1.36系 | Podの猶予秒数 | 30秒 | 指定した値 |
| ECS Fargate | stopTimeout | 30秒 | 120秒 |
| Docker CLI | 停止時の待機秒数 | 10秒 | 指定した値 |
この上限は設計に効きます。後始末に120秒を超える時間が要る処理は、そもそも要求処理と同じコンテナに置くべきではない、という判断がここから導けるためです。長時間のジョブは別基盤へ切り出し、停止時は進捗を保存して次回に再開する形へ組み替えます。
PID 1問題でシグナルが子プロセスへ届かない起動コマンドの型
コード側に停止処理を書いたのに効かない場合、多くはシグナルが届いていません。コンテナのPID 1がアプリケーションではなくシェルになっていると、シェルは受け取ったシグナルを子へ転送しないため、アプリケーションは何も知らないまま強制終了されます。
起動コマンドをシェル形式で書いたときに起きる型で、配列形式で直接実行するか、シグナルを転送する軽量な初期プロセスを挟むことで解消します。切り分けは簡単です。コンテナ内でプロセス一覧を取り、PID 1がアプリケーション本体かどうかを見る。手動でシグナルを送って終了ログが出るかを確かめれば、基盤の設定を疑う前に原因を確定できます。
非同期ワーカーとバッチ処理の停止で壊れやすい箇所への対処方法
HTTPサーバーの停止は情報が多い一方、ワーカー側は各自の実装に委ねられます。取りこぼしはここに集中します。
キュー消費プロセスで処理中メッセージを失わない停止の手順設計
キューを消費するワーカーの停止は、ポーリングの停止と処理中メッセージの完了という2段階で考えます。シグナルを受けたら新しいメッセージの取得をやめ、手元で処理中のものだけを最後まで進めて確認応答を返す。ここまで終えてから接続を閉じます。
猶予時間内に終わらない場合の逃げ道も用意してください。処理を中断し、確認応答を返さずに終了すれば、メッセージは可視性タイムアウトの満了後に再配信されます。この設計が成り立つ前提が処理の冪等性で、二重実行しても結果が変わらない作りにしておけば、停止のたびに整合性を心配せずに済みます。
長時間バッチを途中で止める場合の再開位置と記録方法の具体的な決め方
数十分かかるバッチを猶予時間内に終わらせることはできません。方針は打ち切りと再開に切り替えます。処理を一定件数ごとの区切りに分け、区切りの完了ごとに進捗位置を保存する。停止要求を受けたら現在の区切りだけを終えて終了します。
再開位置の保存先は、処理対象と同じデータベースに置くと整合が取りやすくなります。件数の区切りは、1区切りが猶予時間の半分以内に収まる大きさが目安です。区切りを大きくすると停止時の待ち時間が伸び、小さくしすぎると保存の負荷が乗るため、実測しながら調整します。
グレースフルシャットダウンが効いているかを検証する手順と基準
停止処理は入れただけでは信用できません。壊れても症状が出にくいため、意図的に確かめる必要があります。
停止中に負荷をかけて失敗した要求を数える検証環境の作り方の要点
検証は単純です。一定の負荷をかけ続けた状態で対象を停止し、その区間に失敗した要求の件数を数える。負荷なしで停止しても、取りこぼしが観測されないまま合格になってしまいます。
手順は3つに分かれます。秒間数十件の要求を投げ続ける負荷を用意し、停止コマンドの実行時刻を記録し、区間内の接続失敗と5xxを集計する。合格ラインは「連続して失敗した最大秒数がゼロ」です。1件でも失敗が出るなら、ヘルスチェックの落とし方か、後始末の順序か、猶予時間の入れ子のどれかが崩れています。
終了処理の所要時間をログとメトリクスで可視化する計測項目の選び方
合否だけでなく、猶予時間にどれだけ余裕があるかも見ておきます。計測する項目は、シグナル受信の時刻、受付停止の完了時刻、処理完了待ちの終了時刻、プロセス終了の時刻の4点です。各段階の所要時間が分かれば、どこで時間を使っているかが特定できます。
運用では、この所要時間を毎回の停止で記録し、猶予時間に対する比率で監視します。比率が7割を超えたら設定値の見直し時期です。依存ライブラリの更新や処理の追加で後始末は少しずつ伸びるため、閾値を割ったときに気づける形にしておくと、強制終了に転じる前に手を打てます。
猶予時間を延ばさず打ち切る判断が正しくなる条件と運用の分担基準
停止処理は投資を続けられる領域です。どこで止めるかを条件として決めておきます。
猶予を延ばしても解決しない設計上の問題を切り分ける基準の見方
次のどれかに当てはまるなら、猶予時間の延長は対処になりません。1つ目は、要求処理そのものが数分かかる設計で、非同期化しない限り停止時の待ちは短くならない。2つ目は、外部への書き込みが冪等でないため中断も再実行もできない構造です。3つ目は、プロセスがメモリ上に他へ引き継げない状態を抱えている場合になります。
目安として、猶予時間が60秒を超える設定を要求された時点で、設定ではなく設計を疑ってください。ECSでは120秒という上限があるため、そもそも延長では到達できない領域があります。長時間の処理はジョブとして切り出し、要求と同じプロセスから外す。この組み替えの方が、停止処理を精緻にするより効果が長持ちします。
停止処理の作り込みを止めてよい水準と外部への引き渡し範囲の判断
作り込みの終点は数値で決めます。負荷をかけた状態での停止検証で失敗要求がゼロになり、終了処理の所要時間が猶予の7割以内に収まっていれば、それ以上の作り込みは止めてよい水準です。接続の再試行機構や独自の排水制御をここから足すのは、受託開発の規模では過剰になります。
維持の方が難所です。停止処理は普段の機能テストで壊れたことに気づけず、ライブラリ更新や基盤の設定変更で静かに効かなくなるものです。一創では保守運用・内製化支援として、停止検証をリリース手順へ組み込む設計から、所要時間の監視と閾値運用の内製移管までを請け負っています。仕組みを一度整えたうえで、判断基準ごと運用チームへ引き渡す進め方をおすすめします。
よくある質問
グレースフルシャットダウンの実装で繰り返し寄せられる質問を、判断に直結する形でまとめます。
グレースフルシャットダウンとゼロダウンタイムデプロイは何が違いますか?
担当する層が違います。ゼロダウンタイムデプロイは「止めずに入れ替える」という目的を指す総称で、経路・プロセス・判定・状態の4層すべてが噛み合って成立します。グレースフルシャットダウンはそのうちプロセス層の実装で、停止要求を受けたプロセスが処理中の仕事を終えてから自分で終了する仕組みを指す。停止処理を入れただけでは無停止にはならず、逆に停止処理がなければどの方式を選んでも取りこぼしは残ります。
猶予時間は何秒に設定するのが妥当ですか?
要求処理時間の99パーセンタイルと、ロードバランサーが振り分けを止めるまでの秒数の合計から逆算します。処理が2秒以内に収まり、ヘルスチェックが5秒間隔・2回失敗で切り離す構成なら、10秒の待機に処理完了待ちを足して20秒前後が現実的な値です。既定の30秒を触らずに済む構成も多く、実測せずに120秒へ延ばす対応は、デプロイ時間を台数分だけ伸ばすだけに終わります。
停止処理を書いたのに強制終了されてしまうのはなぜですか?
確認する順序は3つです。まずコンテナのPID 1がアプリケーション本体かを見る。シェル経由で起動しているとシグナルが子へ届きません。次に、猶予時間とpreStopの実行時間が合算される点を踏まえ、後始末に使える残り時間を計算します。最後に、待機中のkeep-alive接続やバックグラウンドのワーカーが待ちを引き延ばしていないかを、段階ごとの所要時間ログで確認してください。
preStopフックとアプリケーション側の停止処理はどちらに書くべきですか?
役割で分けます。preStopは「ロードバランサーが振り分けを止めるまで待つ」ための時間稼ぎに使い、待機だけを担当させる。処理中の要求を終わらせる、接続プールを閉じるといった後始末は、状態を知っているアプリケーション側に置きます。preStopへ後始末を寄せると、猶予時間の合算で使える時間が減り、フックが固まったときにPodが終了できなくなるため避けてください。
グレースフルシャットダウンが効いているか本番以外で確認できますか?
確認できます。検証環境で秒間数十件の負荷をかけながら停止し、区間内の接続失敗と5xxを数える手順で十分です。本番と同じロードバランサー構成が用意できない場合でも、プロセス単体へ手動でシグナルを送り、段階ごとの所要時間ログが最後まで出力されるかを見れば、コード側の実装の正しさは確かめられます。基盤の設定値の妥当性だけは、構成が同じ環境で測ってください。
関連記事
- ゼロダウンタイムデプロイとは?無停止を壊す発生源と設定順序:本記事が扱うプロセス層を含む4層全体の切り分けと、無停止の計測方法を整理しています。
- ローリングアップデートとは?仕組みと設定値:停止処理が整った前提で、台を順に入れ替える方式の設定を扱っています。
- ブルーグリーンデプロイメントとは?切り替え手順:待機系を用意して一度に切り替える方式との比較に使えます。
- Kubernetesとは?仕組みとDockerとの違い:猶予時間やpreStopを設定する前提となる基盤側の全体像をまとめています。
- 可用性とは?稼働率の計算と高可用性の設計:停止時間の許容幅を数値で決めるための考え方を扱っています。