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

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

重新启动而不关闭套接字

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

分叉一个新的过程

ExtraFiles

这是这样的:

1 
2 
3 
4 
5 
6 7 8 9 10 11 12 13 14 
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) } 
netListenerpath
netListener.File()FD_CLOEXEC
ExtraFiles
args-graceful

子初始化

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

1 
2 
3 
4 
5 
6 7 8 9 10 11 12 13 14 15 16 
    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)  } 

信号父母停止

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

1 
2 
3 
4 
5 
6 7 
if gracefulChild {
 parent := syscall.Getppid()  log.Printf("main: Killing parent pid: %v", parent)  syscall.Kill(parent, syscall.SIGTERM) }  server.Serve(l) 

正在进行的请求完成/超时

为此,我们需要使用sync.WaitGroup跟踪打开的连接 。我们需要在每个接受的连接上递增等待组,并在每个连接关闭时递减它。

1
var httpWg sync.WaitGroup 

乍一看,Golang标准的http包不提供任何钩子来对Accept()或Close()采取行动,但这就是界面魔法拯救的地方。(非常感谢Jeff R. Allen 对这篇文章的评价)。

net.Listenerstopstopped
1 
2 
3 
4 
5
type gracefulListener struct {  net.Listener  stop chan error  stopped bool } 
gracefulConn
1 
2 
3 
4 
5 
6 7 8 9 10 11 
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 } 

我们还需要一个“构造函数”:

1 
2 
3 
4 
5 
6 7 8 9 
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 } 
Accept()gl.Listener.Accept()
Close()nil
1 
2 
3 
4 
5 
6 7 
func (gl *gracefulListener) Close() error {  if gl.stopped {  return syscall.EINVAL  }  gl.stop <- nil  return <-gl.stop } 
net.TCPListener
1 
2 
3 
4 
5
func (gl *gracefulListener) File() *os.File {  tl := gl.Listener.(*net.TCPListener)  fl, _ := tl.File()  return fl } 
net.ConnClose()
1 
2 
3 
4 
5 
6 7 8 
type gracefulConn struct {  net.Conn }  func (w gracefulConn) Close() error {  httpWg.Done()  return w.Conn.Close() } 
server.Serve(l)
1 
2
netListener = newGracefulListener(l) server.Serve(netListener) 

还有一件事。您应该避免挂断客户端无意关闭的连接(或不是本周)。最好按如下方式创建服务器:

1 
2 
3 
4 
5
server := &http.Server{  Addr: "0.0.0.0:8888",  ReadTimeout: 10 * time.Second,  WriteTimeout: 10 * time.Second,  MaxHeaderBytes: 1 << 16}