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

阿乐
阿乐 管理员 黑卡会员
发布于 2026-09-09 15:49 ·2 浏览 ·0 回复

上线跑得好好的服务,最怕什么?不是流量洪峰,不是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%的连接中断、任务丢失都出在这几行代码忘了写或写错了。如果你的服务还没有这套逻辑,建议今天就把信号处理加上——下一个发布日,你会感谢现在的自己。

本文转载自 阿乐技术社区,原文地址:https://www.leleweb.cn/thread-172.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~