iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Software Development

從零開始打造 Redis:以 Go 建立 Production Ready 應用程式 系列 第 18

[Day 18] Graceful Shutdown(上):信號與處理

  • 分享至 

  • xImage
  •  

對於一個 Server Client 服務,考慮到網路傳輸成本或是系統內部處理效率等因素,客戶端發出請求之後往往需要等候一小段時間才能獲得回應。

如果在這時伺服器管理者中止服務,可能會導致部份請求未能正常處理(例如 DB 尚未寫入)以及用戶端無法收到預期回應。

這正是 Graceful Shutdown 被設計的初衷:當接收到中止信號時服務並不會馬上退出,而是會等到目前正在服務的連線結束之後再停止服務。

POSIX 信號處理

在 POSIX 標準提供 Signals(信號) 機制,讓作業系統或程序之間進行溝通。詳細文件可以參閱 signal(7),各平台(除了特立獨行的 Windows)基本大同小異。

Go 的 signal

在 Go 中,標準函式庫也抽象了對於 posix 信號的處理,並且在 Windows 上也能夠正常使用(有一些細節例外,但這邊我們並不會遇到這種情況)。

使用 singal.NotifyContext() 就可以監聽信號,以下為監聽 os.Interruptsyscall.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


上一篇
[Day 17] 測試(下):stub 與 mock
下一篇
[Day 19] Graceful Shutdown(中):處理尚未完成的請求與閒置請求
系列文
從零開始打造 Redis:以 Go 建立 Production Ready 應用程式 29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言