入門:グレースフルシャットダウン
おでんが美味しい季節になってきました。年末といえば、公私ともにやりかけたことを終えて新しく仕切り直したい、つまりシャットダウンの時期ですね。本記事では雰囲気で理解しがちなグレースフルシャットダウンを見ていきたいと思います。前半では基本的な概念、中盤に少し詳細を見て、後半に教養として歴史を辿ります。本日はサトシの方の小林が担当します。
※ 主にWebアプリケーションエンジニアを想定しています。
※ AWSを例にすることが多いです。
グレースフルシャットダウンとは
早速、見ていきましょう。まずChatGPT先生に聞いてみます。
「グレースフルシャットダウン(Graceful Shutdown)とは、システムやアプリケーションを停止させる際に、現在の処理やタスクを可能な限り安全に完了させ、リソースの解放を適切に行いながら停止する一連の流れを指します。データ損失やシステムの不整合を防ぎ、ユーザー体験やシステムの安定性を保つために重要です。」
普段、私たちが使う MacOS や Windows におけるアプリケーションの通常終了はまさにグレースフルシャットダウンに該当します。ファイルを保存してから終了したり、アプリケーション内の状態保存をおこなったり、数秒内で安全に終了し次の起動につなげるための状態保存が行われています。一方、もしアプリケーションが応答しなくなったときに強制終了する行為はフォースフルシャットダウン(Forceful Shutdown
)と呼ばれたりします。
図に表してみましょう。
ここで、SIGTERM
、SIGKILL
、Graceful Period
という言葉が出てきました。ローカルから(主にクラウド上で稼働する)アプリケーション開発に視点を移して行ったとき、CI/CDによるアプリケーションのデプロイを正常に行うために押さえておくべき起点、終点、そして期間です。ローカルPCにおいては勝手に強制終了されることがないのに対して、アプリケーションのデプロイにおいてはSIGTERM
による終了要求、数十秒の猶予期間、時間切れしたときのSIGKILL
のよる強制終了という流れを踏むことが一般的です。
グレースフルシャットダウンの流れ
猶予期間(Graceful Period)において、以下のような終了処理を行います。
- 新しいリクエストの受付停止 まず、新しいリクエストやジョブの受付を停止します。HTTPサーバーであれば、ロードバランサーやプロキシを介して接続を遮断し、キューイングシステムであれば新規のジョブポーリングを中断します。この段階で、新しい処理が追加されることを防ぎます。
- 進行中の処理の完了待ち 次に、進行中の処理を完了させます。たとえば、実行中のHTTPリクエストやトランザクションを特定し、それらがすべて終わるまで待機します。この際、長時間のタスクには適切なタイムアウトを設定し、必要に応じて中断可能なポイントを設けて安全に停止できるようにします。未完了の処理については状態を記録し、再試行のための手段を確保することが望ましいでしょう。
- リソースの解放 進行中の処理が完了したら、使用中のリソースを解放します。たとえば、開いているファイルハンドルやデータベース接続を適切に閉じること、メモリやキャッシュに保持された一時データを削除することが含まれます。また、外部APIとの通信を安全に終了させるためには、通信中のリクエストを完了させた後、HTTPクライアントなどを適切に閉じる必要があります。
- 状態の保存 状態の保存も重要なステップです。シャットダウン時のアプリケーションの状態を記録し、次回の起動時に正確に復元できるよう準備します。セッションデータやユーザー入力の途中状態を保存することで、システムの一貫性を保ちながら再開することが可能になります。
- 最終的なシャットダウン そして、SIGKILLによる強制終了に至る前に安全に終了します。たとえば、AWS ECS なら猶予期間のデフォルトは30秒です。それまでにシャットダウンする必要があります。
外部接続とグレースフルシャットダウン
HTTPリクエストを始めとし、データベース、キャッシュ、メッセージングシステムなど、一般的なWebアプリケーションにおいては外界に接するものが数種類存在しますが、それぞれにおけるグレースフルなシャットダウンを見てみます。
ライブラリやフレームワークによっては開発者が意識することなく終了処理をこなしてくれるものもありますが、予期せぬ事態に対処するためにも一連の振る舞いを理解することは重要です。
HTTPリクエストの新規受付停止
- (たとえば AWS ECS の場合)先に新しいタスク定義で指定されたコンテナが起動します。起動したタスクは、ロードバランサー(ALB/NLB)やECSの内部ヘルスチェックを通じて、正常性が確認されます。
- 次にロードバランサーが、停止予定のタスクをターゲットグループから削除します。この段階で、タスクはまだ動作していますが、新しいリクエストを受け付けなくなります。
- 処理中のリクエストが完了するのを待ちます。接続の保持時間はロードバランサーの設定に依存します。ELBにおいては60秒が既定値です。強制終了までとの時間の前後関係に注意が必要です。
SQSメッセージの新規受領停止
- HTTPリクエストが外部からの通信であることに対して、SQSは実際はキューにポーリングを行っています(先程の図でも矢印の向きに注意)。途中の処理を継続することは同様ですが、新しいメッセージの受信停止はアプリケーション側で対応する必要があります。
- シャットダウンシグナルを受けたら、新しいメッセージの受信を停止します。アプリケーションフレームワークのシャットダウンイベントにポーリング停止処理を書く必要があります。
- 一方、もし、強制終了までにすでに受領したメッセージの処理が終了しない場合、SQS上は実行中のママであると認識され、 Visibility Timeout後に失敗としてカウントされます。試行可能回数(maxReceiveCount)を超えるとDead Letter Queue (DLQ) に格納されることとなります。
データベース接続のクローズ
- シャットダウン中にデータベース接続を適切にクローズしないと、データ整合性やリソースリークのリスクがあります。コネクションプール中のすべてのコネクションを安全に解放します。
- 現在のトランザクションをコミットまたはロールバックします。長い実行中のSQLがある場合には一定時間経過後にステートメントやコネクションのキャンセル処理が必要です。
外部API接続のクローズ
- HTTPクライアント等を利用した外部APIとの通信中にシャットダウンすると、未完了のリクエストが切断され、外部システムにエラーが発生する可能性があります。
- タイムアウトを設定し、アプリケーション内で強制終了し、事後処理を行ないます。
他にもRedis等のキャッシュシステムへの接続、ファイルシステム等のローカルリソースへの接続など、安全にクローズするべき処理や接続は多く考えられます。センシティブになりすぎる必要はありませんが、ライブラリやフレームワークの挙動を確認し、必要な中断処理を実装し、最悪の事態を想定したリカバリを計画しておくことが望まれます。
では、最後に教養としてOSシグナルとグレースフルシャットダウンの歴史を掘ってみましょう。
OSシグナルの詳細
いくつかのOSシグナルを表にしてみます。これを見て分かるとおり、SIGTERMは15番でSIGKILLが9番です。前者はローカルでは kill <プロセス番号>
と打つと思いますが、 kill -15 <プロセス番号>
と明示的にシグナル番号を指定することもできます。そして、後者の kill -9 <プロセス番号>
は頻出コマンドのひとつですね。それ以外に、SIGINT は SIGTERM と同様の挙動につながることも多いかもしれません。
シグナル | 番号 | 説明 |
---|---|---|
SIGTERM | 15 | 終了要求を通知する標準的なシグナル |
SIGKILL | 9 | プロセスを即座に強制終了する。プロセスはこのシグナルを無視できない |
SIGHUP | 1 | セッション(端末)切断を通知。プロセスの再起動や設定リロードをトリガーすることも |
SIGINT | 2 | Ctrl+Cで送信される。プロセスに割り込みを要求し、通常は終了処理を行う。ユーザー主導でシャットダウンを開始するために使用される |
SIGQUIT | 3 | プロセスの終了とメモリダンプの出力をトリガー。デバッグ用途に関連する |
SIGUSR1 | 10 | ユーザー定義のシグナル。プロセスに特定のカスタム動作を指示する |
SIGUSR2 | 12 | ユーザー定義のシグナル。SIGUSR1と同様にカスタム動作を指示する |
SIGSTOP | 19 | プロセスの一時停止。プロセスが再開されるまで実行が停止する |
SIGCONT | 18 | 停止中のプロセスを再開 |
SIGPIPE | 13 | プロセスが閉じられたパイプに書き込もうとしたときに送信される |
SIGALRM | 14 | タイマーが満了したときに送信される |
グレースフルシャットダウンの歴史
SIGTERM、SIGKILL がUnixで定義されたものということはグレースフルシャットダウンの考え方やノウハウ自体も先人たちが積み上げてきたものと言えそうです。ここでまたChatGPT先生に登場してもらい、その歴史を見てみます。
1. 初期のコンピューター時代(1950~1960年代)
- メインフレームの誕生: 初期のコンピューターシステム(例: IBMのメインフレーム)では、データの保存やタスクの処理は手動で管理されることが多く、システムの終了もオペレーターが慎重に手順を踏む必要がありました。当時のメモリやデータ保存は揮発性が高く、電源断や突然の終了は重大なデータ損失を引き起こしました。これにより、システムの計算状態を保存しながら安全に終了する必要性が認識されました。
- ジョブ制御システムの導入: メインフレームでは、ジョブ単位でタスクを管理するジョブ制御言語(JCL)が使用され、ジョブ終了時にログを出力し、状態を記録する仕組みが登場しました。
2. オペレーティングシステムの進化(1970~1980年代)
-
Unixの登場(1970年代): Unixは、プロセスの管理やシグナル処理の概念を導入しました。システム終了時にプロセスに対してSIGTERMやSIGHUPといったシグナルを送る仕組みは、グレースフルシャットダウンの基本的な形です。
- SIGTERM: プロセスに終了を通知し、クリーンアップ処理を行う猶予を与える。
- SIGHUP: デーモンプロセスの再起動や終了通知に使用される。
- データベースのトランザクション処理: この時期、データベースシステム(例: IBM DB2、Oracle Database)でトランザクション処理の概念が確立されました。特に、ACID特性(Atomicity, Consistency, Isolation, Durability)の確立は、システム終了時にデータの整合性を保つ重要性を示しました。
3. ネットワークと分散システムの拡大(1990年代)
- クライアント/サーバーモデル: インターネットの普及に伴い、クライアント/サーバー型アーキテクチャが一般化しました。これにより、サーバー側で進行中のセッションを維持しつつ安全に終了する必要性が生まれました。Webサーバー(例: Apache HTTP Server)の登場により、接続中のリクエストを適切に処理してから停止する仕組みが求められました。
- システム障害への耐性: 分散システム(例: 初期の分散ファイルシステムや分散データベース)が普及し始め、ノード停止がシステム全体に与える影響を最小限にするシャットダウン設計が重視されるようになりました。
4. 高可用性とクラウドの時代(2000年代)
-
仮想化技術の進化: 仮想マシン(VMware、Xenなど)の普及により、ホストマシンの再起動やシャットダウン時にゲストマシンの状態を保存する機能が重要となりました。
- 仮想マシンの「スナップショット」機能は、実行中のシステム状態をそのまま保存し、必要に応じて迅速に復元できる仕組みとして、グレースフルシャットダウンの考え方を新しい形で具現化しました。
-
Webアプリケーションのスケールアウト: サーバー数が増加するにつれ、個々のサーバーを安全に停止するプロセスが重要となりました。特に、ロードバランサーを使用する際には、シャットダウン中のサーバーをリクエスト対象から外す仕組みが必要でした。
-
クラウド環境: AWSやGoogle Cloudのようなクラウドサービスでは、インスタンスの停止時にpreStopフックや終了通知イベントが提供され、シャットダウンプロセスをプログラム的に管理できるようになりました。
5. コンテナとマイクロサービス(2010年代~現在)
-
コンテナ技術の普及: DockerやKubernetesなどの登場により、コンテナ化されたアプリケーションが一般化しました。
- コンテナのライフサイクル管理では、停止時にSIGTERMを送信し、シャットダウンスクリプトを実行してから削除するプロセスが標準となっています。
-
マイクロサービスアーキテクチャ: マイクロサービスの普及に伴い、サービス間通信を適切に終了させ、状態の一貫性を保つことが求められました。
- 例えば、HTTPリクエストやキューに保留中のメッセージを処理してからサービスを停止する仕組みが重要です。
6. 今後の展望
- サーバーレス: 現在、サーバーレスは従来のAWS LambdaやGoogle Cloud Functionsといった基盤から進化し、WebAssembly(Wasm)を利用した軽量な実行環境や、セルフホスティング型サーバーレスフレームワーク(例: OpenFaaS、Knative)など、新しいトレンドが注目されています。これらはさらなる柔軟性と性能向上を目指しています。
- エッジコンピューティング: エッジノードのシャットダウンは、リアルタイム性を求められる分野での新たな課題となっています。
グレースフルシャットダウンひとつをとっても面白いものですね。
まとめ
グレースフルシャットダウンの概要、処理のイメージ、外部接続への考慮、そして、他のOSシグナルと歴史まで辿ってきました。当たり前のことしか書いていないと感じられた方も、意外と知らなかったなと感じられた方もいらっしゃったかもしれません。
軽量なアプリケーションが増えたり、デプロイが頻度が増えたりする傾向がある中でも、外部とのインタラクションだったり、想定外に延びてしまった自分たちが実装した処理など、現実を見ると時間がかかるものに直面することはよくあります。グレースフルシャットダウンを意識し、シャットダウンプロセスをより確実にコントロールすることは、より堅牢なシステムに繋がります。
本当はコードに踏み込みたかったのですが、思いのほか長くなってしまいましたので今回はこれにて。
それでは、不要なものは後腐れなくグレースフルにシャットダウンし、身軽な年始を迎えていきましょう!
Discussion