上京エンジニアの葛藤

都会に染まる日々

Go で Graceful Shutdown な Web サーバーを書く

概要

Go で Graceful Shutdown な Web サーバーを書きたかったのでその備忘録です。

結論

net/http パッケージの Server.Shutdown() を使え
http package - net/http - Go Packages
それだけなんですが、動作確認しながら挙動を追いたいので Web サーバーを書きながら確認してみたいと思います。

Graceful Shutdown とは

プロセス内で実行中のプログラムの処理を適切に完了させ、プロセスを安全に終了させることです。
Web サーバーにおける Graceful Shutdown は現在処理中のリクエストを適切に完了させ、新しいリクエストを受け付けずにプロセスを終了させることです。

動作確認

用意したサンプルコードです。

package main

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

func main() {
    ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer cancel()

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        for i := 1; i <= 5; i++ {
            fmt.Println(i)
            time.Sleep(1 * time.Second)
        }
        fmt.Println("Hello, World!")
    })

    server := http.Server{
        Addr:    ":8080",
        Handler: nil,
    }

    done := make(chan error, 1)
    go func() {
        done <- server.ListenAndServe()
    }()

    select {
    case err := <-done:
        if err != http.ErrServerClosed {
            log.Fatalf("HTTP server ListenAndServe: %v", err)
        }
    case <-ctx.Done():
        fmt.Println("Server stopping")
        c, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        if err := server.Shutdown(c); err != nil {
            log.Fatalf("HTTP server Shutdown: %v", err)
        }
        fmt.Println("Server gracefully stopped")
    }
}

リクエストを受けたら 5秒スリープした後にレスポンスを返すようなエンドポイントを用意して確認してみます。

確認方法

  1. Web サーバーを serve する
  2. / に GET リクエストを送信する
  3. リクエスト受信後 Web サーバーに Ctrl + C で SIGINT を送信する
  4. GET リクエストが正常にレスポンスを返す

結果

1
2
^CServer stopping
3
4
5
Hello, World!
Server gracefully stopped
// 別 terminal でリクエストをした場合
$ curl http://localhost:8080
curl: (7) Failed to connect to localhost port 8080 after 7 ms: Couldn't connect to server

期待通り正常にレスポンスを返した後にサーバーが落ちていること、新たにリクエストを受け付けていないことが分かりました。
ここで重要なのはタイムアウトを設定したコンテキストを Shutdown メソッドに渡している点で、この例だと SIGINT を受け取って Shutdown を呼び出してから最長 10秒はサーバーが落ちないようになっています。
このタイムアウトが 5秒以下だと SIGINT を受けてから5秒後にはコンテキストのエラーが返され、リクエストの処理が完了前にサーバーは落ちてしまいます。

// タイムアウトを 1秒に設定した例
1
2
^CServer stopping
3
2023/11/08 00:35:28 HTTP server Shutdown: context deadline exceeded
exit status 1

※ドキュメントにも記載があります。

If the provided context expires before the shutdown is complete, Shutdown returns the context's error, otherwise it returns any error returned from closing the Server's underlying Listener(s).

タイムアウトを設定しないと処理に時間がかかるとそれだけ待つことになるため、いつまでもデプロイが完了しないなどの問題が出るので本番環境ではこの辺りの設定を考えないといけません。