优雅重启go应用
前言
需先了解系统信号和相应函数后,再阅读本文将会更丝滑顺畅
- 信号是Linux或Unix类系统中,一种进程间通讯的方式
- 当信号发送到某个进程时,操作系统会中断该进程的正常流程,并进入相应的信号处理函数执行操作,完成后再回到中断的地方继续执行
- 如果目标进程先前注册了某个信号的处理程序(signal handler),则此处理程序会被调用,否则缺省的处理程序被调用
如果有一个go实现的http服务,就会遇到因代码变更或配置修改需重新启动服务的情景,以前使用其他语言实现服务时,从未注意过
无缝重启的问题,因为web服务器(如nginx等)实现无缝重启所以你无需关心,而现在使用Go写服务后,才发现无缝重启也需要自己
动手解决
实际上,无缝重启需要解决的问题有两个
- 无缝重启时,UNIX层面问题,如进程能不关闭正在监听的socket实现重启的机制
- 确保正在处理的请求可被正常处理完毕或超时退出
无需关闭socket的重启
- 生成一个子进程并继承旧进程正在监听的socket
- 子进程初始化后,开始接收socket的连接
- 一旦子进程发送信号给父进程时,父进程立刻停止接收新连接并终止运行
生成一个子进程
在go库里,有很多种方式可生成子进程,但在解决本文的问题上,使用exec.Cammand是最适合的方式,因为返回的Cmd结构体有个ExtraFiles属性,
该属性指向由父进程继承来的且已打开的文件(除了stdin/err/out,标准输入/输出/错误外)
1 | file := netListener.File() //netListener[^1]是一个指针,指向正在监听http请求的net.Listener,File()返回一个fd的dup(2)[^2],dup(2)返回的是fd的副本fd2,fd2不含`FD_CLOEXEC`标志,该标志会导致子进程的fd2关闭 |
你也许会在命令行里把需继承的fd号码通过参数传入子进程,但是有ExtraFiles字段,就无需这样做了,ExtraFiles文档说「如果该字段为非nil,则下标i代表的fd是3+i」,也即从fd下标从3开始(因为不包含stdin/out/err)
所以子进程继承的fd永远都是3开始
最后 args切片包含 -graceful 选项,意味着你的程序需要有处理该选项的逻辑,用于分辨子进程需重用socket而不是创建新socket
子进程初始化
下面是程序启动的流程
1 | server := &http.Server{Addr: "0.0.0.0:8888"} |
发送信号通知父进程终止运行
在子进程准备接收请求前,需要通知父进程停止接收请求并退出:
1 | if gracefulChild { |
还在父进程里处理的请求
使用sync.WaitGroup
来追踪正在处理的请求,每接收新请求就需自增1,在完成一个请求时就自减1
Go的http标准包里没有hook方式以供在Accept()
和Close()
时进行一些自定义处理,但这也正是接口发挥作用的地方(Accept和Close属于接口Listener)
(可参考JefR.Allen的这篇文章)
1 | type gracefulListener struct { |
然后重写Accept方法
1 | func (gl *gracefulListener) Accept() (c net.Conn, err error) { |
有了自增,则还需要在连接处理完毕被关闭时的自减方法
1 | type gracefulConn struct { |
再写一个new方法
1 | func newGracefulListener(l net.Listener) (gl *gracefulListener) { |
重写Close
1 | func (gl *gracefulListener) Close() error { |
还有一个获取fd的方法
1 | func (gl *gracefulListener) File() *os.File { |
要用上面的无缝重启版的Listener,只需要修改server.Serve
1 | netListener = newGracefulListener(l) |
为了避免挂起连接,使用以下方式创建server:
1 | server := &http.Server{ |
其他
Q&A
可以在
1 | func (gl *gracefulListener) Close() error { |
Q:关闭listener吗,这样就不用在newGracefulListener里的goroutine了
A:这样做不管用,没法关闭listener,但是原因还不清楚呢,有朋友知道可以分享下吗?