原文 Graceful Restart in Golang

作者 grisha

声明:本文目的仅仅作为个人mark,所以在翻译的过程中参杂了自己的思想甚至改变了部分内容,其中有下划线的文字为译者添加。但由于译者水平有限,所写文字或者代码可能会误导读者,如发现文章有问题,请尽快告知,不胜感激。


前言

Update (Apr 2015): Florian von Bock已经根据本文实现了一个叫做endless的Go package

大家知道,当我们用Go写的web服务器需要修改配置或者需要升级代码的时候我们需要重启服务器,普通的重启会有一段宕机的时间,但优雅重启则不然:

什么是优雅重启

本文中的优雅重启表现为两点

  1. 进程在不关闭其所监听的端口的情况下重启
  2. 重启过程中保证所有请求能被正确的处理

1.进程在不关闭其所监听的端口的情况下重启

  • fork一个子进程,该子进程继承了父进程所监听的socket
  • 子进程执行初始化等操作,并最终开始接收该socket的请求
  • 父进程停止接收请求并等待当前处理的请求终止
fork一个子进程

有不止一种方法fork一个子进程,但在这种情况下推荐exec.Command,因为Cmd结构提供了一个字段ExtraFiles,该字段(注意不支持windows)为子进程额外地指定了需要继承的额外的文件描述符,不包含std_in, std_out, std_err

需要注意的是,ExtraFiles描述中有这样一句话:

If non-nil, entry i becomes file descriptor 3+i

这句是说,索引位置为i的文件描述符传过去,最终会变为值为i+3的文件描述符。ie: 索引为0的文件描述符565, 最终变为文件描述符3

