主要介绍服务优雅重启的基本概念。git
查阅相关资料后,大概猜想出作法github
服务重启时,旧进程并不直接中止,而是用旧进程fork一个新进程,同时旧进程的全部句柄都dup到新进程。这时新的请求都由新的进程处理,旧进程在处理完本身的任务后,自行退出。app
这只是大概流程,里面还有许多细节须要考虑socket
https://github.com/facebookar...源码分析
// 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流程
。code
一般创建socket须要经历如下四步:server
一般,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 } } }
这段代码是启动服务的入口代码
syscall.SIGTERM
和syscall.SIGINT
,即通常终止进程的信号量能够看出,服务退出的逻辑都在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方法
那么,等待全部请求处理完毕的逻辑,应该处于消费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源码研读。