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

 更新时间:2015年06月29日 11:25:38   投稿:goldensun  
这篇文章主要介绍了在Go程序中实现服务器重启的方法,由于很多人盲目崇拜谷歌"亲爹",Go语言在国内有着不寻常的人气,需要的朋友可以参考下

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)
    […]

 你会发现这个进程使用完全相同的参数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 process
    os.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 server
    fork, err := syscall.ForkExec(os.Args[0], os.Args, execSpec)

 然后在程序的开始处:

   

复制代码 代码如下:
var listener *net.TCPListener
    if os.Getenv("_GRACEFUL_RESTART") == "true" {
      // The second argument should be the filename of the file descriptor
      // however, a socker is not a named file but we should fit the interface
      // of the os.NewFile function.
      file := os.NewFile(3, "")
      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: 不要无限制等待,给定限量的时间

   

复制代码 代码如下:
timeout := time.NewTimer(time.Minute)
    wait := make(chan struct{})
    go func() {
      wg.Wait()
      wait <- struct{}{}
    }()
    
    select {
    case <-timeout.C:
      return WaitTimeoutError
    case <-wait:
      return nil
    }

完整的示例

这篇文章中的代码片段都是从这个完整的示例中提取的:https://github.com/Scalingo/go-graceful-restart-example
结论

socket传递配合ForkExec使用确实是一种无干扰更新进程的有效方式,在最大时间上,新的连接会等待几毫秒——用于服务的启动和恢复socket,但这个时间很短。

相关文章

  • 使用golang引入外部包的三种方式:go get, go module, vendor目录

    使用golang引入外部包的三种方式:go get, go module, ve

    这篇文章主要介绍了使用golang引入外部包的三种方式:go get, go module, vendor目录,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • Golang 实现获取当前函数名称和文件行号等操作

    Golang 实现获取当前函数名称和文件行号等操作

    这篇文章主要介绍了Golang 实现获取当前函数名称和文件行号等操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-05-05
  • GoLang完整实现快速列表

    GoLang完整实现快速列表

    这篇文章主要介绍了GoLang完整实现快速列表,列表是一种非连续的存储容器,由多个节点组成,节点通过一些 变量 记录彼此之间的关系,列表有多种实现方法,如单链表、双链表等
    2022-12-12
  • Golang反射获取结构体的值和修改值的代码示例

    Golang反射获取结构体的值和修改值的代码示例

    这篇文章主要给大家介绍了golang反射获取结构体的值和修改值的代码示例及演示效果,对我们的学习或工作有一定的帮助,感兴趣的同学可以参考阅读本文
    2023-08-08
  • Go语言协程处理数据有哪些问题

    Go语言协程处理数据有哪些问题

    协程(coroutine)是Go语言中的轻量级线程实现,由Go运行时(runtime)管理。本文为大家详细介绍了Go中的协程,协程不需要抢占式调度,可以有效提高线程的任务并发性,而避免多线程的缺点
    2023-02-02
  • Golang优雅保持main函数不退出的办法

    Golang优雅保持main函数不退出的办法

    很多时候我们需要让main函数不退出,让它在后台一直执行,下面这篇文章主要给大家介绍了关于Golang优雅保持main函数不退出的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-07-07
  • 深入讲解Go语言中函数new与make的使用和区别

    深入讲解Go语言中函数new与make的使用和区别

    大家都知道Go语言中的函数new与函数make一直是新手比较容易混淆的东西,看着相似,但其实不同,不过解释两者之间的不同也非常容易,下面这篇文章主要给大家介绍了关于Go语言中函数new与make区别的相关资料,需要的朋友可以参考下。
    2017-10-10
  • golang实现多协程下载文件(支持断点续传)

    golang实现多协程下载文件(支持断点续传)

    本文主要介绍了golang实现多协程下载文件,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-11-11
  • GoLang使goroutine停止的五种方法实例

    GoLang使goroutine停止的五种方法实例

    goroutine是Go并行设计的核心,下面这篇文章主要给大家介绍了关于GoLang使goroutine停止的五种方法,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-07-07
  • Go-家庭收支记账软件项目实现

    Go-家庭收支记账软件项目实现

    这篇文章主要介绍了Go-家庭收支记账软件项目实现,本文章内容详细,具有很好的参考价值,希望对大家有所帮助,需要的朋友可以参考下
    2023-01-01

最新评论