更新(2015年4月):​​Florian von Bock​​​已将本文中描述的内容转换为一个名为​​endless​​的优秀Go包 。

如果您有Golang HTTP服务,可能需要重新启动它以升级二进制文件或更改某些配置。如果你(像我一样)因为网络服务器处理它而优雅地重新启动是理所当然的,你可能会发现这个配方非常方便,因为使用Golang你需要自己动手。

实际上这里有两个问题需要解决。首先是正常重启的UNIX方面,即进程可以在不关闭侦听套接字的情况下自行重启的机制。第二个问题是确保所有正在进行的请求正确完成或超时。

重新启动而不关闭套接字

  • fork一个继承侦听套接字的新进程。
  • 子进程初始化并开始接受套接字上的连接。
  • 紧接着,孩子向父母发送信号,导致父母停止接受连接并终止。

分叉一个新的过程

使用Golang lib分支进程的方法不止一种,但对于这种特殊情况, ​​exec.Command​​​是​​可行​​​的方法。这是因为此函数返回的​​Cmd结构​​​具有此​​ExtraFiles​​成员,该成员指定要由新进程继承的打开文件(除了stdin / err / out)。

这是这样的:

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​​​是一个指向​​net.Listener​​​的指针, ​​用于​​​监听HTTP请求。​​path​​如果要升级,变量应该包含新可执行文件的路径(可能与当前运行的路径相同)。

上面代码中的一个重点是​​netListener.File()​​​ 返回 文件描述符的 ​​dup(2)​​​。重复的文件描述符不会设置​​FD_CLOEXEC标志​​,这会导致文件在子节点中关闭(不是我们想要的)。

您可能会遇到通过命令行参数将继承的文件描述符编号传递给子项的示例,但​​ExtraFiles​​实现的方式 使其不必要。文档指出“如果非零,则条目i变为文件描述符3 + i。”这意味着在上面的代码片段中,子代中的继承文件描述符将始终为3,因此不需要明确地传递它。

最后,​​args​​​数组包含一个​​-graceful​​选项:你的程序需要某种方式通知孩子这是一个正常重启的一部分,孩子应该重新使用套接字而不是尝试打开一个新套接字。另一种方法可能是通过环境变量。

子初始化

这是程序启动序列的一部分

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)
}

信号父母停止

此时我们已准备好接受请求,但就在我们这样做之前,我们需要告诉我们的父母停止接受请求并退出,这可能是这样的:

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()采取行动,但这就是界面魔法拯救的地方。(非常感谢​​Jeff R. Allen​​​ 对​​这篇文章的评价​​)。

下面是一个侦听器示例,它在每个Accept()上递增一个等待组。首先,我们“子类” ​​net.Listener​​​(你会明白我们为什么需要​​stop​​​和​​stopped​​以下):

type gracefulListener struct {
net.Listener
stop chan error
stopped bool
}

接下来,我们“覆盖”Accept方法。(​​gracefulConn​​暂时没关系,稍后会介绍)。

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
}

上面的函数启动goroutine的原因是因为它不能在我们​​Accept()​​​上面完成,因为它会阻塞 ​​gl.Listener.Accept()​​。goroutine将通过关闭文件描述符来解锁它。

我们的​​Close()​​​方法只是发送一个​​nil​​停止通道,以便上面的goroutine完成其余的工作。

func (gl *gracefulListener) Close() error {
if gl.stopped {
return syscall.EINVAL
}
gl.stop <- nil
return <-gl.stop
}

最后,这个小方便方法从中提取文件描述符​​net.TCPListener​​。

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()
}

要开始使用上面优雅的Listener版本,我们只需要将​​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}

作者:​​sunsky303​​