分享
  1. 首页
  2. 文章

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

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

服务优雅重启-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

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

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