イライラしてコンピュータから電源コードを引っ張って抜いたことがありますか? これは簡単な解決策のように思えますが、データの損失やシステムの不安定化につながる可能性があります。ソフトウェアの世界には、同様の概念が存在します。それは、ハード シャットダウンです。この突然の終了は、物理的な終了と同様に問題を引き起こす可能性があります。ありがたいことに、よりよい方法があります。それは、正常なシャットダウンです。 正常なシャットダウンを統合することで、サービスに事前通知を提供します。これにより、進行中のリクエストを完了し、状態情報をディスクに保存し、シャットダウン中のデータ破損を回避できるようになります。 このガイドでは、正常なシャットダウンの世界を詳しく解説し、特に Kubernetes 上で実行される での実装に焦点を当てます。 Go アプリケーション Unix システムにおけるシグナル Unix ベースのシステムで正常なシャットダウンを実現するための重要なツールの 1 つは、シグナルの概念です。シグナルとは、簡単に言えば、あるプロセスから別のプロセスに特定の情報を伝達する簡単な方法です。シグナルの仕組みを理解することで、シグナルを利用してアプリケーション内で制御された終了手順を実装し、スムーズでデータに安全なシャットダウン プロセスを実現できます。 シグナルは多数あり、 見つけることができますが、ここで注目するのはシャットダウン シグナルのみです。 ここで - プロセスの終了を要求するために送信されます。最も一般的に使用され、後で詳しく説明します。 SIGTERM - 「すぐに終了」、妨害することはできません。 SIGKILL - 割り込み信号(Ctrl+C など) SIGINT - 終了シグナル(Ctrl+Dなど) SIGQUIT これらのシグナルは、ユーザー (Ctrl+C / Ctrl+D)、別のプログラム/プロセス、またはシステム自体 (カーネル/OS) から送信できます。たとえば、 (セグメンテーション エラー) は OS によって送信されます。 SIGSEGV 私たちのモルモットサービス 実用的な設定で正常なシャットダウンの世界を探索するために、実験できる簡単なサービスを作成しましょう。この「モルモット」サービスには、Redis の コマンドを呼び出すことによって実際の作業をシミュレートする単一のエンドポイントがあります (わずかな遅延を追加します)。また、プラットフォームが終了信号を処理する方法をテストするための基本的な Kubernetes 構成も提供します。 INCR 最終的な目標は、リクエストやデータを失うことなく、サービスがシャットダウンを適切に処理できるようにすることです。並行して送信されたリクエストの数と Redis の最終カウンター値を比較することで、適切なシャットダウンの実装が成功したかどうかを確認できます。 Kubernetes クラスターと Redis の設定の詳細については説明しません。 確認できます。 完全な設定は Github リポジトリで 検証プロセスは次のとおりです。 Redis と Go アプリケーションを Kubernetes にデプロイします。 を使用して 1000 件のリクエスト (40 秒間で 25/s) を送信します。 vegeta vegeta の実行中に、イメージ タグを更新して Kubernetes を初期化します。 ローリング アップデート Redis に接続して「カウンター」を確認します。1000 になっているはずです。 まずはベースとなる Go HTTP サーバーから始めましょう。 ハードシャットダウン/main.go package main import ( "net/http" "os" "time" "github.com/go-redis/redis" ) func main() { redisdb := redis.NewClient(&redis.Options{ Addr: os.Getenv("REDIS_ADDR"), }) server := http.Server{ Addr: ":8080", } http.HandleFunc("/incr", func(w http.ResponseWriter, r *http.Request) { go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) server.ListenAndServe() } func processRequest(redisdb *redis.Client) { // simulate some business logic here time.Sleep(time.Second * 5) redisdb.Incr("counter") } このコードを使用して検証手順を実行すると、一部のリクエストが失敗し、 ことがわかります (実行ごとに数値が異なる場合があります)。 カウンターが 1000 未満である これは明らかに、ローリング アップデート中に一部のデータが失われたことを意味します。😢 Go でのシグナル処理 Go は、Unix シグナルを処理できる パッケージを提供します。デフォルトでは、SIGINT および SIGTERM シグナルによって Go プログラムが終了することに注意してください。Go アプリケーションが突然終了しないようにするには、着信シグナルを処理する必要があります。 シグナル これを行うには 2 つのオプションがあります。 使用チャネル: c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGTERM) コンテキストを使用する (現在推奨されているアプローチ): ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop() 、リストされたシグナルの 1 つが到着したとき、返された 関数が呼び出されたとき、または親コンテキストの Done チャネルが閉じられたときのいずれか早い時点で、完了としてマークされた (Done チャネルが閉じられた) 親コンテキストのコピーを返します。 NotificationContext は stop() 現在の HTTP サーバーの実装にはいくつかの問題があります。 遅い processRequest goroutine があり、終了シグナルを処理しないため、プログラムは自動的に終了し、実行中のすべての goroutine も終了します。 プログラムは接続を閉じません。 書き直してみましょう。 正常なシャットダウン/main.go package main // imports var wg sync.WaitGroup func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop() // redisdb, server http.HandleFunc("/incr", func(w http.ResponseWriter, r *http.Request) { wg.Add(1) go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) // make it a goroutine go server.ListenAndServe() // listen for the interrupt signal <-ctx.Done() // stop the server if err := server.Shutdown(context.Background()); err != nil { log.Fatalf("could not shutdown: %v\n", err) } // wait for all goroutines to finish wg.Wait() // close redis connection redisdb.Close() os.Exit(0) } func processRequest(redisdb *redis.Client) { defer wg.Done() // simulate some business logic here time.Sleep(time.Second * 5) redisdb.Incr("counter") } アップデートの概要は次のとおりです。 SIGTERM 終了シグナルをリッスンするための を追加しました。 signal.NotifyContext 実行中のリクエスト (processRequest goroutine) を追跡するための を導入しました。 sync.WaitGroup サーバーを goroutine でラップし、コンテキストで を使用して、新しい接続の受け入れを正常に停止しました。 server.Shutdown 続行する前に、すべての実行中のリクエスト (processRequest goroutines) が終了することを確認するために を使用しました。 wg.Wait() リソースのクリーンアップ: 終了前に Redis 接続を適切に閉じるための が追加されました。 redisdb.Close() クリーン終了: 正常終了を示すために を使用しました。 os.Exit(0) ここで、検証プロセスを繰り返すと、1000 件のリクエストがすべて正しく処理されていることがわかります。🎉 Webフレームワーク / HTTPライブラリ Echo、Gin、Fiber などのフレームワークは、受信リクエストごとに goroutine を生成し、コンテキストを与えて、決定したルーティングに応じて関数 / ハンドラーを呼び出します。この場合、それは “/incr” パスの HandleFunc に渡される匿名関数になります。 シグナルを傍受し、フレームワークに正常にシャットダウンするように要求すると、2 つの重要なことが起こります (簡単に言えば)。 SIGTERM フレームワークが受信リクエストの受け入れを停止します 既存の受信リクエストが終了するまで待機します (暗黙的に goroutine が終了するまで待機します)。 注: Kubernetes は、ポッドを「Terminating」とラベル付けすると、ロードバランサーからポッドへの着信トラフィックの送信も停止します。 オプション: シャットダウンタイムアウト プロセスの終了は、特に接続を閉じるなどの多くの手順が関係する場合は複雑になることがあります。スムーズに実行するために、タイムアウトを設定できます。このタイムアウトはセーフティ ネットとして機能し、予想よりも時間がかかった場合にプロセスを正常に終了します。 shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() go func() { if err := server.Shutdown(shutdownCtx); err != nil { log.Fatalf("could not shutdown: %v\n", err) } }() select { case <-shutdownCtx.Done(): if shutdownCtx.Err() == context.DeadlineExceeded { log.Fatalln("timeout exceeded, forcing shutdown") } os.Exit(0) } Kubernetes 終了ライフサイクル Kubernetes を使用してサービスをデプロイしたので、Kubernetes がポッドを終了する方法についてさらに詳しく見てみましょう。Kubernetes がポッドを終了することを決定すると、次のイベントが発生します。 ポッドは「終了中」状態に設定され、すべてのサービスのエンドポイント リストから削除されます。 定義されている場合は フックが実行されます。 preStop シグナルがポッドに送信されます。これで、アプリケーションは何をすべきかがわかりました。 SIGTERM Kubernetes は、デフォルトでは 30 秒である猶予期間 ( ) を待機します。 terminationGracePeriodSeconds シグナルがポッドに送信され、ポッドが削除されます。 SIGKILL ご覧のとおり、長時間実行される終了プロセスがある場合は、アプリケーションが正常にシャットダウンするのに十分な時間を確保するために、 設定を増やす必要がある場合があります。 terminationGracePeriodSeconds 結論 正常なシャットダウンは、データの整合性を保護し、シームレスなユーザー エクスペリエンスを維持し、リソース管理を最適化します。豊富な標準ライブラリと並行性を重視した Go により、開発者は正常なシャットダウン プラクティスを簡単に統合できます。これは、Kubernetes などのコンテナー化またはオーケストレーションされた環境にデプロイされるアプリケーションに不可欠です。 Go コードと Kubernetes マニフェストは で見つかります。 、Github リポジトリ リソース os/signal パッケージ Kubernetes ポッドのライフサイクル