【Go/Gin】サーバーを graceful shutdown に対応させる

golang と gin を利用して構築した web サーバーを graceful shutdown できるようにしてみたいと思います。 graceful shutdown は、サーバーが稼働中に予期せず終了することによる問題を防ぐための仕組みです。

目次

サーバーを構築する

http://localhost:8080 にアクセスすると、5秒待ってからメッセージを返すサーバーを立ててみます。

// main.go
package main

import (
    "time"

    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    r.GET("/", func(c *gin.Context) {
        time.Sleep(5 * time.Second)

        c.JSON(200, gin.H{
            "message": "Hello",
        })
    })

    r.Run() // listen and serve on 0.0.0.0:8080
}

リクエストを送った後、5秒以内に、「Ctl + C」でサーバーを落とします。

$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
^Csignal: interrupt

メッセージが返ってくる前に、サーバーが終了しました。

graceful shutdown に対応させる

gin のドキュメントのようにコードを修正します。

// main.go
package main

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

    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    router.GET("/", func(c *gin.Context) {
        time.Sleep(5 * time.Second)
        c.String(http.StatusOK, "Welcome Gin Server")
    })

    srv := &http.Server{
        Addr:    ":8080",
        Handler: router,
    }

    go func() {
        // service connections
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("listen: %s\n", err)
        }
    }()

    // Wait for interrupt signal to gracefully shutdown the server with
    // a timeout of 5 seconds.
    quit := make(chan os.Signal)
    // kill (no param) default send syscanll.SIGTERM
    // kill -2 is syscall.SIGINT
    // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("Shutdown Server ...")

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal("Server Shutdown:", err)
    }
    // catching ctx.Done(). timeout of 5 seconds.
    select {
    case <-ctx.Done():
        log.Println("timeout of 5 seconds.")
    }
    log.Println("Server exiting")
}

先ほどと同じように、リクエスト送った後、5秒以内に「Ctl + C」をしてみます。

$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
^C2023/09/10 00:25:27 Shutdown Server ...
[GIN] 2023/09/10 - 00:25:30 | 200 |  5.001055625s |             ::1 | GET      "/"
2023/09/10 00:25:32 timeout of 5 seconds.
2023/09/10 00:25:32 Server exiting

「Ctl + C」を実行した直後、^C2023/09/10 00:25:27 Shutdown Server ... が出力されますが、すぐにはサーバーが終了せず、リクエストを捌けています。

srv.Shutdown(ctx)が実行されると、サーバーは新しい接続を受け付けるのを停止し、既存の全ての接続が閉じるのを待つようになります。
ここで、srv.Shutdownはコンテキスト(ctx)を引数として受け取ります。このコンテキストは、サーバーがシャットダウンを完了するまでの最大時間(タイムアウト)を設定します。
サンプルコードでは、5秒に設定されています。

試しに、/にアクセスした時の処理での待機時間を、5秒から10秒に伸ばし、先ほど同様に「Ctl + C」を送ってみます。

// main.go
...
    router.GET("/", func(c *gin.Context) {
        time.Sleep(10 * time.Second)
        c.String(http.StatusOK, "Welcome Gin Server")
    })
...
$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
^C2023/09/10 00:33:58 Shutdown Server ...
2023/09/10 00:34:03 Server Shutdown:context deadline exceeded
exit status 1

サーバーの処理時間 > サーバーがシャットダウンを完了するまでの最大時間 となり、強制的にサーバーが落とされました。

おわりに

gin で立てたサーバーを graceful shutdown に対応させてみました。
簡単ですね!