重启服务,指的是一个关闭、启动进程的完成过程。
目前所有服务基本都无单点问题,都是集群化部署。对一个服务的关闭、启动进程来说,启动的流程基本上问题不大,可以由集群的统一管理器,比如 Kubernetes,来进行服务的启动,启动之后慢慢将流量引入到新启动的节点,整个服务是无损的。
但是在关闭服务的过程中,要考虑的情况就比较复杂了,比如说有服务已经在连接请求中怎么办?如果关闭服务的操作超时了怎么办?我们考虑的就是如何优雅关闭的问题。
如何优雅关闭
什么叫优雅关闭?你可以对比着想,不优雅的关闭比较简单,就是什么都不管,强制关闭进程,这明显会导致有些连接被迫中断。
或许你并没有意识到这个问题的严重性,不妨试想下,当一个用户在购买产品的时候,由于不优雅关闭,请求进程中断,导致用户的钱包已经扣费了,但是商品还未进入用户的已购清单中。这就会给用户带来实质性的损失(就是说某些连续的操作只完成了一半)。
所以,优雅关闭服务,其实说的就是,关闭进程的时候,不能暴力关闭进程,而是要等进程中的所有请求都逻辑处理结束后,才关闭进程。按照这个思路,需要研究两个问题“如何控制关闭进程的操作” 和 “如何等待所有逻辑都处理结束”。
当我们了解了如何控制进程关闭操作,就可以延迟关闭进程行为,设置为等连接的逻辑都处理结束后,再关闭进程(核心:不理解结束进程,等所有连接的操作都完成,再结束)。
如何控制关闭进程的操作
平时关闭一个进程的方法:
- Ctrl + C: 向进程发送SIGINT信号,信号可被阻塞和处理
- Ctrl + : 向进程发送SIGQUIT信号,也可被阻塞和处理
- kill命令: kill pid 会向进程发送 SIGTERM 信号,而 kill -9 会向进程发送 SIGKILL 信号。这两个信号都用于立刻结束进程,但是 SIGTERM 是可以被阻塞和处理的,而 SIGKILL 信号是不能被阻塞和处理的。
除了 SIGKILL 信号无法被捕获之外,其他的信号都能捕获,所以,只要在程序中捕获住这些信号,就能实现控制关闭进程操作了。
在go中如何捕获信号:
// 忽略某个信号
func Ignore(sig ...os.Signal){}
// 判断某个信号是否被忽略了
func Ignored(sig os.Signal) bool{}
// 关注某个/某些/全部 信号
func Notify(c chan<- os.Signal, sig ...os.Signal){}
// 取消使用 notify 对信号产生的效果
func Reset(sig ...os.Signal){}
// 停止所有向 channel 发送的效果
func Stop(c chan<- os.Signal){}
- 订阅信号Notify:关注某些信号,一旦信号到来就会放入到chan中
- 忽略信号Ignode:忽略某个信号
- 停止所有订阅Stop
- 重置Reset:
那么在主业务中监测信号可以这么写:
func main() {
go func() {
server.ListenAndServe()
}()
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, system.SIGQUIT)
<- quit
}
一般将监听signal的处理放在主goroutine中,便于处理。
如何等待所有逻辑都处理结束
go 1.8 版本之后,net/http 引入了 server.Shutdown 来进行优雅重启。
server.Shutdown 方法是个阻塞方法,一旦执行之后,它会阻塞当前 Goroutine,并且在所有连接请求都结束之后,才继续往后执行。其主要逻辑如下:
- 设置标记位:原子操作,设置后后续不再接受新请求
- 调用钩子函数:onShutdonw是一个钩子函数列表
- for循环不断判断所有连接是否都已结束:使用ticker计时,真正执行的是closeIdleConns 方法。
- closeIdleConns 判断所有连接中的请求是否已完成,是返回true,否返回false
// closeIdleConns 关闭所有的连接并且记录是否服务器的连接已经全部关闭
func (s *Server) closeIdleConns() bool {
s.mu.Lock()
defer s.mu.Unlock()
quiescent := true
for c := range s.activeConn {
st, unixSec := c.getState()
// Issue 22682: 这里预留5s以防止在第一次读取连接头部信息的时候超过5s
if st == StateNew && unixSec < time.Now().Unix()-5 {
st = StateIdle
}
if st != StateIdle || unixSec == 0 {
// unixSec == 0 代表这个连接是非常新的连接,则标记位需要标记false
quiescent = false
continue
}
c.rwc.Close()
delete(s.activeConn, c)
}
return quiescent
}
梳理之后,我们的代码可以这么写:
func main() {
...
// 当前的 Goroutine 等待信号量
quit := make(chan os.Signal)
// 监控信号:SIGINT, SIGTERM, SIGQUIT
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
// 这里会阻塞当前 Goroutine 等待信号
<-quit
// 调用Server.Shutdown graceful结束
if err := server.Shutdown(context.Background()); err != nil {
log.Fatal("Server Shutdown:", err)
}
}
【小结】:
- 监测信号使用os.signal包
- net/http包内部的server.Shutdown阻塞等待左右连接关闭