在服务端程序更新或重启时,如果咱们间接 kill -9
杀掉旧过程并启动新过程,会有以下几个问题:
- 旧的申请未解决完,如果服务端过程间接退出,会造成客户端链接中断(收到
RST
) - 新申请打过去,服务还没重启结束,造成
connection refused
- 即便是要退出程序,间接
kill -9
依然会让正在解决的申请中断
很间接的感触就是:在重启过程中,会有一段时间不能给用户提供失常服务;同时粗鲁敞开服务,也可能会对业务依赖的数据库等状态服务造成净化。
所以咱们服务重启或者是从新公布过程中,要做到新旧服务无缝切换,同时能够保障变更服务 零宕机工夫!
作为一个微服务框架,那 go-zero
是怎么帮开发者做到优雅退出的呢?上面咱们一起看看。
优雅退出
在实现优雅重启之前首先须要解决的一个问题是 如何优雅退出:
对 http 服务来说,个别的思路就是敞开对
fd
的listen
, 确保不会有新的申请进来的状况下解决完曾经进入的申请, 而后退出。
go 原生中 http
中提供了 server.ShutDown()
,先来看看它是怎么实现的:
- 设置
inShutdown
标记 - 敞开
listeners
保障不会有新申请进来 - 期待所有沉闷链接变成闲暇状态
- 退出函数,完结
别离来解释一下这几个步骤的含意:
inShutdown
func (srv *Server) ListenAndServe() error {if srv.shuttingDown() {return ErrServerClosed}
....
// 理论监听端口;生成一个 listener
ln, err := net.Listen("tcp", addr)
if err != nil {return err}
// 进行理论逻辑解决,并将该 listener 注入
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
func (s *Server) shuttingDown() bool {return atomic.LoadInt32(&s.inShutdown) != 0
}
ListenAndServe
是 http 启动服务器的必经函数,外面的第一句就是判断 Server
是否被敞开了。
inShutdown
就是一个原子变量,非 0 示意被敞开。
listeners
func (srv *Server) Serve(l net.Listener) error {
...
// 将注入的 listener 退出外部的 map 中
// 不便后续管制从该 listener 链接到的申请
if !srv.trackListener(&l, true) {return ErrServerClosed}
defer srv.trackListener(&l, false)
...
}
Serve
中注册到外部 listeners map
中 listener
,在 ShutDown
中就能够间接从 listeners
中获取到,而后执行 listener.Close()
,TCP 四次挥手后,新的申请就不会进入了。
closeIdleConns
简略来说就是:将目前 Server
中记录的沉闷链接变成变成闲暇状态,返回。
敞开
func (srv *Server) Serve(l net.Listener) error {
...
for {rw, err := l.Accept()
// 此时 accept 会产生谬误,因为后面曾经将 listener close 了
if err != nil {
select {
// 又是一个标记:doneChan
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
}
}
}
其中 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
func init() {go func() {
var profiler Stopper
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGTERM)
for {
v := <-signals
switch v {
case syscall.SIGUSR1:
dumpGoroutines()
case syscall.SIGUSR2:
if profiler == nil {profiler = StartProfile()
} else {profiler.Stop()
profiler = nil
}
case syscall.SIGTERM:
// 正在执行优雅敞开的中央
gracefulStop(signals)
default:
logx.Error("Got unregistered signal:", v)
}
}
}()}
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 反对咱们!
微信交换群
关注『微服务实际 』公众号并点击 交换群 获取社区群二维码。