CClara轻量论坛系统

Go服务优雅退出:处理信号、连接池与任务队列

不语
不语 正式会员正式会员认证极客认证极客
发布于 2026-08-01 20:30 ·116 浏览 ·0 回复
内容摘要

Go服务在发布或重启收到SIGTERM时默认直接退出,会导致正在处理的请求被掐断、数据库连接未归还、内存中待消费的任务丢失,因此需要优雅退出:用signal.NotifyContext捕获信号并与context绑定,先停止接收新请求、等待存量请求处理完毕,再关闭连接池与任务队列。

上线跑得好好的服务,最怕什么?不是流量洪峰,不是BUG,而是发布或重启时那一瞬间的连接中断、任务丢失。尤其是Go服务,很多人以为go run跑起来就完事了,实际上进程收到SIGTERM时,默认行为是直接退出——正在处理的请求被掐断,数据库连接池里的连接没来得及归还,内存里待消费的任务灰飞烟灭。

优雅退出不是一个加分项,而是生产环境的基本体面。

信号处理:不抢跑,也不硬等

优雅退出的第一步,是听懂操作系统在说什么。Linux下常用的信号无非两种:SIGTERM(kill默认发这个)和SIGINT(Ctrl+C触发的)。Go标准库用signal.Notify捕获它们,但这里有个容易踩的坑:

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

用NotifyContext而不是裸的signal.Notify,好处是它能自动把信号和context绑定——收到信号时,ctx就被cancel了。这样后面所有的组件,都只需要监听ctx.Done(),不需要各自维护一套信号处理逻辑。

但要注意,NotifyContext的cancel不是立刻就让所有事停下来,它只是发出一个“该收拾东西了”的通知。真正体面的做法是两层退出:先停止接收新请求,再等存量请求处理完毕。

连接池:先摘流量,再关连接

如果是HTTP服务,最直接的方法是http.Server的Shutdown:

srv := &http.Server{Addr: ":8080"}

go func() {
    <-ctx.Done()
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    srv.Shutdown(shutdownCtx)
}()

Shutdown会先关闭所有监听器,停止接收新连接,然后等待所有活跃连接变为空闲。这步做对了,线上请求就不会在发布时出现502。

但连接池不只是HTTP连接。数据库连接池、Redis连接池、gRPC连接,都要一并考虑。很多人只关了HTTP server,数据库连接还敞开着,进程结束后MySQL那边会报一大堆“Aborted connection”。正确的顺序是:先摘流量(Shutdown HTTP),等业务请求都处理完了,再统一关下游连接池。

这里有个时间博弈:Shutdown的等待时间太长,发布流程会被拖死;太短,没处理完的请求被强杀。建议用可配置的超时,比如默认30秒,超过就强制退出,并打日志告警——别让一次发布把整个发布队列堵住。

任务队列:在途任务不丢,是底线

如果你的服务里有后台goroutine在消费任务队列(无论是内存channel还是Redis Streams),情况会更复杂。因为goroutine不像HTTP连接那样有标准的Shutdown接口,得自己写编排。

粗暴的做法是time.Sleep几秒再退出,这本质上是在赌运气。靠谱的做法是用WaitGroup或errgroup管理goroutine生命周期:

go func() {
    <-ctx.Done()
    close(taskCh)    // 1. 停止投递新任务
    wg.Wait()        // 2. 等待所有worker处理完当前任务
}()

核心逻辑是两步:先关生产端,再等消费端。如果任务队列本身是类似Kafka这种有Offset提交的,还要记得在任务处理完之后再提交offset,保证重启后能从正确位置续跑。

这里分享一个血泪教训:之前写过一个消费者,任务处理完先提交了offset再去更新业务库,结果业务库更新到一半进程退了——重启后任务丢了。正确的姿势是业务操作成功后再提交offset,宁可变幂等重复,也不要丢数据。

兜底逻辑:超时总得有,但超时不是终点

优雅退出最怕的不是代码写不好,是写太好了——所有goroutine都在傻等,等一个永远不会来的channel消息。建议所有优雅退出都给一个硬超时上限:

  • HTTP Shutdown:10秒
  • 数据库连接池关闭:5秒
  • 任务队列Drain:30秒

超过这个时间,日志打ERROR级,os.Exit(1)直接走。宁可牺牲这一次,也不能让进程永远挂在退出流程里。为了一次发布让整个监控系统报警“实例卡死”,那才叫得不偿失。

优雅退出这件事,本质上是在操心“进程还在,但已经停止服务”和“进程消失,但事情还没做完”之间的过渡。写起来不复杂,但生产环境里90%的连接中断、任务丢失都出在这几行代码忘了写或写错了。如果你的服务还没有这套逻辑,建议今天就把信号处理加上——下一个发布日,你会感谢现在的自己。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-172.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~