各位好。
Go HTTP server 安全退出是一个比较常见的需求,妥善使用可以降低发版时的服务抖动。
我在最近才发现两年多以来,我的实现一直有问题,原因是我没好好读文档┑( ̄Д  ̄)┍
,另外Shutdown()
这个方法的 API 设计略微有些毛刺,望文生义容易翻车。
我把我的经历写了下来,希望能抛砖引玉,欢迎各位交流拍砖。
谢谢。
1
SorcererXW 2021-09-23 13:28:14 +08:00 1
我的理解是不是将退出操作放在主协程,其实 server 放在另外一个协程,就能避免立即退出?
func main() { go server.Server() <- signal server.Shutdown(ctx) } |
2
nanmu42 OP @SorcererXW 这里就见仁见智了,ListenAndServe()在 goroutine 中的话,错误处理大概率是 log.Fatal(err)这样的操作,如果服务并不是主动退出的(比如启动时立马遇到端口占用的错误),主函数 main()中的 defer 是不会执行的。我这里用了一些额外的复杂度让安全退出的逻辑更圆满了一些。
|
3
v2Geeker 2021-09-23 14:23:09 +08:00 1
见识了。
我一般都是 ListenAndServe 和 Shutdown 都放在 2 个不同的 gorountine 中,用 sync.WaitGroup 的 Wait 来等待结束,于是我好像从来没意识到 Shutdown 有这样的问题。 |
4
whitedroa 2021-09-23 14:44:59 +08:00 1
@nanmu42 没太看懂:“错误处理大概率是 log.Fatal(err)这样的操作” 这句话是什么意思呢
“如果服务并不是主动退出的(比如启动时立马遇到端口占用的错误),主函数 main()中的 defer 是不会执行的” 这里 main 中的 defer 为什么不会执行呢,是因为其他协程 panic 导致程序直接退出吗? |
6
FrankAdler 2021-09-23 15:30:31 +08:00 1
@nanmu42 #2 想让 main 的 defe 能执行,就需要让 main 正常退出,那在 goroutine 里出错错误的时候,发送 sign 到 main 里等等的 chan 就行了,比如
defer func() { log.Println("defer") }() server := http.Server{ Addr: fmt.Sprintf(":%d", *port), Handler: downright.SlowHandler(*sleepSeconds), } quit := make(chan os.Signal, 1) go func() { err := server.ListenAndServe() if err != http.ErrServerClosed { log.Printf("ListenAndServe err: %v", err) quit <- syscall.SIGTERM } }() signal.Notify(quit, os.Interrupt) <-quit log.Println("waiting for shutdown finishing...") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { log.Fatalf("shutdown err: %v", err) } log.Println("shutdown finished") ListenAndServe 不管出现什么级别的错误都可以处理(只要不调用 os.Exit ),毕竟 main 一直在等待新号阻塞着 |
7
FrankAdler 2021-09-23 15:30:59 +08:00
有几个错别字,见谅。。
|
8
nanmu42 OP @whitedroa 在另一个 goroutine 里做 ListenAndServe(),它的返回值一般是用 log.Fatal()来接的,要不然就不晓得 HTTP 服务启停状态了。
log.Fatal()调用的是 os.Exit(),这个方法会造成 go 程序直接退出,main()里的 defer 函数不运行(博文里链了 godoc 链接)。 当然也可以不用 log.Fatal(),自己搞定同步,但是那样复杂度上来了。 |
9
nanmu42 OP @FrankAdler 是呢,这样可以让 main()执行完,我们的思路挺类似呢。
|
10
zouzou0208 2021-09-23 15:38:06 +08:00 1
写的好,学到了,感谢感谢。之前没主要到过这个。还给出了代码,真贴心。
|
11
hhaobao 2021-09-23 16:08:23 +08:00 1
go 1.16 后, Notify 可以改成 NotifyContext
|
12
Yoock 2021-09-23 16:21:41 +08:00 1
学到了
|
13
index90 2021-09-23 17:30:06 +08:00 1
你这样子改,如果一个程序要启动多个 http 服务就不行了。
如果你是担心 goroutine 启动 server,server 意外退出的问题,用 errgroup 好了。 比较完善的多 routine 和退出处理,用 rungourp 一把梭 |
14
hu8245 2021-09-23 18:14:05 +08:00
with context 啊
|
16
lesismal 2021-09-24 12:34:28 +08:00
Shutdown 只是减少了停服的短暂过程的抖动数量,对于当时 qps/tps 非常高的服务效果好点。但仍可能存在在途请求(网络链路、尚未被读取的内核缓冲区中的数据)被放弃、请求方失败、超时的情况。
所以虽然冠以了 graceful 之名,只是 part of graceful,仍然需要业务层来保证需求的实现,以及集群架构层面的高可用性部署、调度等相关支持,业务逻辑相关的重试、幂等保证是必需品。 即使不是程序本身的导致的抖动,也存在其他网络链路抖动的影响比如 ISP 线路故障,仍然是需要集群架构层面的高可用性部署、调度等做相关的强支持,而这些支持能够同时从更高层面照顾到程序引起的抖动造成的影响。( ISP 、程序抖抖可能造成请求方重试、累积踩踏雪崩之类的,都是需要网络、运维、高可用部署相关的这些保障) 有了业务和运维层面的保证,对于绝大多数业务量级而言,程序引起的短暂抖动其实影响很小。而对于中小厂的流量,抖那么一下,受影响的请求数也是极小的。 所以其实 graceful Shutdown,虽然照样用,但实际发挥的用处不大。 顺便蹭蹭,欢迎关注我的两个框架,高性能、海量并发相关: https://www.v2ex.com/t/794435#reply3 |
17
zoharSoul 2021-09-24 21:01:37 +08:00 1
博客是用什么搭的啊 挺好看的
|
19
index90 2021-09-26 09:27:52 +08:00 1
|
22
Lihanx9 2022-01-05 17:13:51 +08:00 1
https://gin-gonic.com/docs/examples/graceful-restart-or-stop/
是不是和 Gin 给的这个示例异曲同工?如果直接用 ctx.Done() 的 channel ,是不是就可以不用自己创建一个 s.shutdownFinished 这个 channel 了呢?😳 |