前回にて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
を読んでいますね。引数には port
と Engine(=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 singalの Notify
を利用することでシステム側の指定した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...
問題なく動きますね。
まとめ
ちゃんとアプリケーションを作るときはテクニックとして覚えておく必要がありそうです。