概要
以前はGraceful shutdownをするために以下のようなライブラリを使用していました。
しかしながらGo 1.8 からGraceful Shutdown機能が標準で提供されるようになりました。
今回はその導入方法を紹介します。
環境
- golang 1.10.3
これまでの問題
例えば以下のような重い処理が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-C
でSIGINT
を投げると
$ ./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です。
処理中のリクエストがあれば、SIGINT
やSIGTERM
といったシグナルが飛んでも設定した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をハンドリングする
SIGINT
とSIGTERM
の両方
- 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でも挙がっていましたが
k8sのpodはSIGTERM
をハンドリングしないとすぐ停止してしまうため、処理中のリクエストがきちんと返らない問題が起きます。
なのでGraceful shutdownをアプリ側で対応する必要があります。
AWSの場合
ALBのconnection drainingを使えばアプリ側で対応しなくても処理中のリクエストを返すことができそうです。
ただAWSのLBの機能にロックインされてしまうため、インフラを意識しないようにするためにはアプリ側の責務としてGraceful shutdownを実装したほうが良さそうです。
またECSを利用している場合、タスク終了時にSIGTERM
が呼ばれるので今回のようにハンドリングできますが、デフォルトだと30秒後にSIGKILL
が呼ばれて強制的に停止させられるため、ECS_CONTAINER_STOP_TIMEOUT
を5min
程度に延長することをお勧めします。
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の導入を紹介しました。