Go Gin Graceful-Shutdownについて

Posted on
go gin

前回にてgin-gonic/ginで触りこぼしていたGraceful-Shutdownをちゃんと試してみました。

Graceful Shutdown

名前の通り終了処理にあたってセッションがクローズするまでなるべく待ってあげることです。

調べた結果は簡単に次の通りです。

  • Ginも標準パッケージもGraceful処理は変わらない
  • contextで最長の待ち時間を決定できる

Graceful-ShutdownのManuallyに記載されている方法をまずは試してみます。

$ curl localhost:8080 で5秒後に返信がきます。

$ go run main.go 
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN] 2020/08/25 - 15:19:54 | 200 |  5.003282194s |       127.0.0.1 | GET      "/"
^C  #   <--- SIGINT
2020/08/25 15:19:58 Shutting down server...
[GIN] 2020/08/25 - 15:20:01 | 200 |  5.002745537s |       127.0.0.1 | GET      "/"
2020/08/25 15:20:02 Server exiting
$ 
$ time sh -c "curl localhost:8080"
Welcome Gin Server
real    0m5.030s
user    0m0.007s
sys     0m0.010s

$ time sh -c "curl localhost:8080" # <--- 返信待ちの間にSIGINTを送ったが、正しく5秒かかっている
Welcome Gin Server
real    0m5.025s
user    0m0.007s
sys     0m0.008s

いい感じですね。

コードをみていく

ざっと俯瞰するとmain の goroutineは quit に入ってくる signal をキャッチするまで止めておいて、http serverを別のgoroutineで起動しているようです。signal をキャッチできたら context の最長5秒の制限を渡して、sessionがそれまでにcloseしなければ強制終了するという流れのようです。また、src.Shutdown() を使うことで先ほどの別のgoroutineで動かしているserverを終了しています。

func main() {
	router := gin.Default()
	router.GET("/", func(c *gin.Context) {
		time.Sleep(5 * time.Second)
		c.String(http.StatusOK, "Welcome Gin Server")
	})

	srv := &http.Server{
		Addr:    ":8080",
		Handler: router,
	}

	go func() {
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("listen: %s\n", err)
		}
	}()

	quit := make(chan os.Signal)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	log.Println("Shutting down server...")

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := srv.Shutdown(ctx); err != nil {
		log.Fatal("Server forced to shutdown:", err)
	}

	log.Println("Server exiting")
}

src周りを調べる

r.Run() の代わりに src.ListenAndServe() と標準パッケージでの使い方をしています。とりあえず r.Run() のソースコードをみてみます。

func (engine *Engine) Run(addr ...string) (err error) {
	defer func() { debugPrintError(err) }()

	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)
	err = http.ListenAndServe(address, engine)
	return
}

func resolveAddress(addr []string) string {
	switch len(addr) {
	case 0:
		if port := os.Getenv("PORT"); port != "" {
			debugPrint("Environment variable PORT=\"%s\"", port)
			return ":" + port
		}
		debugPrint("Environment variable PORT is undefined. Using port :8080 by default")
		return ":8080"
	case 1:
		return addr[0]
	default:
		panic("too many parameters")
	}
}

みていただくとわかるように、Port番号を解決して同様に http.ListenAndServe を読んでいますね。引数には portEngine(=handler) を渡しているので、 srv.ListhenAndServe() で同じことをしています。

r.Run() ではなく srv を使うには、次の Shutdown メソッドが関わっていますね。

if err := srv.Shutdown(ctx); err != nil {
    log.Fatal("Server forced to shutdown:", err)
}

src.Shutdown() のソースコードをのぞいてみます。 新規のコネクションは受け付けないようにロックして、現存するセッションがクローズしているか 500msec単位に確認のpollingをしています。ここでctxのtimeoutがきてしまうとerrを返して強制終了に繋がるというロジックの模様です。

var shutdownPollInterval = 500 * time.Millisecond

func (srv *Server) Shutdown(ctx context.Context) error {
	atomic.StoreInt32(&srv.inShutdown, 1)

	srv.mu.Lock()
	lnerr := srv.closeListenersLocked()
	srv.closeDoneChanLocked()
	for _, f := range srv.onShutdown {
		go f()
	}
	srv.mu.Unlock()

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

Notify周りを調べる

まだちゃんと理解できていませんがPackage singalNotify を利用することでシステム側の指定したsignalをキャッチして、システム側の動作をさせずにgoのプログラム側で処理ができるようです。

ちなみに SIGKILL と SIGSTOP はキャッチできないようですね。

The signals SIGKILL and SIGSTOP may not be caught by a program, and therefore cannot be affected by this package.

quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit

標準パッケージでも試してみる

標準パッケージでも問題なくGraceful-shutdownは実装できそうなので試してみました。

func main() {
	srv := http.Server{Addr: ":8080"}
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		res := map[string]string{"test": "sample"}
		b, _ := json.Marshal(res)

		log.Println("start timer...")
		time.Sleep(5 * time.Second)
		w.Header().Set("Content-Type", "application/json")
		w.Write(b)
	})

	go func() {
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("listen: %s\n", err)
		}
	}()

	quit := make(chan os.Signal)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	log.Println("Shutting down server...")

	ctx, cansel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cansel()
	if err := srv.Shutdown(ctx); err != nil {
		log.Fatal("Server forced to shutdown: ", err)
	}
	log.Println("Server existing...")
}
$ go run main.go 
2020/08/25 16:47:10 start timer...
^C2020/08/25 16:47:11 Shutting down server...
2020/08/25 16:47:15 Server existing...

問題なく動きますね。

まとめ

ちゃんとアプリケーションを作るときはテクニックとして覚えておく必要がありそうです。