Carpe Diem

備忘録

GoでGraceful Shutdown

概要

以前はGraceful shutdownをするために以下のようなライブラリを使用していました。

github.com

しかしながらGo 1.8 からGraceful Shutdown機能が標準で提供されるようになりました。
今回はその導入方法を紹介します。

環境

これまでの問題

例えば以下のような重い処理がHandlerにあるとします。
簡単のためsleepで実装しています。

func hello(w http.ResponseWriter, r *http.Request) {
    log.Println("heavy process starts")
    time.Sleep(5 * time.Second)
    log.Println("done")
    w.Header().Set("Content-Type", "text/plain")
    w.Write([]byte("hello\n"))
}

func main() {
    http.HandleFunc("/hello", hello)
    http.ListenAndServe(":8000", nil)
}

正常系

これをcurlで叩くと、ずっとサーバが動いていれば

$ ./main 
2018/06/19 11:32:57 heavy process starts
2018/06/19 11:33:02 done

のように処理が完了し、

$ curl localhost:8000/hello
hello

クライアントにはこのようにレスポンスが返ります。

異常系

しかしサーバ側はデプロイなどで停止することも多々あります。
Ctrl-CSIGINTを投げると

$ ./main 
2018/06/19 11:32:43 heavy process starts
^C

このように途中で処理が中断され

$ curl localhost:8000/hello
curl: (52) Empty reply from server

クライアント側はエラーになってしまいます。

Graceful Shutdownだと

これを解決するのがGraceful shutdownです。
処理中のリクエストがあれば、SIGINTSIGTERMといったシグナルが飛んでも設定したTimeout以内の時間処理を続け、ちゃんと返すようにします。
また新規のリクエストは受け付けなくなります。

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/hello", hello)
    srv := &http.Server{
        Addr:    ":8000",
        Handler: mux,
    }

    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            // Error starting or closing listener:
            log.Fatalln("Server closed with error:", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGTERM, os.Interrupt)
    log.Printf("SIGNAL %d received, then shutting down...\n", <-quit)

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        // Error from closing listeners, or context timeout:
        log.Println("Failed to gracefully shutdown:", err)
    }
    log.Println("Server shutdown")
}

ポイントは

  • signalをハンドリングする
    • SIGINTSIGTERMの両方
  • Shutdown()メソッドを利用する
    • context.WithTimeout()でTimeoutを設定する

です。

異常系

先程のようにリクエストの途中でCtrl-Cで中断してみます。

$ ./main 
2018/06/19 11:48:04 heavy process starts
^C2018/06/19 11:48:08 SIGNAL 2 received, then shutting down...
2018/06/19 11:48:08 ListenAndServe returns an error http: Server closed
2018/06/19 11:48:09 done
2018/06/19 11:48:09 Server shutdown

SIGINTが飛びましたが、プロセスは停止せず処理を続けていることが分かります。
そしてリクエストを処理し終わるとプロセスを終了しています。

$ curl localhost:8000/hello
hello

レスポンスもちゃんと返ってますね。

ユースケース

dockerの場合

例えばdockerはstop時にSIGTERMを投げる、とドキュメントに書いてあります。

The main process inside the container will receive SIGTERM, and after a grace period, SIGKILL.

ref: https://docs.docker.com/engine/reference/commandline/stop/

なのでリクエストの処理が中断されないよう、signalをハンドリングしてgraceful shutdownすることが重要です。

Kubernetesの場合

こちらのstack overflowでも挙がっていましたが

stackoverflow.com

k8sのpodはSIGTERMをハンドリングしないとすぐ停止してしまうため、処理中のリクエストがきちんと返らない問題が起きます。
なのでGraceful shutdownをアプリ側で対応する必要があります。

AWSの場合

ALBのconnection drainingを使えばアプリ側で対応しなくても処理中のリクエストを返すことができそうです。
ただAWSのLBの機能にロックインされてしまうため、インフラを意識しないようにするためにはアプリ側の責務としてGraceful shutdownを実装したほうが良さそうです。

またECSを利用している場合、タスク終了時にSIGTERMが呼ばれるので今回のようにハンドリングできますが、デフォルトだと30秒後にSIGKILLが呼ばれて強制的に停止させられるため、ECS_CONTAINER_STOP_TIMEOUT5min程度に延長することをお勧めします。

When StopTask is called on a task, the equivalent of docker stop is issued to the containers running in the task. This results in a SIGTERM and a default 30-second timeout, after which SIGKILL is sent and the containers are forcibly stopped. If the container handles the SIGTERM gracefully and exits within 30 seconds from receiving it, no SIGKILL is sent.

ref: StopTask - Amazon Elastic Container Service

まとめ

GolangでのGraceful Shutdownの導入を紹介しました。

ソース