💬

グレースフルシャットダウンの必要性

2024/03/28に公開

こんにちは、@nerusan_mainです。

皆さんは、「グレースフルシャットダウン」という言葉を聞いたことがありますか?
今回は、その「グレースフルシャットダウン」について以下の内容でご説明します。

  • グレースフルシャットダウンとは何か?
  • グレースフルシャットダウンの必要性は何か?
  • Go言語におけるグレースフルシャットダウンの実装方法

それでは、順番に見ていきましょう!

グレースフルシャットダウンとは

「グレースフルシャットダウン」とは、プログラムやシステムを正常に終了させるための手法です。
通常、プログラムやシステムは予期せぬエラーや問題によって異常終了することがありますが、グレースフルシャットダウンを実装することで、異常終了を最小限に抑えることができます。

グレースフルシャットダウンの必要性

グレースフルシャットダウンを実装することには以下のような利点があります:

  1. データの損失を最小限に抑えることができる。
  2. ユーザーエクスペリエンスを向上させることができる。
  3. システムの安定性を高めることができる。

例えば、サーバーが突然終了すると、データが保存されず整合性が失われたり、処理が途中で中断する可能性があります。
これは、処理の整合性が求められるサービスにとっては望ましくありません。

グレースフルシャットダウンでは、進行中の処理を終了してからシャットダウンするため、データが保存されないということは起きません。

サーバーっていつ異常終了するの?

これまで見てきたように、グレースフルシャットダウンの有効性は理解できましたね。
しかし、本番のサーバーが突然終了することはあるのでしょうか?
自分自身も同じ疑問を抱いたことがあります😅

近年、サーバーレスでサービスを構築することが一般的になってきていますが、
このサーバーレスのサーバーでも異常終了が発生する可能性があります。

例えば、AWSのECSとFargateで構成されたサービスを考えてみましょう。

ECSは、AWS内部でハードウェア障害やセキュリティ脆弱性が検出された場合、
新しいECSタスクに切り替える「リタイア」というイベントが発生します。

リタイアが発生すると、ECSはサーバー(Fargate)にリタイア通知(SIGTERMシグナル)を送信します。
その後、起動中のサーバーを中断し、プラットフォームを更新し、サーバーを再起動します。

https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-maintenance.html

このSIGTERMシグナルが重要であり、この通知を受信した後、
デフォルトで30秒後には強制終了するSIGKILLシグナルを送信して、サーバーを強制終了させます。
この強制終了によりデータの不整合が発生する可能性があるため、グレースフルシャットダウンではSIGTERMシグナルを受け取った後、正常にシャットダウンしてデータの不整合を防ぎます。

例えば、パソコンの電源を突然切るのが強制終了であり、
30秒後に終了すると通知を受けてから処理を最後まで完了し、その後正規の手順でシステムを終了するのが、グレースフルシャットダウンです。

グレースフルシャットダウンお処理の流れ

処理の流れは以下になります。

  1. SIGTERMシグナル受け取る
  2. 今やっている処理を最後まで終わらせる
  3. シャットダウンを行う

グレースフルシャットダウンの実装方法

グレースフルシャットダウンを実現するためのコード例を以下に示します。
環境は以下の通りです。

  • フレームワーク: Gin
  • Goバージョン: 1.22
server.go
// サーバー起動
//
// @params
// ctx コンテキスト
func (s *Server) Run(ctx context.Context) error {
	ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
	defer stop()
	eg, ctx := errgroup.WithContext(ctx)
	eg.Go(func() error {
		// http.ErrServerClosed は
		// http.Server.Shutdown() が正常に終了したことを示すので異常ではない
		if err := s.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Printf("failed to close: %+v", err)
			return err
		}
		return nil
	})

	// SIGTERMシグナルなどの終了通知を待つ
	<-ctx.Done()
	// サーバーをシャットダウン
	log.Println("shutdown server ...")
	if err := s.srv.Shutdown(context.Background()); err != nil {
		log.Printf("failed to shutdown: %+v", err)
	}
	// 別ルーチンのグレースフルシャットダウンの終了を待つ
	return eg.Wait()
}

全てのコードはこちらで確認できます。

関数の中で、signal.NotifyContext関数を使用して新たなコンテキストを作成しています。
この関数は、指定した終了シグナル(ここではos.Interruptとsyscall.SIGTERM)が受信されたときにコンテキストをキャンセルします。
これにより、Ctrl+Cが押されたときやSIGTERMシグナルが送信されたときにサーバーを停止することができます。

次に、errgroup.WithContext関数を使用して新たなエラーグループとコンテキストを作成しています。
エラーグループは、複数のゴルーチンを管理し、そのいずれかでエラーが発生した場合にそれを報告します。

エラーグループのGoメソッドを使用して新たなゴルーチンを作成し、その中でhttp.ServerのListenAndServeメソッドを呼び出してサーバーを起動しています。
このメソッドは、エラーが発生した場合にそれを返しますが、サーバーが正常にシャットダウンした場合にはhttp.ErrServerClosedを返します。
そのため、エラーチェックではこの値を無視しています。

最後に、コンテキストのDoneメソッドを使用して終了シグナルを待ち、サーバーのシャットダウンを開始します。
シャットダウンは、アクティブな接続を中断せずにサーバーを正常にシャットダウンします。
シャットダウンは、まず開いているすべてのリスナーを閉じ、次にアイドル状態の接続をすべて閉じ、接続がアイドル状態に戻るまで無期限に待機してからシャットダウンします。

https://pkg.go.dev/net/http#Server.Shutdown

そして、エラーグループのWaitメソッドを使用して、サーバーを起動しているゴルーチンが終了するのを待ちます。
これにより、サーバーはすべてのリクエストを処理し終えてからシャットダウンする、いわゆる「グレースフルシャットダウン」を実現しています。

流れをまとめると以下になります。

  1. ListenAndServeメソッドでサーバーを起動し、<-ctx.Done()でキャンセル通知が来るまで待機
  2. SIGTERMシグナルで受信した後にShutdownメソッドでシャットダウン処理を実行
  3. 正常にシャットダウンが成功したらListenAndServeメソッドはhttp.ErrServerClosedを返す
  4. eg.Wait()のエラーにnilが返却

まとめ

以上がグレースフルシャットダウンの概要と、その実装方法についての説明です。
グレースフルシャットダウンを実装することで、プログラムやシステムの安定性を向上させることができます。
特に、クラウド上でサービスを提供する場合は、必ずこの実装を検討するようにしましょう。

GitHubで編集を提案

Discussion