在Go程序中实现服务器重启的方法

2019-11-10 10:35:54刘景俊

Go被设计为一种后台语言,它通常也被用于后端程序中。服务端程序是GO语言最常见的软件产品。在这我要解决的问题是:如何干净利落地升级正在运行的服务端程序。
目标:

    不关闭现有连接:例如我们不希望关掉已部署的运行中的程序。但又想不受限制地随时升级服务。     socket连接要随时响应用户请求:任何时刻socket的关闭可能使用户返回'连接被拒绝'的消息,而这是不可取的。     新的进程要能够启动并替换掉旧的。

原理

在基于Unix的操作系统中,signal(信号)是与长时间运行的进程交互的常用方法.

    SIGTERM: 优雅地停止进程     SIGHUP: 重启/重新加载进程 (例如: nginx, sshd, apache)

如果收到SIGHUP信号,优雅地重启进程需要以下几个步骤:

    服务器要拒绝新的连接请求,但要保持已有的连接。     启用新版本的进程     将socket“交给”新进程,新进程开始接受新连接请求     旧进程处理完毕后立即停止。

停止接受连接请求

服务器程序的共同点:持有一个死循环来接受连接请求:

    for {
      conn, err := listener.Accept()
      // Handle connection
    }

跳出这个循环的最简单方式是在socket监听器上设置一个超时,当调用listener.SetTimeout(time.Now())后,listener.Accept()会立即返回一个timeout err,你可以捕获并处理:

    for {
      conn, err := listener.Accept()
      if err != nil {
        if nerr, ok := err.(net.Err); ok && nerr.Timeout() {
           fmt.Println(“Stop accepting connections”)
           return
        }
      }
    }

注意这个操作与关闭listener有所不同。这样进程仍在监听服务器端口,但连接请求会被操作系统的网络栈排队,等待一个进程接受它们。
启动新进程

Go提供了一个原始类型ForkExec来产生新进程.你可以与这个新进程共享某些消息,例如文件描述符或环境参数。

    execSpec := &syscall.ProcAttr{
      Env:   os.Environ(),
      Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
    }
    fork, err := syscall.ForkExec(os.Args[0], os.Args, execSpec)
    […]