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

HammerMax · · 1043 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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

梗概

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

逐步分析

猜测

查阅相关资料后,大概猜测出做法

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

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

分析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流程

通常建立socket需要经历以下四步:

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


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:HammerMax

查看原文:服务优雅重启-facebook/grace学习

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1043 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传