file := netListener.File() // this returns a Dup()
path := "/path/to/executable"
args := []string{
"-graceful"} cmd := exec.Command(path, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.ExtraFiles = []*os.File{file} err := cmd.Start()
if err != nil {
log.Fatalf("gracefulRestart: Failed to launch, error: %v", err)
}

上面的代码中,netListener是一个net.Listener类型的指针,path变量则是我们要更新的新的可执行文件的路径。

需要注意的是:上面netListener.File()dup函数类似,返回的是一个拷贝的文件描述符。另外,该文件描述符不应该设置FD_CLOEXEC标识,这将会导致出现我们不想要的结果:子进程的该文件描述符被关闭。

你可能会想到可以使用命令行参数把该文件描述符的值传递给子进程,但相对来说,我使用的这种方式更为简单

最终,args数组包含了一个-graceful选项,你的进程需要以某种方式通知子进程要复用父进程的描述符而不是新打开一个。

子进程初始化
server := &http.Server{Addr: "0.0.0.0:8888"}

var gracefulChild bool
var l net.Listever
var err error flag.BoolVar(&gracefulChild, "graceful", false, "listen on fd open 3 (internal use only)") if gracefulChild {
log.Print("main: Listening to existing file descriptor 3.")
f := os.NewFile(3, "")
l, err = net.FileListener(f)
} else {
log.Print("main: Listening on a new file descriptor.")
l, err = net.Listen("tcp", server.Addr)
}
通知父进程停止
if gracefulChild {
parent := syscall.Getppid()
log.Printf("main: Killing parent pid: %v", parent)
syscall.Kill(parent, syscall.SIGTERM)
} server.Serve(l)
父进程停止接收请求并等待当前所处理的所有请求结束

为了做到这一点我们需要使用sync.WaitGroup来保证对当前打开的连接的追踪,基本上就是:每当接收一个新的请求时,给wait group做原子性加法,当请求结束时给wait group做原子性减法。也就是说wait group存储了当前正在处理的请求的数量

var httpWg sync.WaitGroup

匆匆一瞥,我发现go中的http标准库并没有为Accept()和Close()提供钩子函数,但这就到了interface展现其魔力的时候了(非常感谢Jeff R. Allen的这篇文章)

下面是一个例子,该例子实现了每当执行Accept()的时候会原子性增加wait group。首先我们先继承net.Listener实现一个结构体

type gracefulListener struct {
net.Listener
stop chan error
stopped bool
} func (gl *gracefulListener) File() *os.File {
tl := gl.Listener.(*net.TCPListener)
fl, _ := tl.File()
return fl
}

接下来我们覆盖Accept方法(暂时先忽略gracefulConn)

func (gl *gracefulListener) Accept() (c net.Conn, err error) {
c, err = gl.Listener.Accept()
if err != nil {
return
} c = gracefulConn{Conn: c} httpWg.Add(1)
return
}

我们还需要一个构造函数以及一个Close方法,构造函数中另起一个goroutine关闭,为什么要另起一个goroutine关闭,请看refer^{[1]}

func newGracefulListener(l net.Listener) (gl *gracefulListener) {
gl = &gracefulListener{Listener: l, stop: make(chan error)}
// 这里为什么使用go 另起一个goroutine关闭请看文章末尾
go func() {
_ = <-gl.stop
gl.stopped = true
gl.stop <- gl.Listener.Close()
}()
return
} func (gl *gracefulListener) Close() error {
if gl.stopped {
return syscall.EINVAL
}
gl.stop <- nil
return <-gl.stop
}

我们的Close方法简单的向stop chan中发送了一个nil,让构造函数中的goroutine解除阻塞状态并执行Close操作。最终,goroutine执行的函数释放了net.TCPListener文件描述符。

接下来,我们还需要一个net.Conn的变种来原子性的对wait group做减法

type gracefulConn struct {
net.Conn
} func (w gracefulConn) Close() error {
httpWg.Done()
return w.Conn.Close()
}

为了让我们上面所写的优雅启动方案生效,我们需要替换server.Serve(l)行为:

netListener = newGracefulListener(l)
server.Serve(netListener)

最后补充:我们还需要避免客户端长时间不关闭连接的情况,所以我们创建server的时候可以指定超时时间:

server := &http.Server{
Addr: "0.0.0.0:8888",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 16}

译者总结

译者注:

  1. refer^{[1]}

    在上面的代码中使用goroutine的原因作者写了一部分,但我并没有读懂,但幸好在评论中,jokingus问道:如果用下面的方式,是否就不需要在newGracefulListener中使用那个goroutine函数了
func (gl *gracefulListener) Close() error {
// some code
gl.Listener.Close()
}

作者回复道:

Honestly, I cannot fathom why there would need to be a goroutine for this, and simply doing gl.Listener.Close() like you suggest wouldn't work.... May be there is some reason that is escaping me presently, or perhaps I just didn't know what I was doing? If you get to the bottom of it, would you post here, so I can correct the post if this goroutine business is wrong?

作者自己也较为疑惑,但表示像jokingus所提到的这种方式是行不通的

译者的个人理解:在绝大多数情况下,需要一个goroutine(可以称之为主goroutine)来创建socket,监听该socket,并accept直到有请求到达,当请求到来之后再另起goroutine进行处理。首先因为accept一般处于主goroutine中,且其是一个阻塞操作,如果我们想在accept执行后关闭socket一般来说有两个方法:

  • 为accept设置一个超时时间,到达超时时间后,检测是否需要close socket,如果需要就关闭。但这样的话我们的超时时间可定不能设置太大,这样结束就不够灵敏,但设置的太小,就会对性能影响很大,总之来说不够优雅。
  • accept方法可以一直阻塞,当我们需要close socket的时候,在另一个goroutine执行流中关闭socket,这样相对来说就比较优雅了,作者所使用的方法就是这种

另外,也可以参考:Go中如何优雅地关闭net.Listener

[译]Golang中的优雅重启的更多相关文章

  1. Golang开发支持平滑升级(优雅重启)的HTTP服务

    Golang开发支持平滑升级(优雅重启)的HTTP服务 - tabalt的博客 http://tabalt.net/blog/graceful-http-server-for-golang/ http ...

  2. iota: Golang 中优雅的常量

    阅读约 11 分钟 注:该文作者是 Katrina Owen,原文地址是 iota: Elegant Constants in Golang 有些概念有名字,并且有时候我们关注这些名字,甚至(特别)是 ...

  3. Golang中设置函数默认参数的优雅实现

    在Golang中,我们经常碰到要设置一个函数的默认值,或者说我定义了参数值,但是又不想传递值,这个在python或php一类的语言中很好实现,但Golang中好像这种方法又不行.今天在看Grpc源码时 ...

  4. golang中使用Shutdown特性对http服务进行优雅退出使用总结

    golang 程序启动一个 http 服务时,若服务被意外终止或中断,会让现有请求连接突然中断,未处理完成的任务也会出现不可预知的错误,这样即会造成服务硬终止:为了解决硬终止问题我们希望服务中断或退出 ...

  5. 优雅处理Golang中的异常

    我们在使用Golang时,不可避免会遇到异常情况的处理,与Java.Python等语言不同的是,Go中并没有try...catch...这样的语句块,我们知道在Java中使用try...catch.. ...

  6. Golang中的自动伸缩和自防御设计

    Raygun服务由许多活动组件构成,每个组件用于特定的任务.其中一个模块是用Golang编写的,负责对iOS崩溃报告进行处理.简而言之,它接受本机iOS崩溃报告,查找相关的dSYM文件,并生成开发者可 ...

  7. Apache 优雅重启 Xampp开机自启 - 【环境变量】用DOS命令在任意目录下启动服务

    D:\xampp\apache\bin\httpd.exe" -k runservice Apache 优雅重启 :httpd -k graceful Xampp开机自启动  参考文献:ht ...

  8. Spring Boot 1.X和2.X优雅重启实战

    纯洁的微笑 今天 项目在重新发布的过程中,如果有的请求时间比较长,还没执行完成,此时重启的话就会导致请求中断,影响业务功能,优雅重启可以保证在停止的时候,不接收外部的新的请求,等待未完成的请求执行完成 ...

  9. apache2 重启、停止、优雅重启、优雅停止

    停止或者重新启动Apache有两种发送信号的方法 第一种方法: 直接使用linux的kill命令向运行中的进程发送信号.你也许你会注意到你的系统里运行着很多httpd进程.但你不应该直接对它们中的任何 ...

随机推荐

  1. 《Linux内核分析》第六周学习总结

    <Linux内核分析>第六周学习总结                         ——进程的描述和进程的创建 姓名:王玮怡  学号:20135116 一.理论部分 (一)进程的描述 1 ...

  2. 20145221 《Java程序设计》实验报告四:Android开发基础

    20145221 <Java程序设计>实验报告四:Android开发基础 实验要求 基于Android Studio开发简单的Android应用并部署测试; 了解Android组件.布局管 ...

  3. input file multiple 批量上传文件

    这几天维护系统,有一个批量上传文件功能,出现了一点小问题 我的笔记本选择要上传的文件很正常 但在测试环境上,别人的电脑上,选择上传文件之后 一开始,以为是代码问题,网上找了很多的资料,但还是没用,然后 ...

  4. Jquery ajax $getScript()和$getJSON和JSONP

  5. rhel6+apache2.4+mysql5.7+php5.6部署LAMP架构

    rhel6+apache2.4+mysql5.7+php5.6部署LAMP架构 2017年10月01日 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~准备阶段~~~~~~~~~~~~~ ...

  6. CSS实现垂直居中的5种思路

    前面的话 相对于水平居中,人们对于垂直居中略显为难,大部分原因是vertical-align不能正确使用.实际上,实现垂直居中也是围绕几个思路展开的.本文将介绍关于垂直居中的5种思路 line-hei ...

  7. Rob Pike 编程五原则

    Rob Pike's 5 Rules of Programming Rule 1: You can't tell where a program is going to spend its time. ...

  8. BZOJ1926[Sdoi2010]粟粟的书架——二分答案+主席树

    题目描述 幸福幼儿园 B29 班的粟粟是一个聪明机灵.乖巧可爱的小朋友,她的爱好是画画和读书,尤其喜欢 Thomas H. Co rmen 的文章.粟粟家中有一个 R行C 列的巨型书架,书架的每一个位 ...

  9. BZOJ1069 SCOI2007最大土地面积(凸包+旋转卡壳)

    求出凸包,显然四个点在凸包上.考虑枚举某点,再移动另一点作为对角线,容易发现剩下两点的最优位置是单调的.过程类似旋转卡壳. #include<iostream> #include<c ...

  10. HDU5399-多校-模拟

    Too Simple Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)Total ...