Hook优雅停止注册的Starter,发现貌似出现一个并发问题,该如何解决呢?

来源:8-8 golang中如何优雅的退出进程-notify hook starter编码实战

胡正阳

2020-05-28

老师,您好,非常感动于精彩的讲解。我有个问题想要提问,是关于使用HookStarter优雅的停止服务,并触发其余Starter的Stop方法。
在app.go文件中,我只启用了4个基本的Starter,分别对四个Starter的Stop方法只写了一个fmt.Println的方法,但是在手动Ctrl-C停止服务时,却发现执行了四次针对IrisServerStarter函数的Stop方法的执行。
图片描述

图片描述

上述现象发生之后,我感觉这个现象好像是函数在异步执行,于是做了如下改造:
图片描述
将callbacks中的函数通过闭包函数的形式执行,同时使用WaitGroup控制等待完成执行。可是结果居然还是一样的。

于是我继续折腾
图片描述
这次我把WaitGroup废掉,改用了1个缓冲区的channel,结合闭包传递函数执行,结果干脆不打印所要停止的Starter的Stop函数中的fmt.Println的内容了,而且还报了一个错误,尝试用recover忽略也不行,貌似又一个问题。

有点黔驴技穷,所以没办法,只能求助一下偶像神老师了!


今天按照评论里面的思路,尝试的对代码进行了改写,发现这样是可以的了。
先看下执行效果,我在validator和props中增加了一个Start函数,在这个函数的最后增加一行调用ListenStopSig函数。该函数其实就是监听context.Context的Cancel函数被调用,从而触发调用当前Starter的Stop函数,以便达到资源回收的效果。这里的ListenStopSig最好原样不动的从BaseStarter中复制过来,如若在这里不写,则默认就会调用到BaseStarter中的Stop函数,就无法达到回收当前Starter的资源的效果了。这是由于go的组合调用策略导致的,也是和java、python等其他面向对象的差别。go就是go,不一样的烟火。我用的是go1.13.8,兴许以后google修改调用策略呢,也难说!
图片描述
下面是把StarterContext改为了context.Context,并将原来的map[string]interface{}放到了context的value中。同时SetProps函数也将starterCancel函数对外暴露一个函数。
图片描述

在hook的init函数中,接到signal信号,则通过该函数执行来触发所有需要回收资源的Starter。(注意这里是两对儿括号)
图片描述

这样优雅就相对舒服了~!:)

写回答

4回答

枫荇

2020-06-01

同学!您好,这么多方法的尝试相信同学的收获也不少,可以看到,过程中思考了代码结构和编程技术的巧妙应用。没有最好的方法,但有最适合的。

同学最后找到了优雅的方法,相信一定查阅了不少资料,也看的出来,非常用心,精益求精,非常有工匠精神。

在同学的内容和下面同学的回复中,提到了如下方法的组合:

  1. signal

  2. callback

  3. channel

  4. 全局静态函数

同学的问题是并发问题?


首先,从实现需求、目的和效果上说,signal是触发stop的必选项,通过对信号量的监听来触发stop动作。

其次,触发stop动作后在分发给各个starter来执行stop方法,从而优雅关闭starter。

那问题的关键就在第二步如何通知并且保证并发安全。

从starter的角度来说,大部分starter都是独立的资源,并不会有直接资源依赖;少部分有资源依赖;还有部分starter并不关心stop:


独立的无资源依赖的starter:

单独处理调用stop来停止和释放资源。

有资源依赖的starter:

HookStarter的职责并不关心starter之间的依赖。所以有依赖的starter需要自己在stop中消化,在其中一个starter的stop方法中来处理所依赖的stop顺序和stop依赖问题;其他starter不处理stop。

不关心stop的starter:

很明显不用处理stop


另外,在调用stop的时候还需要考虑流量入口相关的starter,通常是web端口或rpc端口或定时调度器等相关的starter。这样的考虑在于,我们停止所有资源前要保证,请求和新的执行不再被进入,并且尽可能等待正在执行的任务执行完成,从而更优雅的停止资源和释放资源。

