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

使用 Go 语言实现优雅的服务器重启
http://www.oschina.net/translate/graceful-server-restart-with-go
使用 Go 语言实现优雅的服务器重启

英文原文:Graceful server restart with Go

标签: Go
#TagsSwitcher{color:#A00;} #RecTags,#MyTags{margin:5px 0;} #RecTags a.tag{line-height:24px;} #favor_form p:first-child{margin:5px 0;} #favor_form{width:200px;} #favor_form p {color:#666;} #favor_form form{min-height: 85px;width:200px;} #favor_form form ._favor_input{display:block;margin:2px 0;width:199px;} #favor_form form ._favor_button{float:left;padding:2px 5px;} .favor_ok {text-align:center;font-size:10.5pt;width:199px;height:60px;margin-top:10px;} #TagsSwitcher{cursor:pointer;float:right;margin-top:10px;} #MyTags{display:none;width:199px;} #MyTags a.tag {float:left; background-color: #E0EAF1;border-bottom: 1px solid #3E6D8E;border-right: 1px solid #7F9FB6;color: #3E6D8E;font-size: 8pt;line-height: 16px;margin: 2px 2px 2px 0;padding: 2px 4px;text-decoration: none;white-space: nowrap;} #MyTags a.tag:hover{background-color: #3E6D8E;color: #FFF;} 238人收藏此文章, 我要收藏 oschina 推荐于 11个月前 (共 7 段, 翻译完成于 12-24) ( 30评)
参与翻译(3人)
pseudo, Garfielt, Ley
仅中文 | 中英文对照 | 仅英文 | 打印此文章

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

使用 Go 语言实现优雅的服务器重启_第1张图片

目标:

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

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

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

pseudo
pseudo
翻译于 11个月前

2人顶

 翻译的不错哦!

原理

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

  • SIGTERM: 优雅地停止进程

  • SIGHUP: 重启/重新加载进程 (例如: nginx, sshd, apache)

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

  1. 服务器要拒绝新的连接请求,但要保持已有的连接。

  2. 启用新版本的进程

  3. 将socket“交给”新进程,新进程开始接受新连接请求

  4. 旧进程处理完毕后立即停止。

pseudo
pseudo
翻译于 11个月前

2人顶

 翻译的不错哦!

停止接受连接请求

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

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

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

?
1
2
3
4
5
6
7
8
for  {
   conn, err := listener.Accept()
   if  err != nil {
     if  nerr, ok := err.(net.Err); ok && nerr.Timeout() {
        fmt.Println(“Stop accepting connections”)
        return
     }
   }}

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

pseudo
pseudo
翻译于 11个月前

3人顶

 翻译的不错哦!

启动新进程

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

?
1
2
3
4
5
6
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启动了一个新进程。

pseudo
pseudo
翻译于 11个月前

1人顶

 翻译的不错哦!

发送socket到子进程并恢复它

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

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

?
1
2
3
4
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)

 

然后在程序的开始处:

?
1
2
3
4
5
6
7
8
9
10
11
12
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。留意隐式声明问题。

Garfielt
Garfielt
翻译于 11个月前

1人顶

 翻译的不错哦!

最后一步,等待旧服务连接停止

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

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

?
1
2
3
for  {  conn, err := listener.Accept()
 
   wg.Add(1)  go func() {    handle(conn)    wg.Done()  }()}

 

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

Ley
Ley
翻译于 11个月前

1人顶

 翻译的不错哦!

Bonus: 不要无限制等待,给定限量的时间

有time.Timer,实现很简单:

?
1
2
3
4
5
timeout :=  time .NewTimer( time .Minute)wait := make(chan  struct {})go func() {
   wg.Wait()
   wait <-  struct {}{}}()select { case  <-timeout.C:
   return  WaitTimeoutErrorcase <-wait:
   return  nil}

完整的示例

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

结论

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

这篇文章是我#周五技术系列的一部分,下这个周不会有新的更新了,大家圣诞节快乐。

链接:

  • 示例: https://github.com/Scalingo/go-graceful-restart-example

  • Go变量的隐形声明: http://www.qureet.com/blog/golang-beartrap/

  • Bonus的问题:fork()Golang进程安全吗? : https://groups.google.com/forum/#!msg/golang-nuts/beKfn7rujNg/zwY6zwl7QtQJ

  • 我们的网站: https://appsdeck.eu

— Léo Unbekandt CTO @ Appsdeck

Garfielt
Garfielt
翻译于 11个月前

1人顶

 翻译的不错哦!

你可能感兴趣的:(使用 Go 语言实现优雅的服务器重启)