概述
在 Web 或者网络应用中,时不时会有热重启(加载)的需求,例如 Nginx 在不断开连接的情况下重新加载新配置,微服务在不断业务的情况下完成热升级,这些都是热重启的一些应用,本文就将聊一下 Go 语言中可以如何实现热重启。
原理
其实热加载抽象一下,就是如何实现在不关闭 Socket 的情况下,运行新的代码,那么可以理解成这么几个步骤:
- 从旧进程中 fork 出一个新进程,继承打开的 Socket
- 新进程初始化完毕并运行起来之后,从继承的 Socket 中开始接收连接
- 新进程发送信号给旧进程,让旧进程退出
看上去不是很难,那么下面就对每一个步骤进行介绍在 Go 中可以怎么实现。
fork 进程
在 Go 的标准库里面,不止一种用于 fork 进程的方法,我这里使用的是 exec.Command
。因为参数 Cmd 的结构里面又一个 ExtraFiles 的成员,可以用于给新进程继承父进程中打开的文件。代码示例如下:
[root@liqiang.io]# cat fork.go
file := netListener.File() // this returns a Dup()
path := "/path/to/executable"
args := []string{
"-graceful"}
cmd := exec.Command(path, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.ExtraFiles = []*os.File{file}
err := cmd.Start()
if err != nil {
log.Fatalf("gracefulRestart: Failed to launch, error: %v", err)
}
这里的 netListener 是用于监听 HTTP 请求的 Listener,然后 File()
方法返回的是 dup
的文件描述符,但是,因为这个文件描述符没有设置 FD_CLOEXEC
标志位,所以,如果直接这样传给子进程的话,会导致在子进程中它是关闭的。
但是,好在我们有更好的办法,继续看代码,你会发现下面有一个 ExtraFiles 参数,根据文档描述,如果这个参数是非空的,那么实例 i 的文件描述符会变成 3+i,这也就意味着在子进程中,这段代码继承来的文件描述符始终是 3。
最后一个特别的地方就是我这里传了一个 -graceful
参数,因为这是开启的子进程,总要让进程区分一下自己是子进程还是父进程吧,因为父进程需要自己开启监听;而子进程只需要从父进程继承就可以了。
子进程初始化
接着就进入子进程的世界了,这里直接来看下代码:
[root@liqiang.io]# cat client.go
server := &http.Server{Addr: "0.0.0.0:8888"}
var gracefulChild bool
var l net.Listever
var err error
flag.BoolVar(&gracefulChild, "graceful", false, "listen on fd open 3 (internal use only)")
if gracefulChild {
log.Print("main: Listening to existing file descriptor 3.")
f := os.NewFile(3, "")
l, err = net.FileListener(f)
} else {
log.Print("main: Listening on a new file descriptor.")
l, err = net.Listen("tcp", server.Addr)
}
这里就做了区分,如果是父进程,那么自己开一个监听;如果是子进程,那么请直接使用父进程的监听文件描述符。
通知父进程退出
下一步就要通知父进程退出了,这里的代码还是接着上一段代码:
[root@liqiang.io]# cat server.go
if gracefulChild {
parent := syscall.Getppid()
log.Printf("main: Killing parent pid: %v", parent)
syscall.Kill(parent, syscall.SIGTERM)
}
server.Serve(l)
这里是给父进程发送了一个 SIGTERM 的信号,告诉父进程你可以退下了,后面的事情就交给我吧。
连接管理
这里有个很难的话题就是连接要怎么管理,例如父进程准备退出的时候,现有的连接要怎么处理?要想处理好连接,那么就需要对连接的状态进行更多的管理,这里先定义一个自己的连接结构,并且重写它的 Accept 方法:
[root@liqiang.io]# cat listern.go
type gracefulListener struct {
net.Listener
stop chan error
stopped bool
}
func (gl *gracefulListener) Accept() (c net.Conn, err error) {
c, err = gl.Listener.Accept()
if err != nil {
return
}
c = gracefulConn{Conn: c}
httpWg.Add(1)
return
}
func newGracefulListener(l net.Listener) (gl *gracefulListener) {
gl = &gracefulListener{Listener: l, stop: make(chan error)}
go func() {
_ = <-gl.stop
gl.stopped = true
gl.stop <- gl.Listener.Close()
}()
return
}
这里还有一个自己封装的结构体,gracefulConn,也顺便列出来:
[root@liqiang.io]# cat connection.go
type gracefulConn struct {
net.Conn
}
func (w gracefulConn) Close() error {
httpWg.Done()
return w.Conn.Close()
}
然后对应的安全退出的是实现也就很简单了:
[root@liqiang.io]# cat listener.go
func (gl *gracefulListener) Close() error {
if gl.stopped {
return syscall.EINVAL
}
gl.stop <- nil
return <-gl.stop
}
func (gl *gracefulListener) File() *os.File {
tl := gl.Listener.(*net.TCPListener)
fl, _ := tl.File()
return fl
}
最后,怎么使用这个 Listener 呢,代码如下:
[root@liqiang.io]# cat server.go
server := &http.Server{
Addr: "0.0.0.0:8888",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 16}
netListener = newGracefulListener(l)
server.Serve(netListener)
小结
这就是整的一个如何在 Go 语言中实现一个热重启的实现。这里没有给出完整的实现,但是已经有了一个真实的开源实现:endless。
本文翻译自:Graceful Restart in Golang