天天看点

使用 Go 语言实现优雅的服务器重启使用 Go 语言实现优雅的服务器重启

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

使用 Go 语言实现优雅的服务器重启使用 Go 语言实现优雅的服务器重启

image

不关闭现有连接:例如我们不希望关掉已部署的运行中的程序。但又想不受限制地随时升级服务。

socket连接要随时响应用户请求:任何时刻socket的关闭可能使用户返回'连接被拒绝'的消息,而这是不可取的。

新的进程要能够启动并替换掉旧的。

<a target="_blank"></a>

在基于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,你可以捕获并处理: 

if err != nil {

if nerr, ok := err.(net.err); ok &amp;&amp; nerr.timeout() {

fmt.println(“stop accepting connections”)

return

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

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

execspec := &amp;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启动了一个新进程。 

正如你先前看到的,你可以将文件描述符传递到新进程,这需要一些unix魔法(一切都是文件),我们可以把socket发送到新进程中,这样新进程就能够使用它并接收及等待新的连接。

但fork-execed进程需要知道它必须从文件中得到socket而不是新建一个(有些兴许已经在使用了,因为我们还没断开已有的监听)。你可以按任何你希望的方法来,最常见的是通过环境变量或命令行标志。

listenerfile, err := listener.file()

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

env: os.environ(),

files: []uintptr{os.stdin.fd(), os.stdout.fd(), os.stderr.fd(), listenerfd},

// fork exec the new version of your server

 然后在程序的开始处:

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)

// handle

var bool ok

listener, ok = listener.(*net.tcplistener)

if !ok {

} else {

listener, err = newlistenerwithport(12345)

到此为止,就这样,我们已经将其传到另一个正在正确运行的进程,对于旧服务器的最后操作是等其连接关闭。由于标准库里提供了sync.waitgroup结构体,用go实现这个功能很简单。

每次接收一个连接,在waitgroup上加1,然后,我们在它完成时将计数器减一:

wg.add(1)

go func() {

handle(conn)

wg.done()

}()

至于等待连接的结束,你仅需要wg.wait(),因为没有新的连接,我们等待wg.done()已经被所有正在运行的handler调用。

timeout := time.newtimer(time.minute)

wait := make(chan struct{})

wg.wait()

wait &lt;- struct{}{}

select {

case &lt;-timeout.c:

return waittimeouterror

case &lt;-wait:

return nil

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

原文发布时间:2014-12-26

本文来自云栖合作伙伴“linux中国”