谈 Golang http.Server 安全退出:容易被误用的 Shutdown()方法

2021-09-23 12:55:22 +08:00
 nanmu42

各位好。

Go HTTP server 安全退出是一个比较常见的需求,妥善使用可以降低发版时的服务抖动。

我在最近才发现两年多以来,我的实现一直有问题,原因是我没好好读文档┑( ̄Д  ̄)┍,另外Shutdown()这个方法的 API 设计略微有些毛刺,望文生义容易翻车。

我把我的经历写了下来,希望能抛砖引玉,欢迎各位交流拍砖。

谢谢。

3108 次点击
所在节点    Go 编程语言
25 条回复
SorcererXW
2021-09-23 13:28:14 +08:00
我的理解是不是将退出操作放在主协程,其实 server 放在另外一个协程,就能避免立即退出?

func main() {
go server.Server()
<- signal
server.Shutdown(ctx)
}
nanmu42
2021-09-23 13:32:03 +08:00
@SorcererXW 这里就见仁见智了,ListenAndServe()在 goroutine 中的话,错误处理大概率是 log.Fatal(err)这样的操作,如果服务并不是主动退出的(比如启动时立马遇到端口占用的错误),主函数 main()中的 defer 是不会执行的。我这里用了一些额外的复杂度让安全退出的逻辑更圆满了一些。
v2Geeker
2021-09-23 14:23:09 +08:00
见识了。

我一般都是 ListenAndServe 和 Shutdown 都放在 2 个不同的 gorountine 中,用 sync.WaitGroup 的 Wait 来等待结束,于是我好像从来没意识到 Shutdown 有这样的问题。
whitedroa
2021-09-23 14:44:59 +08:00
@nanmu42 没太看懂:“错误处理大概率是 log.Fatal(err)这样的操作” 这句话是什么意思呢

“如果服务并不是主动退出的(比如启动时立马遇到端口占用的错误),主函数 main()中的 defer 是不会执行的”
这里 main 中的 defer 为什么不会执行呢,是因为其他协程 panic 导致程序直接退出吗?
nanmu42
2021-09-23 15:30:09 +08:00
@v2Geeker 我是错了将近两年,涉及好几个服务,直到线上日志观察到了问题才醒悟的。
FrankAdler
2021-09-23 15:30:31 +08:00
@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 一直在等待新号阻塞着
FrankAdler
2021-09-23 15:30:59 +08:00
有几个错别字,见谅。。
nanmu42
2021-09-23 15:34:16 +08:00
@whitedroa 在另一个 goroutine 里做 ListenAndServe(),它的返回值一般是用 log.Fatal()来接的,要不然就不晓得 HTTP 服务启停状态了。
log.Fatal()调用的是 os.Exit(),这个方法会造成 go 程序直接退出,main()里的 defer 函数不运行(博文里链了 godoc 链接)。
当然也可以不用 log.Fatal(),自己搞定同步,但是那样复杂度上来了。
nanmu42
2021-09-23 15:36:30 +08:00
@FrankAdler 是呢,这样可以让 main()执行完,我们的思路挺类似呢。
zouzou0208
2021-09-23 15:38:06 +08:00
写的好,学到了,感谢感谢。之前没主要到过这个。还给出了代码,真贴心。
hhaobao
2021-09-23 16:08:23 +08:00
go 1.16 后, Notify 可以改成 NotifyContext
Yoock
2021-09-23 16:21:41 +08:00
学到了
index90
2021-09-23 17:30:06 +08:00
你这样子改,如果一个程序要启动多个 http 服务就不行了。
如果你是担心 goroutine 启动 server,server 意外退出的问题,用 errgroup 好了。
比较完善的多 routine 和退出处理,用 rungourp 一把梭
hu8245
2021-09-23 18:14:05 +08:00
with context 啊
nanmu42
2021-09-24 10:04:10 +08:00
@index90 兄台说得对,这个方案不适合一个程序要启动多个 http 服务,只覆盖了部分用例。
感谢你的分享。
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
zoharSoul
2021-09-24 21:01:37 +08:00
博客是用什么搭的啊 挺好看的
Goat121
2021-09-25 16:06:43 +08:00
@index90 请教下这里的 rungroup 是指什么呢
index90
2021-09-26 09:27:52 +08:00
Goat121
2021-09-27 12:05:38 +08:00
@index90 谢谢 去看看先

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/803612

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX