Go Micro + Gin 不同层级服务软停服(平滑关闭服务)的回顾一
下面是我这个小菜鸡对一次软停服需求的处理的一个总结,有啥不对的地方麻烦各位大佬帮我这个小菜鸡纠正一下呀
服务软停服是指在关闭服务时,如果有请求在处理,应该等待请求处理完成,再关闭服务,从而达到平滑关闭服务的目的。基本思路如下:
- 监听到进程终止信号
- 把服务从注册中心摘除,不再接收后续请求
- 检测是否有请求在处理
- 当前请求已全部处理完成 即可停止服务
因为本人现在从事于Go开发,所以就从Go的角度说一下基于Go Micro + Gin 来实现这四大步。
先来大概说一下micro web 包下服务run的一点相关源码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5B18bpGX-1638535990496)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20211202162254056.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PWgLJ3mW-1638535990498)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20211202162725475.png)]
run方法从上到下主要是做了以下事情:
- 调用 start 方法来启动服务
- 调用 register 方法向注册中心注册服务信息
- 创建一个通道ex 同时调用 s.run(ex)方法开始循环注册,run方法代码比较简短,如下,主要是当设置了RegisterInterval,开始建立定时任务,定时注册一下
func (s *service) run(exit chan bool) {
if s.opts.RegisterInterval <= time.Duration(0) {
return
}
t := time.NewTicker(s.opts.RegisterInterval)
for {
select {
case <-t.C:
s.register()
case <-exit:
t.Stop()
return
}
}
}
4,监听进程退出信号
5,deregister反注册 把节点从注册中心摘除
6,stop停止服务
展开说说start方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TkBtLJBU-1638535990498)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20211203155516337.png)]
该方法里主要是执行自定的前置增强函数,创建网络监听器,构建service对象,调用serve方法接收请求和处理请求,然后执行自定义后置增强函数,同时把关闭监听器时可能出现的error放入exit中。至此,start函数就执行完成了,服务已经启动。然后继续往下执行注册等方法。然后通过select{}来阻塞,直到收到进程退出信号或者context cancel才会继续往下走。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kHcKVvc5-1638535990499)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20211203160259067.png)]
stop方法非常的简单,但里面也会分别执行一个前置增强和后置增强函数,这是实现软停服的关键点之一
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-99a3tBtU-1638535990500)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20211203161352472.png)]
至此 micro web包下的service的启动到停止的一整个流程就大概梳理完成了呀。一般WEB包都是搭配Gin框架来放在API层面使用。现在就来说说在这一层面如何实现软停服:
监听到进程终止信号
在go里面可以使用 signal.Notify
方法来监听进程的信号,至于各个信号在syscall包下对应的常量值这里就不累述了。前面提到的start
方法中,恰好micro内部就自己监听了信号:
ch := make(chan os.Signal, 1)
if s.opts.Signal {
signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT)
}
select {
// wait on kill signal 等待一个进程退出信号
case sig := <-ch:
if logger.V(logger.InfoLevel, log) {
log.Infof("Received signal %s", sig)
}
// 等待context cancel
case <-s.opts.Context.Done():
if logger.V(logger.InfoLevel, log) {
log.Info("Received context shutdown")
}
}
意味着这一步不需要我们去处理了,当然了,也可以通过设置options中的Signal来不让micro内部监听,而选择自己监听,像以下这样:
func main() {
ctx, cancel := context.WithCancel(context.Background())
service := web.NewService()
service.Init(
func(o *web.Options) {
o.BeforeStop = append(o.BeforeStop, BeforeStop)
},
)
go func() {
if err := service.Run(); err != nil {
fmt.Println("err:", err)
}
fmt.Println("service exit")
}()
var state int32 = 1
sc := make(chan os.Signal)
signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
select {
case sig := <-sc:
//手动从注册中心摘除节点 可以参考start方法中的deregister方法 这里不展开细写 因为我没有用这种方式 有*干嘛不用非要自己造呢 鹅鹅鹅鹅鹅鹅
BeforeStop()
atomic.StoreInt32(&state, 0)
fmt.Printf("获取到退出信号[%s]", sig.String())
}
fmt.Println("cancel执行")
cancel()
if callFunc != nil {
callFunc()
}
fmt.Printf("服务退出")
os.Exit(int(atomic.LoadInt32(&state)))
return
}
把服务从注册中心摘除,不再接收后续请求
一般注册中心都会提供一个方法来摘除节点的,micro的实现如下。注意:如果采用手动摘除节点时,一定要把service.Run()
放在一条新的协程里面,不然其实这一段就是无用功。因为run方法会select{}阻塞着,过了select后micro内部自己就摘除节点了。放在run
前的话 节点还没注册进去。
r := service.Options().Service.Client().Options().Registry
n := ®istry.Node{
Id: service.Options().Id,
}
var ns []*registry.Node
ns = append(ns, n)
r.Deregister(®istry.Service{
Name: service.Options().Name,
Nodes: ns,
})
因为博主是直接借助micro内部的监听信号的,顺手就再借助一下micro内部的这个反注册吧。毕竟有*干嘛不用呢。
检测是否有请求在处理
这一步的实现一开始我是想通过拦截器来做的,后来发现Gin支持自定义中间件,同时通过Use
方法添加进去的中间会包含每一个请求的处理链路中,那这不就原地起飞了嘛。
因此,只要定义一个中间件,并添加到gin的引擎中,那么每一条请求都自然会触发中间件,那记录请求就简单了呀。中间件实现如下:
router := gin.Default()
router.Use(Logger())
var RequestNumber atomic.Int32
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("-----请求进来-----")
//请求数量+1
RequestNumber.Inc()
//执行下一个节点
c.Next()
//执行完成 请求数量-1
fmt.Println("----请求完成----", c.Writer.Status())
RequestNumber.Dec()
}
}
当前请求已全部处理完成 即可停止服务
这时候节点已经不在注册中心了,后续不可能有请求进来了。这时候只要等待当前所有请求都处理完成即可退出进程。退出前的操作,是不是有点熟悉?是的,就是前面提到的stop方法中的前置增强函数。有一说一,micro的这一套前置后置增强,还有一系列的中间件Wrapper,能让我们非常方便的去扩展。
func main() {
ctx, cancel := context.WithCancel(context.Background())
service := web.NewService()
service.Init(
func(o *web.Options) {
o.BeforeStop = append(o.BeforeStop, BeforeStop)
},
)
if err := service.Run(); err != nil {
log.WithError(err).Error("service.Run")
}
cancel()
return
}
func BeforeStop() error {
log.Info("当前请求数量:", handler.RequestNumber)
for {
log.Info("当前请求数量:", handler.RequestNumber)
if handler.RequestNumber.Load() == 0 {
return nil
}
time.Sleep(1 * time.Second)
}
}
以上就是go micro web包下的软停服的实现思路与一个方案。总结一下:
1,利用Gin的中间件会参与到每一条请求处理链路中来记录请求,也就是上文提到的Logger()
2,因为run
方法中,会监听进程退出信号,同时会select{}阻塞着,直到收到进程退出或者contextcancel()
才会往下走。然后调用deregister()
从注册中心移除服务节点。这也就意味着监听信号和移除服务节点都不需要我们自己来处理了。
3,判断当前是否还有请求未处理完成,利用micro提供的BeforeStop
函数数组,在stop方法执行前判断当前请求是否全部完成。
参考链接:
Go Micro文档:https://learnku.com/docs/go-micro/2.x/packer/8501
Go Micro一些wrapper:https://github.com/microhq/go-plugins/tree/master/wrapper
Go Micro防坑指南:https://magodo.github.io/go-micro-tips/#wrapper