设为首页 加入收藏

TOP

使用 Go 语言实现优雅的服务器重启
2015-02-02 14:31:41 来源: 作者: 【 】 浏览:14
Tags:使用 语言 实现 优雅 服务器 重启

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


image


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


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


停止接受连接请求


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


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启动了一个新进程。


正如你先前看到的,你可以将文件描述符传递到新进程,这需要一些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}


完整的示例


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


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


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


— Léo Unbekandt CTO @ Appsdeck


】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇求n个数的子集的三种思路 下一篇利用HTML5 <canvas>实现204..

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: