Go Micro + Gin 不同层级服务软停服(平滑关闭服务)的回顾一

Go Micro + Gin 不同层级服务软停服(平滑关闭服务)的回顾一

下面是我这个小菜鸡对一次软停服需求的处理的一个总结,有啥不对的地方麻烦各位大佬帮我这个小菜鸡纠正一下呀

服务软停服是指在关闭服务时,如果有请求在处理,应该等待请求处理完成,再关闭服务,从而达到平滑关闭服务的目的。基本思路如下:

  1. 监听到进程终止信号
  2. 把服务从注册中心摘除,不再接收后续请求
  3. 检测是否有请求在处理
  4. 当前请求已全部处理完成 即可停止服务

因为本人现在从事于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方法从上到下主要是做了以下事情:

  1. 调用 start 方法来启动服务
  2. 调用 register 方法向注册中心注册服务信息
  3. 创建一个通道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 := &registry.Node{
    Id: service.Options().Id,
}
var ns []*registry.Node
ns = append(ns, n)
r.Deregister(&registry.Service{
    Name: service.Options().Name,
    Nodes: ns,
})

因为博主是直接借助micro内部的监听信号的,顺手就再借助一下micro内部的这个反注册吧。毕竟有*干嘛不用呢。

检测是否有请求在处理

这一步的实现一开始我是想通过拦截器来做的,后来发现Gin支持自定义中间件,同时通过Use方法添加进去的中间会包含每一个请求的处理链路中,那这不就原地起飞了嘛。
Go Micro + Gin 不同层级服务软停服(平滑关闭服务)的回顾一
因此,只要定义一个中间件,并添加到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

上一篇:[GO]Gin框架学习笔记


下一篇:gin框架中项目的初始化