服务优雅重启-facebook/grace学习
梗概
主要介绍服务优雅重启的基本概念。
逐步分析
猜测
查阅相关资料后,大概猜测出做法
服务重启时,旧进程并不直接停止,而是用旧进程fork一个新进程,同时旧进程的所有句柄都dup到新进程。这时新的请求都由新的进程处理,旧进程在处理完自己的任务后,自行退出。
这只是大概流程,里面还有许多细节需要考虑
分析grace
github
https://github.com/facebookar...
流程简述
- 利用启动时的参数(包括命令行参数、环境变量等),重新启动新进程。同时将当前socket句柄给新进程。
- 旧进程不再Accept,待当前任务结束后,进程退出
源码分析
如何启动新进程
// facebookgo/grace/gracenet/net.go:206(省略非核心代码)
func (n *Net) StartProcess() (int, error) {
listeners, err := n.activeListeners()
// 复制socket句柄
files := make([]*os.File, len(listeners))
for i, l := range listeners {
files[i], err = l.(filer).File()
defer files[i].Close()
}
// 复制标准IO句柄
allFiles := append([]*os.File{os.Stdin, os.Stdout, os.Stderr}, files...)
// 启动新进程,并传递句柄
process, err := os.StartProcess(argv0, os.Args, &os.ProcAttr{
Dir: originalWD,
Env: env,
Files: allFiles,
})
return process.Pid, nil
}
这段代码是启动新进程的过程。
- 变量
files
保存listeners
句柄(即socket句柄) - 变量
allFiles
保存files
+stdout、stdin、stderr
句柄 -
os.StartProcess
启动新进程,并传递父进程句柄
注:这里传递的句柄只包括socket句柄与标准IO句柄。
旧进程如何退出
旧进程退出需要确保当前的请求全部处理完成。同时不再接收新的请求。
- 如何不接收新的请求
回答这个问题需要提到socket流程
。
通常建立socket需要经历以下四步:
- socket
- bind
- listen
- accept
通常,accept处于一个循环中,这样就能持续处理请求。所以若不想接收新请求,只需退出循环,不再accept即可。
- 如何确保当前请求全部处理完成
回答这个问题,我们需要给每一个连接赋予一系列状态。恰好,net/http
包帮我们做好了这件事。
// GOROOT/net/http/server.go:2743
type ConnState int
const (
// 新连接刚建立时
StateNew ConnState = iota
// 连接处于活跃状态,即正在处理的请求
StateActive
// 连接处于空闲状态,一般用于keep-alive
StateIdle
// 劫持状态,可以理解为关闭状态
StateHijacked
// 关闭状态
StateClosed
)
通过状态,我们就能精确判断所有请求是否处理完成。只要所有活跃(StateActive)的连接都成为空闲(StateIdle)或者关闭(StateClosed)状态。就可以保证请求全部处理完成。
具体代码
// facebookgo/httpdown/httpdown.go:347
func ListenAndServe(s *http.Server, hd *HTTP) error {
// 监听端口,提供服务
hs, err := hd.ListenAndServe(s)
signals := make(chan os.Signal, 10)
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
// 监听信号量2和15(即kill -2 -15)
select {
case <-signals:
signal.Stop(signals)
// hs.Stop() 开始停止服务
if err := hs.Stop(); err != nil {
return err
}
}
}
这段代码是启动服务的入口代码
- ListenAndServe 监听端口,提供http服务
- signal.Notify 注册要监听的信号量,这里监听
syscall.SIGTERM
和syscall.SIGINT
,即一般终止进程的信号量 - hs.Stop() 停止服务,结束当前进程
可以看出,服务退出的逻辑都在hs.Stop()
// facebookgo/httpdown/httpdown.go:293
func (s *server) Stop() error {
s.stopOnce.Do(func() {
// 禁止keep-alive
s.server.SetKeepAlivesEnabled(false)
// 关闭listener,不再接收请求
closeErr := s.listener.Close()
<-s.serveDone
// 通过stop(一个chan),传递关闭信号
stopDone := make(chan struct{})
s.stop <- stopDone
// 若在s.stopTimeout以内没有结束,则强行kill所有连接。默认s.stopTimeout为1min
select {
case <-stopDone:
case <-s.clock.After(s.stopTimeout):
// stop timed out, wait for kill
killDone := make(chan struct{})
s.kill <- killDone
}
})}
Stop方法
- 禁止keep-alive
- 关闭listener,即不再accept新请求
- 想s.stop(一个chan)传递关闭的信号
- 若s.stopTimeout时间内,没有退出,则强行kill所有连接。
那么,等待所有请求处理完毕的逻辑,应该处于消费s.stop的地方。
这里我们注意到,最核心的结构体有这样几个属性
// facebookgo/httpdown/httpdown.go:126
type server struct {
...
new chan net.Conn
active chan net.Conn
idle chan net.Conn
closed chan net.Conn
stop chan chan struct{}
kill chan chan struct{}
...
}
stop和kill说过了,是用来传递停止和强行终止信号的。
其余new
、active
、idle
、closed
是用来记录处于不同状态的连接的。
我们记录了不同状态的连接,那么在关闭时,就能等连接处于“空闲“或”关闭“时再关闭它。
// facebookgo/httpdown/httpdown.go:233
case c := <-s.idle:
conns[c] = http.StateIdle
// 那些处于“活跃”的连接,会等到它转为“空闲”时,将其关闭
if stopDone != nil {
c.Close()
}
case c := <-s.closed:
// 所有连接关闭后,退出
if stopDone != nil && len(conns) == 0 {
close(stopDone)
return
}
case stopDone = <-s.stop:
// 所有连接关闭后,退出
if len(conns) == 0 {
close(stopDone)
return
}
// 关闭所有“空闲”连接
for c, cs := range conns {
if cs == http.StateIdle {
c.Close()
}
}
这里可以看出,当接收到关闭信号时(stopDone = <-s.stop)
- 会遍历所有“空闲”连接,将其关闭。
- 而那些处于“活跃”的连接,会等到它转为“空闲”时,将其关闭
- 在所有连接关闭后,退出
总结
进程重启主要就是如何退出、如何启动。grace代码量不多,以上叙述了核心的逻辑,有兴趣的同学可以fork github源码研读。
有疑问加站长微信联系(非本文作者)