Gracefully-Shutdown
时间是宇宙唯一的资源 — 我
因为上周准备code review的时候遇到了这个问题,大家也给出了一些建议,和imcom哥也进行了一番讨论,了解了golang中context的一些用法,在这里记录备份一下,也希望对别人有所帮助。
首先简单的阐述一下问题是什么。golang编程,经常会go出去一些goroutine,抛出去不难,关键要看怎么收回来,因为程序不仅有启动,还要有退出,不管是正常退出还是非正常退出,总得有一个clean up的过程,不然就会导致程序不可控,引发非正常退出,数据丢失或者脏数据等一些乱七八糟的问题。
所以我们要在程序退出的时候对申请的资源进行释放,主动关闭已经建立的连接,完成正在进行的工作,然后退出程序。
github上有很多公开的让http服务graceful退出的lib,好像某些框架也提供了gracefulstop的方法。其本质就是停止服务,关闭端口,拒绝了新来的请求,然后把手头正在进行的请求处理完,或者设置一个超时时间强制结束正在进行的请求,然后server就stop了。这里我不谈论这个,因为有很多框架都自带了这个功能,而且不限于http服务。
但有个东西是要参考的,golang中的context。写过http服务的人都知道,每个请求的request中都带了一个context,一开始我是不知道这是做什么用的,但每个东西都有其用法,这个context的用法就是让server端和client联系起来的一个上下文,也可以理解为纽带。最基础的,比如client发出了一个请求,但是由于某种原因(网络断了,链接丢了,客户端主动关闭了,Ctrl+C了),这个连接断了,那么server端还要继续处理么,肯定不要了嘛,不然发给谁,给鬼啊,所以server就要根据这个context进行下一步处理,如果context已经done了,那么这个请求就可以直接return了。
说起来会很枯燥,看代码(跟上面说的server无关了哈,有点偏):
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
n := 3
wg.Add(n)
for i := 0; i < n; i++ {
go testContext(ctx)
}
time.Sleep(2100 * time.Millisecond)
cancel()
wg.Wait()
}
func testContext(ctx context.Context) {
defer wg.Done()
defer fmt.Println("stop")
tk := time.NewTicker(time.Second)
defer tk.Stop()
for {
select {
case <-ctx.Done():
return
case <-tk.C:
fmt.Println("tick")
}
}
}
https://play.golang.org/p/Y3X-f_jP_mZ
或者我的短域名hhh -> https://u.kfd.me/00000f
把一个函数go出去,如果不管他的话,程序退出的时候可以靠GC去清理,但在一个长期运行的服务里,不知道要go出去多少协程,如果不去管理,随着时间的增长,机器很快就会撑不住,性能受影响不说,业务也受影响,这是不允许的。
所以在上面的例子中可以看到,go出去的协程接收了一个context作为其参数,并且监听这个context的变化。context在这里要说明一点的是,context是可以衍生出新的“自己”的,即每个context都是由context.Background()
衍生出来的。更具体的解释可以看这篇文章。父context生成(context.WithCancel()
或者别的)出来的context还可以继续生成新的。而且子context被cancel了之后,父context不受影响,但父context“退出”之后,所有的子context以及子context生成的context也都会done。这就给我们程序分层带来了好处。主进程有一个大的context,由于某种原因退出后,context自动cancel掉,然后子服务随即退出。或者,如果一个程序需要提供多种服务,那么每个子服务就要select这个context,然后主程序g掉之后,自动clean up。
这里有个选择,就是当我们初始化一个service的时候,假设这个service有两个方法,Serve
和Shutdown
,那么还有必要主动Shutdown吗。我个人认为哈,在select了context之后,是没必要主动shutdown服务的,因为在初始化的时候就(或许,全凭个人)已经挂上了clean up的操作,只要context done了就进行,所以Shutdown的接口有点多余了。
但如果在初始化的时候没有利用context去管理退出之后的清理工作,而是利用这个context进行了别的操作,放到了别的goroutine当中,那么还是有必要加一个Shutdown的接口,手动退出的。
接下来还一个问题,如何知道main中的所有service都退出了。或者说,如何知道某一个service已经graceful shutdown了。
这就需要一个wait group。
我们可以在每个service中添加一个local的wait group,每go出去一个函数,都在wg里加一,然后退出(GracefulShutdown()
)的时候就有数了。这里要注意的一个问题是错误处理,如果在退出的时候出现了异常,即如果有的goroutine没能退出,那么在shutdown的时候就要直接return error
而不是继续wg.Wait()
了,因为出现了错误,service永远不会(可能,也许,大概,全凭个人怎么写,也可以加上重试机制,或者强制退出,时间超时等等)优雅退出了。
其实还是将wait group在main里的用法,扩展到了每一个package,每个package负责自己的context,自己的wait group,不论是初始化还是退出,个人管个人的,不建议有全局的wait group,不方便管理,而且逻辑上也不好区分和分层。
有了context
作为退出信号的发起者,wait group
作为保证,那么基本上可以放心的让程序退出了。