原文 Graceful Restart in Golanghtml
做者 grishagit
声明:本文目的仅仅做为我的mark,因此在翻译的过程当中参杂了本身的思想甚至改变了部份内容,其中有下划线的文字为译者添加。但因为译者水平有限,所写文字或者代码可能会误导读者,如发现文章有问题,请尽快告知,不胜感激。github
Update (Apr 2015): Florian von Bock已经根据本文实现了一个叫作endless的Go packagegolang
你们知道,当咱们用Go写的web服务器须要修改配置或者须要升级代码的时候咱们须要重启服务器,普通的重启会有一段宕机的时间,但优雅重启则否则:web
本文中的优雅重启表现为两点windows
有不止一种方法fork一个子进程,但在这种状况下推荐exec.Command,由于Cmd结构提供了一个字段ExtraFiles
,该字段(注意不支持windows)为子进程额外地指定了须要继承的额外的文件描述符,不包含std_in, std_out, std_err
。
须要注意的是,ExtraFiles
描述中有这样一句话:api
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变量则是咱们要更新的新的可执行文件的路径。less
须要注意的是:上面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}
refer^{[1]}
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通常来讲有两个方法:
另外,也能够参考:Go中如何优雅地关闭net.Listener