Go 后端优雅关机:signal + context,让服务“体面地”停下来
Go 后端优雅关机:signal + context,让服务“体面地”停下来
部署过 Go 服务的人,大概都遇到过这样的场景:正在处理一个下单请求,运维一键重启,进程被 SIGTERM 直接杀掉,请求做到一半就断了,用户看到 502,订单状态还卡在中间态。问题不在重启本身,而在关机的方式太粗暴——进程没有机会把手头的事做完。
优雅关机要解决的其实就一件事:收到停机信号后,先停掉新流量,再等正在处理的请求收尾,最后才退出。Go 标准库已经把这套机制准备好了,关键是把它串起来。
第一步:接住信号,别让进程“猝死”
Linux 下 systemctl stop 发的是 SIGTERM,Ctrl+C 发的是 SIGINT。默认行为是立即终止进程。正确做法是用 signal.Notify 把这两个信号接住,转成一个 channel:
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
收到信号只是开始,真正的工作在后面。注意 channel 要带 1 的缓冲,否则信号可能在主 goroutine 忙的时候丢掉。
第二步:停掉新流量,再等旧请求
http.Server 提供了 Shutdown(ctx):它先关闭监听端口,拒绝新连接,然后等已建立的连接处理完再返回。配合一个带超时的 context,就能给收尾工作定一个上限:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("强制关闭: %v", err)
}
10 秒这个数字要根据业务定。点餐接口一般几百毫秒就能完成,10 秒足够;如果有长轮询或大文件上传,就要放宽,或者在 handler 里监听 r.Context().Done() 主动中断。
第三步:收尾的不只是 HTTP
一个真实服务里需要收尾的东西往往不止 HTTP 连接:数据库连接池要 Close(),后台定时任务要停掉,消息队列的消费者要提交 offset 再退出。做法是把这些清理动作都注册到一个统一的 shutdown 流程里,按依赖顺序倒序执行——先停消费者,再关连接池。顺序错了,消费者可能在连接池关闭后还尝试写库,直接报错。
一个常被忽略的细节:健康检查
如果服务前面挂了负载均衡,只靠 Shutdown 还不够:关机信号发出后,负载均衡可能还不知道这个节点要下线,继续把新请求打过来。稳妥的做法是在收到信号后,先让健康检查接口返回不健康,等几秒让负载均衡把节点摘掉,再执行 Shutdown。这几秒的延迟换来的是零 502,值得。
超时之后怎么办
Shutdown 的 context 超时后,剩下的连接会被强制关闭。超时不是失败,而是兜底——它保证进程一定能在有限时间内退出,不会因为一个挂住的请求永远停不下来。systemd 的 TimeoutStopSec 也要和这里对齐:如果代码里给 10 秒,systemd 默认给 90 秒,进程早已退出,不会有冲突;反过来如果代码里等 120 秒而 systemd 只给 90 秒,进程会被 SIGKILL 强杀,优雅关机就白做了。
小结
优雅关机 = 接信号 + 停新流量 + 等旧请求 + 按序清理 + 超时兜底。代码量不到五十行,却是区分“能跑”和“能放心重启”的分界线。部署脚本里多写这几行,下次半夜重启服务,就不用再盯着错误日志提心吊胆了。
- 点赞
- 收藏
- 关注作者
评论(0)