3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

グレースフルシャットダウンの責務

Last updated at Posted at 2025-12-19

この記事はZOZOアドベントカレンダー2025 Series6 20日目の記事です。

1. はじめに

現在、私は26卒内定者アルバイトとしてバックエンド(Go)の開発に携わっています。
実際に、ZOZOTOWNのようなECサービスの開発現場に入り、個人開発では気づけなかった学びを日々得ています。例えば、Kubernetes Podの停止時の挙動や、APIにおける認可処理などです。

今回はその学びの1つであるグレースフルシャットダウンについて共有しようと思います!

起動中のアプリケーションでのPodの停止や再起動などの行為一つとっても、そこには「今まさにリクエストを送ってくれているユーザー」への配慮が必要です。もし操作の途中でサーバーを強制終了させてしまったら…想像するだけで冷や汗が出ます。

この記事では、「Go言語のアプリケーションコードとして、どうやって行儀よく(Gracefulに)終了するか」に焦点を当てます 。

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

簡単に言うと、「処理中のタスクをできる限り完了させてから、安全にシステムを停止すること」です。

これを行うメリットの1つはユーザーへの悪影響を最小限にすることです。

例えば、ユーザーが「注文確定」ボタンを押して、サーバー側でリクエストを処理している最中にサーバーが強制終了したらどうなるでしょうか?

リクエストがエラーになって、ユーザーはフォームの再入力を強いられたり、エラーになっている間に商品の在庫がなくなってしまうといった事態が発生する恐れがあります。

グレースフルシャットダウンを実装することで、こうした重要な処理を途中で切ることなく、安全に完了させてからサーバーを閉じることができます。

3.実装

以下は、公式ドキュメント[1][2]を参考に実装した検証用コードです。

main.go
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	// 1. サーバーの設定
	srv := &http.Server{
		Addr: ":8080",
	}

    idleConnsClosed := make(chan struct{})

	// ハンドラの設定(10秒かかる重い処理をシミュレート)
	http.HandleFunc("/heavy", func(w http.ResponseWriter, r *http.Request) {
		log.Println("⏳ 重い処理を開始しました (所要時間: 10秒)...")
		
		// 処理のシミュレーション
		time.Sleep(10 * time.Second)
		
		fmt.Fprintln(w, "✅ 処理が完了しました!")
		log.Println("✨ 重い処理が完了しました")
	})

	// 2. シグナルの待機とシャットダウン処理をサブゴルーチンで実行
	// SIGINT (Ctrl+C) や SIGTERM (killコマンド) を受け取るチャネル
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

	go func() {
		// シグナルが来るまでここでブロック(待機)
		<-quit
		
		log.Println("\n🛑 シャットダウンシグナルを受信しました。終了処理を開始します...")

		// 最大10秒間、処理中のリクエストが終わるのを待ちます
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()

		if err := srv.Shutdown(shutdownCtx); err != nil {
			// srv.Shutdownがエラーを返した場合、致命的なエラーとしてログに出力
			log.Fatalf("❌ シャットダウン中にエラーが発生しました: %v", err)
		}
		log.Println("👋 サーバーを正常に停止しました")

       close(idleConnsClosed)
	}()

	// 3. サーバーをメインゴルーチンで起動
	// ListenAndServeはブロックするため、サーバー停止(Shutdown)がなければここでプログラムは停止します。
	log.Println("🚀 サーバーを起動しました (http://localhost:8080)")
	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		// http.ErrServerClosed は Shutdown() が呼ばれた際に発生するため、これはエラーとして扱わない
		log.Fatalf("サーバー起動エラー: %v", err)
	}
    //   ListenAndServe終了後、チャネルが閉じられるのを待ちます
    //   この行は、Shutdown()が完了するまでプログラムをブロックします。
	<-idleConnsClosed
    Printf("main関数が終了します")
	
}

コードの流れ

1. サーバー起動準備とシグナル待機ゴルーチンの設定:

  • http.Server を設定し、同時にOSの停止シグナル(SIGINT, SIGTERM)を待ち受けるサブゴルーチンを起動します。

  • このサブゴルーチンは、シグナルを受信するまでブロックします。

  • idleConnsClosed := make(chan struct{}) が宣言され、シャットダウン完了通知の役割を担います。

2. ハンドラの設定:

  • /heavy エンドポイントを設定します。この処理は10秒間かかります。

3. サーバー起動:

  • メインゴルーチンで srv.ListenAndServe() を呼び出し、サーバーをポート:8080で起動します。メインゴルーチンはここでブロックされます。

4. グレースフルシャットダウン:

  • OSから停止シグナルを受信するとサブゴルーチンの <-quit がブロック解除されます。

  • Shutdownは、新しいリクエストの受け付けを停止し、既存の処理中のリクエストが完了するのを最大10秒間待ちます。

  • srv.Shutdown() が開始すると、メインゴルーチンの srv.ListenAndServe()http.ErrServerClosedを返してブロックを解除し、次の行に進みます。

  • サブゴルーチン内では、srv.Shutdown() が完了した直後に close(idleConnsClosed) が実行されます。close()はチャネルを閉じ、メインゴルーチンへの通知を行います。

5. 終了:

  • メインゴルーチンはsrv.ListenAndServe()の終了後、<-idleConnsClosed で待機します。

  • サブゴルーチンから送られた close(idleConnsClosed) によって、この待機が解除されます。

  • log.Println("main関数が終了します")が実行され、プログラムプロセス全体が終了します。

参考文献 [1] net/http - Go Packages [2] os/signal - Go Packages

4.検証

7854b5d6-bace-4602-948a-9af7b2724111.gif

解説:

  • 左の画面(サーバー)では停止シグナルを受信後も、処理中のリクエストが完了するまで終了を待機しています。

  • 右の画面(クライアント)には、エラーではなく正常に「処理が完了しました(200 OK)」が返ってきています。

  • 処理が終わったのを見届けてから、サーバーが終了しています。

5.最後に

サービスの要件を満たすだけでなく、そのサービスの向こう側にいるユーザーの体験まで想像し、配慮することが、バックエンドエンジニアとしての重要な役割であるという気付きを得ることができました。まだ未熟者ですが、自分から積極的に技術イベントに参加して、知見を増やしZOZOのプロダクトをより良いものにしていきたいと思いました!

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?