paint-brush
Go で正常なシャットダウンをマスターする: Kubernetes の総合ガイド@gopher

Go で正常なシャットダウンをマスターする: Kubernetes の総合ガイド

Alex6m2024/08/14
Read on Terminal Reader

長すぎる; 読むには

このガイドでは、正常なシャットダウンの世界を詳しく解説し、特に Kubernetes 上で実行される Go アプリケーションでの実装に焦点を当てます。
featured image - Go で正常なシャットダウンをマスターする: Kubernetes の総合ガイド
Alex HackerNoon profile picture

イライラしてコンピュータから電源コードを引っ張って抜いたことがありますか? これは簡単な解決策のように思えますが、データの損失やシステムの不安定化につながる可能性があります。ソフトウェアの世界には、同様の概念が存在します。それは、ハード シャットダウンです。この突然の終了は、物理的な終了と同様に問題を引き起こす可能性があります。ありがたいことに、よりよい方法があります。それは、正常なシャットダウンです。


正常なシャットダウンを統合することで、サービスに事前通知を提供します。これにより、進行中のリクエストを完了し、状態情報をディスクに保存し、シャットダウン中のデータ破損を回避できるようになります。


このガイドでは、正常なシャットダウンの世界を詳しく解説し、特に Kubernetes 上で実行されるGo アプリケーションでの実装に焦点を当てます。

Unix システムにおけるシグナル

Unix ベースのシステムで正常なシャットダウンを実現するための重要なツールの 1 つは、シグナルの概念です。シグナルとは、簡単に言えば、あるプロセスから別のプロセスに特定の情報を伝達する簡単な方法です。シグナルの仕組みを理解することで、シグナルを利用してアプリケーション内で制御された終了手順を実装し、スムーズでデータに安全なシャットダウン プロセスを実現できます。


シグナルは多数あり、ここで見つけることができますが、ここで注目するのはシャットダウン シグナルのみです。

  • SIGTERM - プロセスの終了を要求するために送信されます。最も一般的に使用され、後で詳しく説明します。
  • SIGKILL - 「すぐに終了」、妨害することはできません。
  • SIGINT - 割り込み信号(Ctrl+C など)
  • SIGQUIT - 終了シグナル(Ctrl+Dなど)


これらのシグナルは、ユーザー (Ctrl+C / Ctrl+D)、別のプログラム/プロセス、またはシステム自体 (カーネル/OS) から送信できます。たとえば、 SIGSEGV (セグメンテーション エラー) は OS によって送信されます。


私たちのモルモットサービス

実用的な設定で正常なシャットダウンの世界を探索するために、実験できる簡単なサービスを作成しましょう。この「モルモット」サービスには、Redis のINCRコマンドを呼び出すことによって実際の作業をシミュレートする単一のエンドポイントがあります (わずかな遅延を追加します)。また、プラットフォームが終了信号を処理する方法をテストするための基本的な Kubernetes 構成も提供します。


最終的な目標は、リクエストやデータを失うことなく、サービスがシャットダウンを適切に処理できるようにすることです。並行して送信されたリクエストの数と Redis の最終カウンター値を比較することで、適切なシャットダウンの実装が成功したかどうかを確認できます。

Kubernetes クラスターと Redis の設定の詳細については説明しません。完全な設定は Github リポジトリで確認できます。


検証プロセスは次のとおりです。

  1. Redis と Go アプリケーションを Kubernetes にデプロイします。
  2. vegetaを使用して 1000 件のリクエスト (40 秒間で 25/s) を送信します。
  3. vegeta の実行中に、イメージ タグを更新して Kubernetesローリング アップデートを初期化します。
  4. 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()


NotificationContext は、リストされたシグナルの 1 つが到着したとき、返されたstop()関数が呼び出されたとき、または親コンテキストの Done チャネルが閉じられたときのいずれか早い時点で、完了としてマークされた (Done チャネルが閉じられた) 親コンテキストのコピーを返します。


現在の HTTP サーバーの実装にはいくつかの問題があります。

  1. 遅い processRequest goroutine があり、終了シグナルを処理しないため、プログラムは自動的に終了し、実行中のすべての goroutine も終了します。
  2. プログラムは接続を閉じません。


書き直してみましょう。


正常なシャットダウン/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 に渡される匿名関数になります。


SIGTERMシグナルを傍受し、フレームワークに正常にシャットダウンするように要求すると、2 つの重要なことが起こります (簡単に言えば)。

  • フレームワークが受信リクエストの受け入れを停止します
  • 既存の受信リクエストが終了するまで待機します (暗黙的に 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 がポッドを終了することを決定すると、次のイベントが発生します。

  1. ポッドは「終了中」状態に設定され、すべてのサービスのエンドポイント リストから削除されます。
  2. 定義されている場合はpreStopフックが実行されます。
  3. SIGTERMシグナルがポッドに送信されます。これで、アプリケーションは何をすべきかがわかりました。
  4. Kubernetes は、デフォルトでは 30 秒である猶予期間 ( terminationGracePeriodSeconds ) を待機します。
  5. SIGKILLシグナルがポッドに送信され、ポッドが削除されます。

ご覧のとおり、長時間実行される終了プロセスがある場合は、アプリケーションが正常にシャットダウンするのに十分な時間を確保するために、 terminationGracePeriodSeconds設定を増やす必要がある場合があります。

結論

正常なシャットダウンは、データの整合性を保護し、シームレスなユーザー エクスペリエンスを維持し、リソース管理を最適化します。豊富な標準ライブラリと並行性を重視した Go により、開発者は正常なシャットダウン プラクティスを簡単に統合できます。これは、Kubernetes などのコンテナー化またはオーケストレーションされた環境にデプロイされるアプリケーションに不可欠です。

Go コードと Kubernetes マニフェストは、Github リポジトリで見つかります。

リソース