在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语言社区(Golangweb)

原文发表时间:2016-09-10

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Ken的杂谈

基于GitLab的Code Review教程

也就是说,使用GitLab进行Code Review就是在分支合并环节发起Merge Request,然后Code Review完成后将代码合并到目标分支。

2K30
来自专栏我的博客

max x Yosemide无法安装jdk8解决

1.下载 好jdk 1.7(1.8) 地址:http://www.oracle.com/technetwork/java/javase/downloads/in...

450130
来自专栏lestat's blog

ngrok+nginx实现内网穿透

写在前面: 前天在qq群里看到有人在讨论替代花生壳的工具,说到了ngrok,说是可以实现花生壳一样的内网穿透,个人认为主要有以下几个用处: 可以在公司测试服务...

94660
来自专栏前端布道

HTML5离线应用与客户端存储

支持离线 Web 应用开发是 HTML5 的另一个重点。所谓离线 Web 应用,就是在设备不能上网的情况下仍然可以运行的应用。

14910
来自专栏Spark学习技巧

深入了解HBase架构

23920
来自专栏LanceToBigData

IntelliJ IDEA使用(一)基本设置与类、方法模板设置

其实之前一直开发都是在使用的是Eclipse,但是最近在做Maven项目的时候要用IntelliJ IDEA,据说这个idea功能非常的强大,最近在使用的时候发...

86570
来自专栏技术点滴

远程线程注入引出的问题

远程线程注入引出的问题 一、远程线程注入基本原理 远程线程注入——相信对Windows底层编程和系统安全熟悉的人并不陌生,其主要核心在于一个Windows AP...

317100
来自专栏博岩Java大讲堂

Java日志体系(log4j2)

50990
来自专栏NetCore

了解EF CodeFirst的Migrator功能与Migrator.Net对比

在上一篇【数据库迁移利器:Migrator.Net】中,很多朋友提到了EF的CodeFirst也有数据库的迁移功能,说来真惭愧,玩了那么多年,至今还未去了解EF...

21790
来自专栏熊训德的专栏

Hbase replication源码分析

本文主要通过Replication的核心原理和failover 过程,对Hbase replication源码分析,希望对大家了解Hbase源码有所帮助。

1.6K00

扫码关注云+社区

领取腾讯云代金券