服务优雅重启-facebook/grace学习

服务优雅重启-facebook/grace学习

梗概

主要介绍服务优雅重启的基本概念。git

逐步分析

猜想

查阅相关资料后,大概猜想出作法github

服务重启时,旧进程并不直接中止,而是用旧进程fork一个新进程,同时旧进程的全部句柄都dup到新进程。这时新的请求都由新的进程处理,旧进程在处理完本身的任务后,自行退出。app

这只是大概流程,里面还有许多细节须要考虑socket

分析grace

github

https://github.com/facebookar...源码分析

流程简述

  1. 利用启动时的参数(包括命令行参数、环境变量等),从新启动新进程。同时将当前socket句柄给新进程。
  2. 旧进程再也不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句柄。
旧进程如何退出

旧进程退出须要确保当前的请求所有处理完成。同时再也不接收新的请求。命令行

  1. 如何不接收新的请求

回答这个问题须要提到socket流程code

一般创建socket须要经历如下四步:server

  • socket
  • bind
  • listen
  • accept

一般,accept处于一个循环中,这样就能持续处理请求。因此若不想接收新请求,只需退出循环,再也不accept便可。进程

  1. 如何确保当前请求所有处理完成

回答这个问题,咱们须要给每个链接赋予一系列状态。刚好,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.SIGTERMsyscall.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说过了,是用来传递中止和强行终止信号的。

其他newactiveidleclosed是用来记录处于不一样状态的链接的。

咱们记录了不一样状态的链接,那么在关闭时,就能等链接处于“空闲“或”关闭“时再关闭它。

// 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源码研读。

相关文章
相关标签/搜索