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

发布于 1周前 作者 phonegap100 来自 Go语言

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

各位好。

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

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

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

谢谢。


更多关于Golang Go语言中谈 http.Server 安全退出:容易被误用的 Shutdown()方法的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

26 回复

我的理解是不是将退出操作放在主协程,其实 server 放在另外一个协程,就能避免立即退出?

func main() {
go server.Server()
<- signal
server.Shutdown(ctx)
}

更多关于Golang Go语言中谈 http.Server 安全退出:容易被误用的 Shutdown()方法的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这里就见仁见智了,ListenAndServe()在 goroutine 中的话,错误处理大概率是 log.Fatal(err)这样的操作,如果服务并不是主动退出的(比如启动时立马遇到端口占用的错误),主函数 main()中的 defer 是不会执行的。我这里用了一些额外的复杂度让安全退出的逻辑更圆满了一些。

见识了。

我一般都是 ListenAndServe 和 Shutdown 都放在 2 个不同的 gorountine 中,用 sync.WaitGroup 的 Wait 来等待结束,于是我好像从来没意识到 Shutdown 有这样的问题。

没太看懂:“错误处理大概率是 log.Fatal(err)这样的操作” 这句话是什么意思呢

“如果服务并不是主动退出的(比如启动时立马遇到端口占用的错误),主函数 main()中的 defer 是不会执行的”
这里 main 中的 defer 为什么不会执行呢,是因为其他协程 panic 导致程序直接退出吗?

我是错了将近两年,涉及好几个服务,直到线上日志观察到了问题才醒悟的。

#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 一直在等待新号阻塞着

有几个错别字,见谅。。

在另一个 goroutine 里做 ListenAndServe(),它的返回值一般是用 log.Fatal()来接的,要不然就不晓得 HTTP 服务启停状态了。
log.Fatal()调用的是 os.Exit(),这个方法会造成 go 程序直接退出,main()里的 defer 函数不运行(博文里链了 godoc 链接)。
当然也可以不用 log.Fatal(),自己搞定同步,但是那样复杂度上来了。

是呢,这样可以让 main()执行完,我们的思路挺类似呢。

写的好,学到了,感谢感谢。之前没主要到过这个。还给出了代码,真贴心。

go 1.16 后, Notify 可以改成 NotifyContext

你这样子改,如果一个程序要启动多个 http 服务就不行了。
如果你是担心 goroutine 启动 server,server 意外退出的问题,用 errgroup 好了。
比较完善的多 routine 和退出处理,用 rungourp 一把梭

with context 啊

兄台说得对,这个方案不适合一个程序要启动多个 http 服务,只覆盖了部分用例。
感谢你的分享。

Shutdown 只是减少了停服的短暂过程的抖动数量,对于当时 qps/tps 非常高的服务效果好点。但仍可能存在在途请求(网络链路、尚未被读取的内核缓冲区中的数据)被放弃、请求方失败、超时的情况。

所以虽然冠以了 graceful 之名,只是 part of graceful,仍然需要业务层来保证需求的实现,以及集群架构层面的高可用性部署、调度等相关支持,业务逻辑相关的重试、幂等保证是必需品。

即使不是程序本身的导致的抖动,也存在其他网络链路抖动的影响比如 ISP 线路故障,仍然是需要集群架构层面的高可用性部署、调度等做相关的强支持,而这些支持能够同时从更高层面照顾到程序引起的抖动造成的影响。( ISP 、程序抖抖可能造成请求方重试、累积踩踏雪崩之类的,都是需要网络、运维、高可用部署相关的这些保障)

有了业务和运维层面的保证,对于绝大多数业务量级而言,程序引起的短暂抖动其实影响很小。而对于中小厂的流量,抖那么一下,受影响的请求数也是极小的。

所以其实 graceful Shutdown,虽然照样用,但实际发挥的用处不大。

顺便蹭蹭,欢迎关注我的两个框架,高性能、海量并发相关:
https://www.v2ex.com/t/794435#reply3

博客是用什么搭的啊 挺好看的

请教下这里的 rungroup 是指什么呢

谢谢 去看看先

谢谢夸奖。Hugo 和 Zzo 主题,稍微自己改了一点点。

https://gin-gonic.com/docs/examples/graceful-restart-or-stop/

是不是和 Gin 给的这个示例异曲同工?如果直接用 ctx.Done() 的 channel ,是不是就可以不用自己创建一个 s.shutdownFinished 这个 channel 了呢?😳

啊,我怀疑 Gin 的这个例子是错的…

好吧,那我再理解理解😂😂😂 谢谢

Shutdown 这个 API 挺容易踩雷的,不过服务退出和重启并不经常发生,实际影响还是有限。

在Golang的net/http包中,http.ServerShutdown()方法提供了一种优雅地关闭HTTP服务器的方式,确保正在处理的请求能够完成,同时拒绝新的连接。然而,Shutdown()方法在实际使用中确实存在一些常见的误用情况,以下是几点需要注意的地方:

  1. 确保Server正在运行:在调用Shutdown()之前,必须确保http.Server实例已经启动并监听在相应的端口上。如果服务器未启动,调用Shutdown()将不会产生任何效果。

  2. 正确处理返回值Shutdown()方法返回一个error类型的值,用于指示关闭过程中的错误。务必检查并处理这个返回值,以确保服务器能够正确关闭。

  3. 避免重复调用:一旦Shutdown()被调用,服务器将不再接受新的连接,并且会尝试关闭所有活动连接。重复调用Shutdown()可能会导致不必要的错误或资源消耗。

  4. 结合上下文使用:在长时间运行的服务器中,可以使用上下文(context.Context)来管理服务器的生命周期。通过监听上下文的取消信号,可以在适当的时候调用Shutdown()方法。

  5. 日志记录:在调用Shutdown()时,建议记录日志,以便在服务器关闭时留下审计轨迹,帮助排查潜在的问题。

总之,虽然Shutdown()方法提供了一种优雅关闭HTTP服务器的方式,但在实际使用中需要特别注意上述几点,以避免误用和潜在的问题。

回到顶部