Goのhttp.ServerはどのようにGraceful Shutdownをしているか

サーバーのプログラムを書いてると、Graceful Shutdown という単語をときどき目にする。 直訳すると「優雅な終了処理」。美しい響き。

サーバーを停止する際は、一瞬で全部止めるのではなく、適切な手順を踏んできれいに停止しましょうということみたい。 しかし、✨優雅✨ って言われても……具体的に何をしたら優雅な停止になるのか?

Gracefulって具体的にどういうこと??

これは、明確な定義があるわけではない。 どんな停止処理が「優雅」かどうかは対象のアプリケーションによって異なるはず。

とはいえ、実際にGraceful shutdownを実装している例をみることで、「あぁ、このソフトウェアではこのような方法を取っているのか」って学びはありそうだと思った、ので Go 1.13 の net/http のGraceful shutdown実装 を覗いた。

https://golang.org/pkg/net/http/#Server.Shutdown

関数の説明文がとてもわかりやすい。簡単にまとめると

  1. 新規接続の受け入れを停止する
  2. 待機状態の接続を切断する
  3. 処理中の接続が終わるのを待つ

新規接続の受け入れを停止する

今からサーバーを停止することが決まっているので、まずは新規の接続は受け付けないようにする。

具体的には、TCP Listenerを閉じているだけ。

   for ln := range s.listeners {
        if cerr := (*ln).Close(); cerr != nil && err == nil {
            err = cerr
        }
        delete(s.listeners, ln)
    }

待機状態の接続を切断する

接続の状態が待機中 (Idle) のものをすべて切断している。 逆に言うと、Idle 状態以外の接続はそのまま維持される。

   quiescent := true
    for c := range s.activeConn {
        st, unixSec := c.getState()
        // Issue 22682: treat StateNew connections as if
        // they're idle if we haven't read the first request's
        // header in over 5 seconds.
        if st == StateNew && unixSec < time.Now().Unix()-5 {
            st = StateIdle
        }
        if st != StateIdle || unixSec == 0 {
            // Assume unixSec == 0 means it's a very new
            // connection, without state set yet.
            quiescent = false
            continue
        }
        c.rwc.Close()
        delete(s.activeConn, c)
    }

HTTPはステートレスなのに待機状態の接続なんてあるのか・・? って一瞬思ったけど、keep-aliveによって保持されている接続らしい。なるほど!

    // StateIdle represents a connection that has finished
    // handling a request and is in the keep-alive state, waiting
    // for a new request. Connections transition from StateIdle
    // to either StateActive or StateClosed.
    StateIdle

処理中の接続が終わるのを待つ

ここがとても✨優雅✨ なポイント。 現在処理中のリクエストが終わるまで待つことで、中途半端な処理を残さずにきれいにサーバーを停止するというわけだ。

↑にあげた通り、Idle 以外の状態の接続はそのまま維持されているので、これらが終わるのを待つということになる。 Goらしい方法で、time.Ticker を使ってfor-selectで待ち続けている。

   ticker := time.NewTicker(shutdownPollInterval)
    defer ticker.Stop()
    for {
        if srv.closeIdleConns() {
            return lnerr
        }
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-ticker.C:
        }
    }

待つにも限度がある?

現在処理中のリクエストは待つ……といっても無限に待つわけにはいかない。

あまりにも長時間処理が続く場合は、どこかで諦めて強制停止しなければならない。 これを実現するためにcontextが活用されている。

       select {
        case <-ctx.Done():
            return ctx.Err()

待つ部分のコードのここが重要。Tickerでポーリング的に待ち続けるのだが、 contextが中断された場合は即座に待つのをやめている。

注意すべきは、contextによって中断された場合、処理中の接続は切断されないこと。 待つのをやめて、そのままShutdown関数はエラー(おそらく context.DeadlineExceeded)を返すので、その時まだ処理中の接続はそのまま続いてしまう。

現実的にはShutdownを呼んだ後はサーバーのプロセスが一度死ぬことがほとんどだと思うので、接続は強制切断されるだろう。(つまり、優雅な停止ではない

まぁこれは、さすがにcontextの期限を超えるほど長時間の処理までは待てないよってことだろう。

この挙動は次の記事でも説明されている。

また、contextの待ち時間をものすごく長くしても、大元のプロセスが死んでしまったらすべては終わる。 こうなるとまた、優雅な停止はできなくなる。

たとえばクラウドでよく使われるKubernetesなんかだと、インフラ側の都合でPodが落とされるときは SIGTERM が送られる → preStop フックが呼び出される → 猶予期間(gracePeriod)後、SIGKILL によって強制停止される(多分)。

という流れみたいなので、やはりあまりにも長時間になると、待つのを諦めて強制停止するしかない という方針のようだ。

まとめ

Graceful Shutdownは明確な定義はないと思われるが、Goのnet/httpの実装を見ることで、できるだけきれいにサーバーを停止するとはどういうことか知ることができた。

また、無限に待ち続けるわけにもいかないので、一定時間待ってダメだったらGracefulじゃない停止になることもやむを得ない、という感じらしい。