Monibuca v5 实现热重启

优雅关闭

在 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 退出的时候,需要

  1. 退出所有发布者
  2. 退出所有订阅者
  3. 关闭所有插件的连接监听
  4. 关闭 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 实例,进行测试,也可以关闭这些实例,测试其他内容。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章