post Image
Go 1.8 の Graceful Shutdown の振る舞いを調べてみた

Go 1.8 から Graceful Shutdown 機能が標準で提供されるようになりました。これまではサードパーティーライブラリを使ってその機能を実装していた方も多いのではないでしょうか。

標準ライブラリにあるのだからサードパーティーライブラリから移行しようと考えている方も多いでしょう。私も少し前に移行を行ったのですが、移行前に使っていたサードパーティーライブラリと振る舞いが違うという話を同僚から聞いたので Go 1.8 の Graceful Shutdown の振る舞いについて調べてみました。

調査に使ったソースコードは以下になります。

基本的な使い方

context を使ってタイムアウト制御できるのが Go らしくて便利です。例えば、以下のように任意の時間を設定して Shutdown() を呼び出すことでクライアントと通信中であっても強制的に終了することができます。

    ctx, cancel := context.WithTimeout(context.Background(), *s.ShutdownTimeout)
    defer cancel()
    if err := httpServer.Shutdown(ctx); err != nil {
        log.Println("Failed to gracefully shutdown HTTPServer:", err)
    }   
    log.Println("HTTPServer shutdown.")

Shutdown すると Serve から ErrServerClosed が返る

ここで Shutdown() の中身をちょっと覗いてみます。

func (srv *Server) Shutdown(ctx context.Context) error {
    ...
    srv.closeDoneChanLocked()

shutdown() 処理の内部で srv.closeDoneChanLocked() が呼び出されて doneChan が close されます。そうすると Serve() から ErrServerClosed エラーが返されます。

func (srv *Server) Serve(l net.Listener) error {
    ...
    for {
        rw, e := l.Accept()
        if e != nil {
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed

例えば、以下のように ListenAndServe() でエラーが返ってくることを意図していないコードだと問題になるかもしれません。

        if err := httpServer.ListenAndServe(); err != nil {
            log.Fatalln("HTTPServer closed with error:", err)
        }

その場合は、以下のように ErrServerClosed を除外するようにしないといけないかもしれません。

        if err := httpServer.ListenAndServe(); err != nil {                                         
            log.Println("ListenAndServe returns an error", err)                                     
            if err != http.ErrServerClosed {                                                        
                log.Fatalln("HTTPServer closed with error:", err)                                   
            }                                                                                       
        }                                                                                           
        log.Println("ListenAndServe goroutine completed.")                        

net/http/#Server.Serve のドキュメントにもそう書いてありますが、見逃していて後から気付く人も多いのではないでしょうか。

Serve always returns a non-nil error. After Shutdown or Close, the returned error is ErrServerClosed.

また、たまたま見つけたのですが、net/http: document ErrServerClosed #19085ErrServerClosed にもコメントが追加されていました。

// ErrServerClosed is returned by the Server's Serve, ListenAndServe,
// and ListenAndServeTLS methods after a call to Shutdown or Close.
var ErrServerClosed = errors.New("http: Server closed")

Shutdown がタイムアウトしても通信中のコネクションはクローズされない

この振る舞いがなにか問題を引き起こすのかどうか、私はよく分かっていないのですが、こういった動きになっているという話も同僚から聞いたので調べてみました。

以下のようにわざと時間のかかるハンドラー処理を実装します。

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

普通の用途なら Shutdown() が正常終了しようがタイムアウトしようが、シャットダウン後の制御としてそのサーバープロセスを終了するだけであれば、あまり気にする必要はないのかもしれません。

ここでは以下の s.Serve(sigCh) が context の DeadlineExceeded が発生して、タイムアウトによって制御が返ってくると仮定します。その後、スリープしてすぐにプロセスが終了しないようにしています。

    s := NewServer(*port, shutdownTimeout, *mustClose)
    s.Serve(sigCh)
    log.Printf("Server shutdown, but waiting 5 second to exit process ...")
    time.Sleep(5 * time.Second)

このときプロセスの終了前であれば、シャットダウン (タイムアウト) 後もクライアントとのコネクションは接続されたままとなっていてプロセスの終了前であれば、そのまま通信できてしまいます。

$ curl -v localhost:8000/hello
*   Trying ::1...
* Connected to localhost (::1) port 8000 (#0)
> GET /hello HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Date: Sat, 18 Mar 2017 05:28:32 GMT
< Content-Length: 22
<
hello
hello again
bye
* Connection #0 to host localhost left intact

もしこの振る舞いを抑制したいのであれば、Shutdown() を呼び出してタイムアウトが発生したら明示的に Close() を呼び出すようにします。

    ctx, cancel := context.WithTimeout(context.Background(), *s.ShutdownTimeout)
    defer cancel()
    if err := httpServer.Shutdown(ctx); err != nil {
        log.Println("Failed to gracefully shutdown HTTPServer:", err)
        httpServer.Close()
        log.Println("Server closed immediately")
    }                                                                                               
    log.Println("HTTPServer shutdown.")                                                 
$ curl -v localhost:8000/hello
*   Trying ::1...
* Connected to localhost (::1) port 8000 (#0)
> GET /hello HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.43.0
> Accept: */*
>
* Empty reply from server
* Connection #0 to host localhost left intact
curl: (52) Empty reply from server

今度はシャットダウン (タイムアウト) 後、すぐにコネクションがクローズされてエラーになりました。

まとめ

大した話題ではありませんが、サードパーティーのライブラリから移行するときにサーバーの振る舞いが異なると、ログ監視などで発見して調べる人もいるんじゃないかなと思います。

リファレンス


『 Go 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。