Go 服务平滑升级处理并发协程的核心是在收到退出信号后,通过 context.Context 通知业务协程停止,并利用 sync.WaitGroup 等待任务完成。适用场景为 Web 服务或后台任务服务,风险边界在于必须设置强制退出超时时间,防止个别协程阻塞导致进程无法结束。
先说结论:平滑升级依赖信号捕获与上下文取消机制,需配合外部流量摘除策略。
- 适合:基于 http.Server 的 Web 服务、含长连接或后台任务的 Go 进程。
- 先准备:代码中埋入信号监听、上下文取消通道及超时强制退出逻辑。
- 验收:验证 SIGTERM 信号发送后,正在处理的请求能完成且新请求被拒绝。
命令速用版
若使用 Kubernetes 部署,配置 preStop 钩子配合 terminationGracePeriodSeconds 是标准做法;若手动管理进程,使用 kill -TERM 触发平滑退出。
# 发送终止信号触发平滑关闭
kill -TERM <pid>
# Kubernetes 部署片段示例
lifecycle:
preStop:
exec:
command: ["sleep", "10"]
terminationGracePeriodSeconds: 30为什么会这样
操作系统发送信号到进程退出之间存在时间差,Go 运行时默认不会等待协程结束。当父进程收到 SIGTERM 信号时,若直接退出,所有子协程会被强制中断,导致数据不一致或请求失败。平滑升级要求进程在退出前主动停止接收新流量,并等待现有协程执行完毕或达到超时阈值。
分步处理
第一步是捕获退出信号,使用 signal.Notify 监听 syscall.SIGINT 和 syscall.SIGTERM。第二步是关闭监听器,调用 http.Server.Shutdown 停止接受新连接。第三步是等待协程,业务协程需监听 context.Done() 退出,主协程使用 sync.WaitGroup 等待。第四步是设置超时,使用 time.After 防止无限等待。
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 等待协程完成
wg.Wait()
// 强制关闭服务器
server.Shutdown(ctx)怎么验证是否生效
查看应用日志确认收到 shutdown 信号且正在处理的请求日志显示完成。使用 curl 连续请求服务,观察信号发送后新请求是否返回 502 或连接拒绝,而旧请求是否成功返回。检查进程退出码,正常平滑退出通常为 0,强制杀死为 137 。
常见坑
部分协程未监听 context 取消信号,导致 WaitGroup 永远无法归零。数据库事务在协程退出时未回滚,产生脏数据。依赖的外部服务超时时间大于服务关闭超时时间,导致关闭过程卡死。全局变量状态未在退出时清理,影响下次启动。
常见问题
协程卡住导致无法退出怎么办?
设置强制超时时间,超时后直接终止进程。代码中需确保所有阻塞操作都带有 context 超时控制,避免单个协程无限阻塞。
Kubernetes 环境下如何配合?
配置 terminationGracePeriodSeconds 大于服务最大处理耗时,并在 preStop 中先摘除流量再发送信号。确保探针检测失败后流量完全切断再开始关闭流程。
后台定时任务怎么处理?
定时任务执行前检查全局退出标志位,若已收到退出信号则跳过本次执行。长任务需支持断点续传或补偿机制,避免中途退出导致数据不一致。
参考来源
- Go Blog, "Graceful shutdown with Go", https://go.dev/blog/graceful-shutdown
- Kubernetes Docs, "Container lifecycle hooks", https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-hooks