概要
以前は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の導入を紹介しました。