イライラしてコンピュータから電源コードを引っ張って抜いたことがありますか? これは簡単な解決策のように思えますが、データの損失やシステムの不安定化につながる可能性があります。ソフトウェアの世界には、同様の概念が存在します。それは、ハード シャットダウンです。この突然の終了は、物理的な終了と同様に問題を引き起こす可能性があります。ありがたいことに、よりよい方法があります。それは、正常なシャットダウンです。
正常なシャットダウンを統合することで、サービスに事前通知を提供します。これにより、進行中のリクエストを完了し、状態情報をディスクに保存し、シャットダウン中のデータ破損を回避できるようになります。
このガイドでは、正常なシャットダウンの世界を詳しく解説し、特に Kubernetes 上で実行されるGo アプリケーションでの実装に焦点を当てます。
Unix ベースのシステムで正常なシャットダウンを実現するための重要なツールの 1 つは、シグナルの概念です。シグナルとは、簡単に言えば、あるプロセスから別のプロセスに特定の情報を伝達する簡単な方法です。シグナルの仕組みを理解することで、シグナルを利用してアプリケーション内で制御された終了手順を実装し、スムーズでデータに安全なシャットダウン プロセスを実現できます。
シグナルは多数あり、ここで見つけることができますが、ここで注目するのはシャットダウン シグナルのみです。
これらのシグナルは、ユーザー (Ctrl+C / Ctrl+D)、別のプログラム/プロセス、またはシステム自体 (カーネル/OS) から送信できます。たとえば、 SIGSEGV (セグメンテーション エラー) は OS によって送信されます。
実用的な設定で正常なシャットダウンの世界を探索するために、実験できる簡単なサービスを作成しましょう。この「モルモット」サービスには、Redis のINCRコマンドを呼び出すことによって実際の作業をシミュレートする単一のエンドポイントがあります (わずかな遅延を追加します)。また、プラットフォームが終了信号を処理する方法をテストするための基本的な Kubernetes 構成も提供します。
最終的な目標は、リクエストやデータを失うことなく、サービスがシャットダウンを適切に処理できるようにすることです。並行して送信されたリクエストの数と Redis の最終カウンター値を比較することで、適切なシャットダウンの実装が成功したかどうかを確認できます。
Kubernetes クラスターと Redis の設定の詳細については説明しません。完全な設定は Github リポジトリで確認できます。
検証プロセスは次のとおりです。
まずはベースとなる 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 は、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()
NotificationContext は、リストされたシグナルの 1 つが到着したとき、返されたstop()関数が呼び出されたとき、または親コンテキストの Done チャネルが閉じられたときのいずれか早い時点で、完了としてマークされた (Done チャネルが閉じられた) 親コンテキストのコピーを返します。
現在の HTTP サーバーの実装にはいくつかの問題があります。
書き直してみましょう。
正常なシャットダウン/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") }
アップデートの概要は次のとおりです。
ここで、検証プロセスを繰り返すと、1000 件のリクエストがすべて正しく処理されていることがわかります。🎉
Echo、Gin、Fiber などのフレームワークは、受信リクエストごとに goroutine を生成し、コンテキストを与えて、決定したルーティングに応じて関数 / ハンドラーを呼び出します。この場合、それは “/incr” パスの HandleFunc に渡される匿名関数になります。
SIGTERMシグナルを傍受し、フレームワークに正常にシャットダウンするように要求すると、2 つの重要なことが起こります (簡単に言えば)。
注: 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 がポッドを終了することを決定すると、次のイベントが発生します。
ご覧のとおり、長時間実行される終了プロセスがある場合は、アプリケーションが正常にシャットダウンするのに十分な時間を確保するために、 terminationGracePeriodSeconds設定を増やす必要がある場合があります。
正常なシャットダウンは、データの整合性を保護し、シームレスなユーザー エクスペリエンスを維持し、リソース管理を最適化します。豊富な標準ライブラリと並行性を重視した Go により、開発者は正常なシャットダウン プラクティスを簡単に統合できます。これは、Kubernetes などのコンテナー化またはオーケストレーションされた環境にデプロイされるアプリケーションに不可欠です。
Go コードと Kubernetes マニフェストは、Github リポジトリで見つかります。