Facebook 如何实现 Golang 服务优雅重启 —— 以 Grace 为例
Grace 是 Facebook 推出的一个用于 Golang 服务优雅重启的框架库,设计目的上可以作为 net/http
的一个 drop-in replacement。
这篇文章就来研究一下 Grace 实现优雅重启的核心方法。
方法论
通常来说,本机环境上实现 graceful restart 的方法都可以归纳为:
- 保留当前进程
A
的情况下先创建一个新进程B
,并且B
监听相同的端口 - 确保新连接都是由
B
处理,即 A 不再接受新连接 A
处理完旧链接请求之后在关闭
其中步骤 2 和 3 可以合并,实现 graceful shutdown。
步骤1中的监听相同端口既可以通过继承父进程的 listen fd 也可以利用 linux 的 SO_REUSEPORT
来直接监听。
并且通常此时新进程 B
是一个新部署的服务二进制文件。
方法看着不难,但是实际实现往往要考虑更多的细节,可能踩的坑也会更多。
具体实现
整个库的核心是 grace/net
即 socket 层面的实现替换;http server 部分的支持也是依赖这里。
外部用户通过 net.Listen()
调用就可以获得经过包装的 TCP 或者 Unix Domain Socket 对象;为了专注讨论,这里只以 TCP 为例子。
这里的使用方法和 Golang 的 net 很类似,区别在于 grace/net
对象支持一个 StartProcess()
函数。这个函数的作用就是创建承接未来流量的新进程,也是实现 graceful restart 的起点。
0x0 StartProcess()
1 | func (n *Net) StartProcess() (int, error) { |
代码逻辑其实很简单,压缩一下就是:
- 先拿到当前的 listener 列表,因为一个服务进程可能会监听多个端口
- 然后将每个 listener 转换得到对应的
os.File*
;因为 Golangos/exec
要继承父进程的 fd 必须要在调用里显式给出os.File
的序列;而 listener 作为 net 的对象显然是不认得。
listener files 会连同 stdio 的 files 最终存到allFiles
,作为os.StartProcess()
参数一部分 - 因为可能有多个 listener,所以数量会通过环境变量
LISTEN_FDS={count}
传递给子进程 - 通过
os.Args[0]
获得程序路径,用来启动子进程
可以发现有相当一部分工作量都是在倒腾 Golang 自己的数据类型以适应 runtime 的接口。后面这种感觉会特别强烈。
0x1 inherit()
另一个关键函数就是 Net.inherit()
,这个函数仅在被热启动的子进程中调用上面 net.Listen()
时会被调用,并且利用 sync.Once
保证实际逻辑只会被调用一次。
这个函数同样很简单
1 | func (n *Net) inherit() error { |
- 首先通过环境变量拿到 listener 的数量
- 然后从 fd=3 开始做
fd -> *os.File -> net.Listener
,因为 Golang runtime 的需要net.Listener
这段代码背后有几个假设。
首先是 n.fdStart
必须要是准确的第一个 listener,这个由父进程保证;并且绝大多数时候是 3,因为 stdin, stdout, stderr 已经占了 0, 1, 2。也说明不要瞎关闭标准输入输出的 fd。
其次是所有的 listener 必须是连续的。这个假设在 StartProcess()
中不太能一眼看得出来。不过考虑到一般服务启动都是先把 listener 给一齐启动好的,所以也算合理。
经过这么一顿操作之后 n.inherited
里保存的就是继承来的 listener 了,而且这几个 listener 也都是活跃的。
0x2 Reuse Listener
Listener 的 reuse 其实也很简单:当外部用 net.Listen("tcp", addr)
请求一个 listener 时,检查一下这个 addr
是不是已经被某个继承来的 listener 在监听了,如果是的话就直接返回这个 listener。
否则的话就创建一个新的 listener。
0x3 信号触发
需要 graceful restart 的时候,比如部署了服务的新二进制版本,就可以给服务进程发送一个 SIGUSR2
信号。
服务进程收到信号后,调用 StartProcess()
创建子进程;子进程启动后会继承父进程的 listener,就绪后再给父进程发送一个 SIGTERM
信号,通知父进程退出。
此时端口上的新请求就可以被子进程平滑接收处理,而父进程进入 graceful shutdown 流程。
这部分逻辑在 grace/gracehttp
中,而 HTTP 服务的优雅退出则是由 Facebook 的另外一个库 httpdown 实现。
Faceboook/httpdown
httpdown 的核心逻辑只有一个文件
httpdown 内部为每一个连接维护一个状态。
当请求关闭时:
- 设置 http server 禁用未来链接的 keep-alive
即使 HTTP 默认有 keep-alive,但是大多时候也是充当短链接的用途,而这里直接禁用 keep-alive 可以让链接关闭的更快 - 通过
s.listener.Close()
关闭监听,让新请求都由前面启动的新进程处理 - 然后等待现有链接结束
a. 一个链接结束之后会触发<-s.closed
,从链接列表里移除后检查是否还有其他链接;如果连接空了,那么就可以出发stopDone
结束服务
b. 但是就这么一直等到天荒地老也不对,所以同时会有个定时器,默认一分钟,超时后会进入 kill mode,就是直接强行关掉每个链接
如果你的业务是长连接,那么就需要在应用层协议设计一个 go-away 的消息,在需要优雅退出的时候告知对端此连接马上要结束了,你准备准备然后关闭这个连接并重连,或者迁移到另外一个服务节点上。
grace 和 httpdown 都是 facebook 里同一个人写的,也都被 archive 了,不知道是不是内部已经不用了。
这两个库的核心逻辑都很简单,但是有很多协调控制 goroutine 的逻辑倒是比较复杂。