Golang 优雅重启(平滑重启)
Golang 中的优雅重启
当我们在管理 Golang HTTP 服务时,可能会遇到需要重启服务的情况,比如更新二进制文件或修改配置。通常情况下,web 服务器会自动处理这些重启过程,确保服务平滑过渡。但在 Golang 中,你需要自己动手实现这一功能。
优雅重启涉及两个关键步骤:
- 如何在不关闭监听Socket的情况下重启进程;
- 如何确保所有正在进行中的请求能够被妥善处理或优雅地超时。
不关闭Socket的重启
- 创建子进程:创建一个新进程,让它继承监听Socket。
- 子进程初始化:子进程启动后,开始初始化并接受连接。
- 通知父进程停止:子进程启动后,立即通知父进程停止接受新连接并终止。
创建子进程
Go 标准库提供了几种创建新进程的方法,但对于优雅重启来说,exec.Command 是最佳选择。这是因为 Command 函数返回的 Cmd 结构体包含了 ExtraFiles 字段,它可以指定新进程继承额外的文件描述符。
下面是创建子进程的具体代码:
file := netListener.File()
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}
if err := cmd.Start(); err != nil {
log.Fatalf("gracefulRestart: Failed to launch, error: %v", err)
}
这里,netListener 指向监听 HTTP 请求的 net.Listener 对象。path 变量应包含新可执行文件的路径,如果是升级操作,这可能是与当前正在运行的服务相同的路径。
重要的一点是 netListener.File() 返回的是文件描述符的副本,这意味着子进程不会关闭这个文件描述符。
子进程初始化
子进程初始化时,需要检查是否为优雅重启模式,并据此决定监听行为。
server := &http.Server{Addr: "0.0.0.0:8888"}
var gracefulChild bool
var l net.Listener
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)
}
通知父进程停止
子进程准备好接受连接后,需要通知父进程停止并退出。
if gracefulChild {
parent := syscall.Getppid()
log.Printf("main: Killing parent pid: %v", parent)
syscall.Kill(parent, syscall.SIGTERM)
}
server.Serve(l)
处理正在进行中的请求
为了确保所有正在进行中的请求能够被妥善处理或优雅地超时,我们需要使用 sync.WaitGroup 来跟踪打开的连接。每当接受一个连接时增加计数器,在每个连接关闭时减少计数器。
var httpWg sync.WaitGroup
Golang 标准 http 包没有直接提供钩子来对 Accept() 或 Close() 采取行动,但我们可以通过自定义监听器来实现这一功能。
下面是一个监听器的例子,它会在每次 Accept() 时增加一个等待组。
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
}
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
}
我们还需要一个 net.Conn 的变体,它会在 Close() 时减少等待组。
type gracefulConn struct {
net.Conn
}
func (w gracefulConn) Close() error {
httpWg.Done()
return w.Conn.Close()
}
为了开始使用上述优雅版本的监听器,我们只需要改变 server.Serve(l) 这一行:
netListener := newGracefulListener(l)
server.Serve(netListener)
此外,为了避免客户端没有意图关闭的挂起连接,建议设置合理的超时时间:
server := &http.Server{
Addr: "0.0.0.0:8888",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 16}
通过以上步骤,我们可以确保 Golang HTTP 服务在重启过程中能够优雅地处理所有正在进行中的请求,从而避免数据丢失和用户体验下降。这样,即使在服务更新期间,用户也能享受到无缝的体验,而不会察觉到任何中断。