加入收藏 | 设为首页 | 会员中心 | 我要投稿 开发网_开封站长网 (http://www.0378zz.com/)- 科技、AI行业应用、媒体智能、低代码、办公协同!
当前位置: 首页 > 教程 > 正文

借助 Go 语言实现优雅的服务器重启

发布时间:2021-12-11 10:35:00 所属栏目:教程 来源:互联网
导读:Go被设计为一种后台语言,它通常也被用于后端程序中。服务端程序是GO语言最常见的软件产品。在这我要解决的问题是:如何干净利落地升级正在运行的服务端程序。 image 目标: 不关闭现有连接:例如我们不希望关掉已部署的运行中的程序。但又想不受限制地随时升

Go被设计为一种后台语言,它通常也被用于后端程序中。服务端程序是GO语言最常见的软件产品。在这我要解决的问题是:如何干净利落地升级正在运行的服务端程序。
 
image
 
目标:
不关闭现有连接:例如我们不希望关掉已部署的运行中的程序。但又想不受限制地随时升级服务。
 
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)[…]
 
你会发现这个进程使用完全相同的参数os.Args启动了一个新进程。
 
发送socket到子进程并恢复它
正如你先前看到的,你可以将文件描述符传递到新进程,这需要一些UNIX魔法(一切都是文件),我们可以把socket发送到新进程中,这样新进程就能够使用它并接收及等待新的连接。
 
但fork-execed进程需要知道它必须从文件中得到socket而不是新建一个(有些兴许已经在使用了,因为我们还没断开已有的监听)。你可以按任何你希望的方法来,最常见的是通过环境变量或命令行标志。
 
listenerFile, err := listener.File()if err != nil {
  log.Fatalln("Fail to get socket file descriptor:", err)}listenerFd := listenerFile.Fd()// Set a flag for the new process start processos.Setenv("_GRACEFUL_RESTART", "true")execSpec := &syscall.ProcAttr{
  Env:  os.Environ(),
  Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), listenerFd},}// Fork exec the new version of your serverfork, err := syscall.ForkExec(os.Args[0], os.Args, execSpec)
 
然后在程序的开始处:
 
var listener *net.TCPListenerif os.Getenv("_GRACEFUL_RESTART") == "true" {
  file := os.NewFile(3, "/tmp/sock-go-graceful-restart")
  listener, err := net.FileListener(file)
  if err != nil {
    // handle
  }
  var bool ok
  listener, ok = listener.(*net.TCPListener)
  if !ok {
    // handle
  }} else {
  listener, err = newListenerWithPort(12345)}
 
文件描述没有被随机的选择为3,这是因为uintptr的切片已经发送了fork,监听获取了索引3。留意隐式声明问题。
 
最后一步,等待旧服务连接停止
到此为止,就这样,我们已经将其传到另一个正在正确运行的进程,对于旧服务器的最后操作是等其连接关闭。由于标准库里提供了sync.WaitGroup结构体,用go实现这个功能很简单。
 
每次接收一个连接,在WaitGroup上加1,然后,我们在它完成时将计数器减一:
 
for {  conn, err := listener.Accept()
 
  wg.Add(1)  go func() {    handle(conn)    wg.Done()  }()}
 
至于等待连接的结束,你仅需要wg.Wait(),因为没有新的连接,我们等待wg.Done()已经被所有正在运行的handler调用。
 
Bonus: 不要无限制等待,给定限量的时间
 
有time.Timer,实现很简单:
 
timeout := time.NewTimer(time.Minute)wait := make(chan struct{})go func() {
  wg.Wait()
  wait <- struct{}{}}()select {case <-timeout.C:
  return WaitTimeoutErrorcase <-wait:
  return nil}

(编辑:开发网_开封站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    热点阅读