3 ポイント 投稿者 GN⁺ 2025-05-06 | 1件のコメント | WhatsAppで共有
  • グレースフルシャットダウン (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ランタイムは SIGTERMSIGINT を受信するとデフォルトでアプリケーションを終了するが、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件のコメント

 
GN⁺ 2025-05-06
Hacker Newsの意見
  • Kubernetesでは、ロードバランサーのターゲットIP更新に時間がかかることがある。問題の90%は、トラフィックが実際にドレインされているかを確認することにある

    • グローバルな preStop フックに15秒の待機時間を追加することで、HTTP 503 の比率が大幅に改善した
    • ロードバランサーの登録解除と SIGTERM の送信の間に時間を作り、アプリケーション側の処理を簡素化した
  • log.Fatal を使うと、defer 内の内容は実行されない

    • log.Fatalos.Exit を呼び出して即座に終了する
    • panic を使えば defer の内容は実行される
  • Prometheus の /metrics エンドポイントが定期的にスクレイプされる場合、最後のスクレイプとプロセス終了の間に記録されたメトリクスが伝播されない可能性がある

    • サービス終了時に最後の数秒分のログを失うことがある
    • ログファイルがサイドカープロセスによって監視されている場合、競合状態が発生することがある
  • 分散システムがクライアントの正常終了に依存すると、システムが深刻に故障する可能性がある

  • 新しいサービスインスタンスが以前のインスタンスからソケットを受け取ることで、接続を切らずにアプリケーションを再起動する方法についての説明が不足している

    • systemd では実装は比較的簡単である
    • nginx は20年以上前からこれをサポートしている
    • Kubernetes と Docker はこれをサポートしていない
  • liveness に関する議論が不足している

    • 同じエンドポイントを liveness/readiness に使っているアプリを何度も見かけた
  • プログラムが ctrl c のようなコマンドをきれいに処理できないなら、適切に書かれていないということだ

  • Elixir はプロセスを小さな VM プロセスとして設計しており、正常終了ルーチンを意図的に作る必要がないようにしている

  • プロジェクトで正常終了を扱うための小さなライブラリを作った

    • さまざまな起動・終了メカニズムを持つサービスを統合するための API を提供している
  • readiness probe を更新した後、数秒待ってシステムが新しいリクエストを送らないようにする

    • 終了中の pod は Ready 状態ではない
    • サービスはエンドポイントを終了中としてマークする
    • SIGTERM 後にもわずかな時間差が生じる可能性はあるが、大きな問題ではない
    • 新しい接続を受け付けず、既存の接続を正常に終了させることが重要である