优雅关闭
在 v4 中关闭一个流通过改变流的生命周期实现
v4 中流有一个 G(goroutine)专门负责管理流的生命周期,并使用状态自动机来实现状态变更。
但是在退出发布者或者订阅者,仍然遇到一些问题,首先发布者和订阅者各自有自己的 G ,多数用于网络通讯。此外退出分为两种情况,一种是内部原因,比如超时,出错等。另一种是外部原因,比如用户手动关闭,连接断开等。很难优雅的统一处理。
v5 中通过第一性原理思考,移除不必要的 G,不再有管理生命周期的状态机,流和发布者变成同一个概念,实现主动被动退出的统一处理,使得代码进一步简化。
优雅关闭流和订阅者
为了尽量减少锁和 G的使用,因此选择使用动态Select方式,在 Server 层面的一个大 G 中实现,对发布者和订阅者的退出监听。下面是伪代码,为了方便理解
代码语言:javascript复制select {
case <-server 退出信号:
退出
case <-定时器信号:
定时任务
case <-事件总线信号:
事件处理
case <-发布者 1 退出信号:
case <-发布者 2 退出信号:
...
case <-订阅者 1 退出信号:
case <-订阅者 2 退出信号:
...
}
为啥优雅呢?因为在一个 G 里面处理,不需要锁,可以方便的修改发布者集合,订阅者集合,以及等待区(订阅时还没有发布者)等很多并发读写的场景。实际上你无法直接写出这个 select,因为发布者和订阅者动态添加和删除的。此时就需要用到 reflect.Select(cases)
了。
优雅关闭 Server
有了优雅关闭发布者和订阅者,那么剩下的就比较简单了,就是要优雅关闭插件。在 v4 中并不支持这种操作。为了能实现动态热更新配置等场景,优雅关闭插件就很重要,因此设计的时候就考虑到了监听和退出监听的逻辑。因此在 sever 退出的时候,需要 1. 退出所有发布者 2. 退出所有订阅者 3. 关闭所有插件的连接监听 4. 关闭 server 级的 http 和 tcp 监听
所有这些对象都包含了可以用来退出的 context
代码语言:javascript复制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 的重启:
代码语言:javascript复制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 实例,进行测试,也可以关闭这些实例,测试其他内容。