對於一個 Server Client 服務,考慮到網路傳輸成本或是系統內部處理效率等因素,客戶端發出請求之後往往需要等候一小段時間才能獲得回應。
如果在這時伺服器管理者中止服務,可能會導致部份請求未能正常處理(例如 DB 尚未寫入)以及用戶端無法收到預期回應。
這正是 Graceful Shutdown 被設計的初衷:當接收到中止信號時服務並不會馬上退出,而是會等到目前正在服務的連線結束之後再停止服務。
在 POSIX 標準提供 Signals(信號) 機制,讓作業系統或程序之間進行溝通。詳細文件可以參閱 signal(7),各平台(除了特立獨行的 Windows)基本大同小異。
signal在 Go 中,標準函式庫也抽象了對於 posix 信號的處理,並且在 Windows 上也能夠正常使用(有一些細節例外,但這邊我們並不會遇到這種情況)。
使用 singal.NotifyContext() 就可以監聽信號,以下為監聽 os.Interrupt 及 syscall.SIGTERM 的範例:
func (app *App) Run() error {
ctx, stop := signal.NotifyContext(context.TODO(), os.Interrupt, syscall.SIGTERM)
defer stop()
slog.Info("restoring data from disk")
if err := app.srv.RestoreFromDisk(); err != nil {
return
}
go func() {
slog.Info("starting olivine server")
if err := app.srv.ListenAndServe(); err != nil {
return
}
}()
<-ctx.Done()
slog.Info("closing olivine server")
return app.srv.Close()
}
我這邊將 app.srv.ListenAndServe() 移到一個 goroutine 之中,因為它包含了一個用來處理請求的無窮迴圈,這會阻塞主程式運作並讓我們無法使用 <-ctx.Done() 來監聽信號。
或許你已經注意到了,上面的程式忽略了 app.srv.ListenAndServe() 回傳的 err--這並不是個好習慣,我們可以用 Go channel 與 select 來處理 gourinte 中的錯誤:
// file: ./cmd/olivine/app.go
func (app *App) Run() error {
ctx, stop := signal.NotifyContext(context.TODO(), os.Interrupt, syscall.SIGTERM)
defer stop()
slog.Info("restoring data from disk")
if err := app.srv.RestoreFromDisk(); err != nil {
return err
}
errch := make(chan error, 1)
go func() {
slog.Info("starting olivine server")
errch <- app.srv.ListenAndServe()
}()
select {
case <-ctx.Done():
slog.Info("closing olivine server")
if err := app.srv.Close(); err != nil {
return err
}
return <-errch
case err := <-errch:
return err
}
}
為什麼不將 app.srv.RestoreFromDisk() 放入另一個 goroutine 中?
如果用戶在 app.srv.RestoreFromDisk() 執行的時候按下 Ctrl+C,它會關閉一個尚不存在的監聽器,然後無限地執行 app.srv.ListenAndServe() 中的迴圈並且阻塞。
註:一般來說,如果只需要抓取 Ctrl+C 的信號,直接註冊
os.Interrupt即可,但是在一些 Container 環境(或 Kubernetes Pod 編排器)下收到 stop 命令時會向 process 發送syscall.SIGTERM,因此這邊一起註冊會是比較好的實踐。
server.(*simpleSrv).Close()我們可以先實現一個最簡單版本的 server.(*simpleSrv).Close():
// file: ./internal/server/server.go
func (s *simpleSrv) Close() error {
if s.listener == nil {
return nil
}
return s.listener.Close()
}
藉由直接調用 s.listener.Close() 來關閉連線,這會讓 s.listener.Accept() 與 s.listener.Write() 直接回傳 net.ErrClosed。