所以,通常先要停止web或rpc端口或者定时任务的调度器,也就是提供外部调用的端口。停止后,新的请求和执行就会被拒绝,从而可以有效等待正在执行的任务完成。

那么在等待正在执行的任务执行完成的时间里,可以执行非相关的starter,但这种非相关的starter从语义上很难界定,所以通常不去界定。假设不存在非相关的starter。

但正在执行的任务是需要时间的,需要多长时间呢?实际上是不好评估的,或者说评估的成本大于本身这件事情。

所以在HookStarter设计中,为简单,只考虑web或rpc端口或者定时任务的调度器等相关的starter的停止,只要他们停止就可以停止其他所有的资源starter了。但在停止web或rpc端口或者定时任务的调度器时也因为其他可能的原因花费的时间比较长,会对HookStarter的执行产生困惑,通常会设置超时时间,如果达到超时时间还没有被停止,就强制停止其他资源并退出进程。


这里我们再回来说说并发问题?

在HookStarter中并发问题大部分都是资源依赖所引起的。所以按照前文处理好依赖关系就问题不大,建议依赖关系交给starter自己来处理。


从如下几个方法上来说:

signal+callback

signal+channel

signal+全局静态函数

最推荐的是signal+callback,其次是signal+channel,signal+全局静态函数不推荐。

另外我建议另一种方法:

signal+starters,代码如下。

//img1.sycdn.imooc.com/szimg/5ed4ac3109ee8b1f15301100.jpg

同学,如果我的观点还没有解答到你的问题,请继续提问并附上问题本身的信息哈。

0
1
胡正阳
非常感谢!偶像老师!
2020-06-02
共1条回复

胡正阳

提问者

2020-05-30

非阻塞的Starter是在boot开启的goroutine中运行的,但当这些Starter的Start函数运行结束,可能会导致绑定其上的Stop函数也无法在Hook中寻址。

方案1:如果简单一些,将各个Starter的Stop函数改为全局静态的,而不是绑定在Starter上面的。这样只能在Hook中明确写出函数的名称来注册到Hook的内存中。但这样明显不能仅在项目中使用app.go文件来注册Starter,以达到一个优雅框架的效果。

方案2:将所有的Starter都改为阻塞的,用channel或者context来实现通信,以达到hook执行之后,通知各Starter自行运行Stop函数来回收资源。

今天的想法,回头用代码实现一个玩玩

0
1
胡正阳
看错了,其实各非阻塞的Starter中的Start函数也是直接被boot调用的,并不是goroutine寻址错误的原因,应该还是go寻址策略的原因。不过还是按照方案2,修改StarterContext为context.Context,并同时增加一个ListenStopSig函数,用于监听Hook调用Cancel函数的信号,以便执行各个Starter中的Stop函数。
2020-05-30
共1条回复

胡正阳

提问者

2020-05-30

因为各Starter都在不同的goroutine中,只有最后一个iris是在当前goroutine中,所以这样直接调用stop函数的方式是无法寻址到其他starter的stop函数的。那为什么每次只能寻址到IrisServerStarter的Stop函数,可能是因为go的struct的层次寻址策略导致的。 所以感觉还是在各Starter中启动监听chan来触发资源回收是对的。

0
1
胡正阳
哦,其实其他的Starter在初始化好资源之后,都已经结束了,所以回收资源的事情,只需要在像IrisServerStarter这样阻塞goroutine中回收即可。除非是有其他阻塞式goroutine,则需要通过chan或者context机制去通信,以达到资源回收。
2020-05-30
共1条回复

胡正阳

提问者

2020-05-29

这个仓库是我测试用的代码,https://git.imooc.com/hu.lyndon/infra

0
0

仿微信抢红包 Golang实战多版本抢红包系统

Golang红包系统单体版+并发版+分布式+微服务版,四大金装版、超值必修课

582 学习 · 159 问题

查看课程