これまで何度か HTTP Server の Graceful Shutdown について記事を書きました。
- Go 言語で Graceful Restart をする
- Go 言語で Graceful Restart をするときに取りこぼしを少なくする
- Go1.8 の Graceful Shutdown と go-gracedown の対応
最終的に Go 1.8 で Server.Shutdown
が導入され、この件は解決を見ました。
しかし、最近「あれ?本当に正しく Server.Shutdown
使えている?」と疑問に思い、少し考えてみました。
というか ↑ の記事もまだ考慮が足りない気がする。
ぼくのかんがえたさいきょうの Go HTTP サーバー起動方法
とりあえず完成形のコード。
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// シグナルを待ち受ける
chSignal := make(chan os.Signal, 1)
signal.Notify(chSignal, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(chSignal)
// 起動したいサーバーを準備
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "from pid %d.\n", os.Getpid())
})
s := &http.Server{
Addr: ":8080",
Handler: mux,
}
http.Handle("/", h)
// 別 goroutine で起動する
chServe := make(chan error, 1)
go func() {
defer close(chServe)
chServe <- s.ListenAndServe()
}()
select {
case err := <-chServe:
// ListenAndServe はたまに失敗するので、失敗したら死んでほしい
// 他のプロセスが使用しているポートを開いたときとか
log.Fatal(err)
case <-chSignal:
}
signal.Stop(chSignal)
// Graceful Shutdown 開始
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := s.Shutdown(ctx); err != nil {
log.Fatal(err)
}
// HTTPサーバーが完全に終了するのを待つ
// (OSがリソース閉じてくれるはずだけど、お行儀よく)
s.Close()
<-chServe // http.ErrServerClosed が返ってくるのがわかっているので、特に何もしない。
}
大事なのは、ListenAndServe
がサーバーの起動に失敗したときはプロセスを終了させること。
HTTP サーバーとしての役割を果たさないのに、プロセスだけ生き残っても仕方がないのでね。
ただし ListenAndServe
がエラーを返したらプロセス即終了、という訳にはいかない。
たとえば、Shutdown
を呼び出すと ListenAndServe
は http.ErrServerClosed
エラーで終了してしまう。
この場合はプロセスを終了させてはいけなくって、すべてのリクエストが正常に処理されるのを待つ必要がある。
「ListenAndServe
の失敗監視」と「シグナルの受信監視」を並行してやらないといけないので、意外と難しい。
まあ、最近はロードバランサーがよしなに取り計らってくれるので、このへん多少雑でも構わない。