優雅關閉
在 v4 中關閉一個流通過改變流的生命週期實現
v4 中流有一個 G(goroutine)專門負責管理流的生命週期,並使用狀態自動機來實現狀態變更。
但是在退出發佈者或者訂閱者,仍然遇到一些問題,首先發布者和訂閱者各自有自己的 G ,多數用於網絡通訊。此外退出分爲兩種情況,一種是內部原因,比如超時,出錯等。另一種是外部原因,比如用戶手動關閉,連接斷開等。很難優雅的統一處理。
v5 中通過第一性原理思考,移除不必要的 G,不再有管理生命週期的狀態機,流和發佈者變成同一個概念,實現主動被動退出的統一處理,使得代碼進一步簡化。
優雅關閉流和訂閱者
爲了儘量減少鎖和 G的使用,因此選擇使用動態Select方式,在 Server 層面的一個大 G 中實現,對發佈者和訂閱者的退出監聽。下面是僞代碼,爲了方便理解
select {
case <-server 退出信號:
退出
case <-定時器信號:
定時任務
case <-事件總線信號:
事件處理
case <-發佈者 1 退出信號:
case <-發佈者 2 退出信號:
...
case <-訂閱者 1 退出信號:
case <-訂閱者 2 退出信號:
...
}
爲啥優雅呢?因爲在一個 G 裏面處理,不需要鎖,可以方便的修改發佈者集合,訂閱者集合,以及等待區(訂閱時還沒有發佈者)等很多併發讀寫的場景。實際上你無法直接寫出這個 select,因爲發佈者和訂閱者動態添加和刪除的。此時就需要用到 reflect.Select(cases)
了。
優雅關閉 Server
有了優雅關閉發佈者和訂閱者,那麼剩下的就比較簡單了,就是要優雅關閉插件。在 v4 中並不支持這種操作。爲了能實現動態熱更新配置等場景,優雅關閉插件就很重要,因此設計的時候就考慮到了監聽和退出監聽的邏輯。因此在 sever 退出的時候,需要
- 退出所有發佈者
- 退出所有訂閱者
- 關閉所有插件的連接監聽
- 關閉 server 級的 http 和 tcp 監聽
所有這些對象都包含了可以用來退出的 context
type Unit struct {
StartTime time.Time
*slog.Logger `json:"-" yaml:"-"`
context.Context `json:"-" yaml:"-"`
context.CancelCauseFunc `json:"-" yaml:"-"`
}
func (unit *Unit) Stop(err error) {
unit.Info("stop", "reason", err.Error())
unit.CancelCauseFunc(err)
}
通過傳遞一個 error 對象,可以用來標記退出的原因。
Server 熱重啓
本文所說的熱重啓並非極端意義的連接保持,那種極難實現
有了以上的鋪墊,就可以用一個標記爲重啓的 error 對象來實現 server 的重啓:
func (s *Server) Run(ctx context.Context, conf any) (err error) {
for err = s.run(ctx, conf); err == ErrRestart; err = s.run(ctx, conf) {
s.reset()
}
return
}
在重啓時首先會優雅關閉 server,銷燬所有資源,然後重新初始化 server 對象,讀取配置,初始化插件對象,監聽端口。就彷彿進程重啓了一樣。
實現熱重啓的好處
進程不再需要退出,對於錯誤處理更友好,對於 docker 容器來說,進程退出往往就會導致 docker 實例退出。此外重啓速度更快,方便快速更新配置。另一個好處是結合多實例,對於單元測試和基準測試更方便,因爲單元測試的時候不能退出進程,此時就可以啓動多個 server 實例,進行測試,也可以關閉這些實例,測試其他內容。