一文讲懂服务的优雅重启和更新
在服务端程序更新或重启时,如果我们直接 kill -9
杀掉旧进程并启动新进程,会有以下几个问题:
旧的请求未处理完,如果服务端进程直接退出,会造成客户端链接中断(收到
RST
)新请求打过来,服务还没重启完毕,造成
connection refused
即使是要退出程序,直接
kill -9
仍然会让正在处理的请求中断
很直接的感受就是:在重启过程中,会有一段时间不能给用户提供正常服务;同时粗鲁关闭服务,也可能会对业务依赖的数据库等状态服务造成污染。
所以我们服务重启或者是重新发布过程中,要做到新旧服务无缝切换,同时可以保障变更服务 零宕机时间!
作为一个微服务框架,那 go-zero
是怎么帮开发者做到优雅退出的呢?下面我们一起看看。
优雅退出
在实现优雅重启之前首先需要解决的一个问题是 如何优雅退出:
对 http 服务来说,一般的思路就是关闭对
fd
的listen
, 确保不会有新的请求进来的情况下处理完已经进入的请求, 然后退出。
go 原生中 http
中提供了 server.ShutDown()
,先来看看它是怎么实现的:
设置
inShutdown
标志关闭
listeners
保证不会有新请求进来等待所有活跃链接变成空闲状态
退出函数,结束
分别来解释一下这几个步骤的含义:
inShutdown
ListenAndServe
是 http 启动服务器的必经函数,里面的第一句就是判断 Server
是否被关闭了。
inShutdown
就是一个原子变量,非 0 表示被关闭。
listeners
Serve
中注册到内部 listeners map
中 listener
,在 ShutDown
中就可以直接从 listeners
中获取到,然后执行 listener.Close()
,TCP 四次挥手后,新的请求就不会进入了。
closeIdleConns
简单来说就是:将目前 Server
中记录的活跃链接变成变成空闲状态,返回。
关闭
其中 getDoneChan
中已经在前面关闭 listener
时,对 doneChan
这个 channel 中 push。
总结一下:Shutdown
可以优雅的终止服务,期间不会中断已经活跃的链接。
但服务启动后的某一时刻,程序如何知道服务被中断了呢?服务被中断时如何通知程序,然后调用 Shutdown 作处理呢?接下来看一下系统信号通知函数的作用
服务中断
这个时候就要依赖 OS 本身提供的 signal
。对应 go 原生来说,signal
的 Notify
提供系统信号通知的能力。
https://github.com/tal-tech/go-zero/blob/master/core/proc/signals.go
SIGUSR1
-> 将goroutine
状况,dump 下来,这个在做错误分析时还挺有用的SIGUSR2
-> 开启/关闭所有指标监控,自行控制 profiling 时长SIGTERM
-> 真正开启gracefulStop
,优雅关闭
而 gracefulStop
的流程如下:
取消监听信号,毕竟要退出了,不需要重复监听了
wrap up
,关闭目前服务请求,以及资源time.Sleep()
,等待资源处理完成,以后关闭完成shutdown
,通知退出如果主 goroutine 还没有退出,则主动发送 SIGKILL 退出进程
这样,服务不再接受新的请求,服务活跃的请求等待处理完成,同时也等待资源关闭(数据库连接等),如有超时,强制退出。
整体流程
我们目前 go 程序都是在 docker
容器中运行,所以在服务发布过程中,k8s
会向容器发送一个 SIGTERM
信号,然后容器中程序接收到信号,开始执行 ShutDown
:
到这里,整个优雅关闭的流程就梳理完毕了。
但是还有平滑重启,这个就依赖 k8s
了,基本流程如下:
old pod
未退出之前,先启动new pod
old pod
继续处理完已经接受的请求,并且不再接受新请求new pod
接受并处理新请求的方式old pod
退出
这样整个服务重启就算是成功了,如果 new pod
没有启动成功,old pod
也可以提供服务,不会对目前线上的服务造成影响。
项目地址
https://github.com/tal-tech/go-zero
欢迎使用 go-zero 并 star 支持我们!
微信交流群
关注『微服务实践』公众号并点击 交流群 获取社区群二维码。
版权声明: 本文为 InfoQ 作者【万俊峰Kevin】的原创文章。
原文链接:【http://xie.infoq.cn/article/58dcbd6e97043269b8651c4ce】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论