Go 服务优雅停机实战:从 SIGTERM 到连接排空,不让请求死在重启瞬间

举报
yd_46858814 发表于 2026/11/05 01:33:24 2026/11/05
【摘要】 一、问题从哪里来服务重启最容易出问题的时间点,不是启动慢,而是旧进程被杀掉的那几秒。滚动发布、扩缩容、节点驱逐时,编排系统会先给旧进程发 SIGTERM,再在宽限期结束时发 SIGKILL。如果程序收到 SIGTERM 就直接退出,正在处理中的请求会被中途切断,客户端看到的就是 502、连接重置,或者写了一半的业务结果。更隐蔽的是:新进程已经开始接流量,旧进程还没放完连接,负载均衡器在两者之...

一、问题从哪里来


服务重启最容易出问题的时间点,不是启动慢,而是旧进程被杀掉的那几秒。滚动发布、扩缩容、节点驱逐时,编排系统会先给旧进程发 SIGTERM,再在宽限期结束时发 SIGKILL。如果程序收到 SIGTERM 就直接退出,正在处理中的请求会被中途切断,客户端看到的就是 502、连接重置,或者写了一半的业务结果。更隐蔽的是:新进程已经开始接流量,旧进程还没放完连接,负载均衡器在两者之间切换,失败会被误判成“新版本有问题”。


二、优雅停机的标准流程


一个可落地的停机流程只有四步,顺序不能乱:


1. 停止接收新请求。先把就绪探针置为不就绪,让负载均衡器和服务发现把流量摘掉,再等一小段时间,让已下发的请求自然流出。Kubernetes 里常用 preStop 加一段短暂 sleep,目的就是给端点摘除留出传播时间。

2. 等待在途请求完成。调用 http.Server 的 Shutdown 方法,传入带超时的 Context,让已建立的连接在超时内跑完,而不是立刻 Close。

3. 停止后台任务。用 sync.WaitGroup 跟踪定时任务、消费协程和异步写盘,停机时先取消它们的 Context,再等待它们退出。

4. 按依赖反序关闭资源。先停接受请求的入口,再停消息消费,最后关闭数据库连接池、缓存连接和文件句柄。顺序反了,正在处理的请求会在最后一步因为连接已关而失败。


三、最小可用的代码骨架


核心思路是:主协程跑 ListenAndServe,另一个协程等信号,收到信号后做 Shutdown。


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

go func() {

  sig := make(chan os.Signal, 1)

  signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

  <-sig

  ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)

  defer cancel()

  _ = srv.Shutdown(ctx)

}()

_ = srv.ListenAndServe()


这里有三个细节值得注意。第一,Shutdown 的超时要和上游超时对齐:如果上游网关的超时只有 10 秒,停机超时设 60 秒也没有意义,在途请求早已被上游判定超时。第二,收到第二次信号时可以直接强制退出,避免运维在紧急情况下无法杀掉进程。第三,ListenAndServe 返回 ErrServerClosed 是正常停机的标志,不要把它当成启动失败去告警。


四、就绪探针和存活探针要分开


很多人把优雅停机做不好,根源是两个探针混用。存活探针只回答“进程是不是卡死了”,失败了编排系统会重启容器;就绪探针回答“现在能不能接流量”,失败了只会把端点摘掉。停机时应该先让就绪探针失败,等流量摘除后再执行 Shutdown。如果把两者写成同一个接口,停机过程中存活探针先失败,容器会被直接杀掉,Shutdown 根本来不及跑完。


五、上线前一定要做的三组验证


1. 手工验证:启动服务,用 curl 发一个故意 sleep 5 秒的请求,在请求进行中给进程发 SIGTERM,确认这个请求最终返回成功,而不是连接中断。

2. 滚动验证:在测试环境做一次滚动发布,观察发布窗口内的错误率和 502 数量,确认旧 Pod 下线期间没有错误尖峰。

3. 超时验证:把一个请求的耗时故意拉长到超过 Shutdown 超时,确认进程能在超时后退出,不会因为某个协程卡住而一直拖着不下线。


六、常见坑位清单


- 只处理 SIGINT,不处理 SIGTERM:本地 Ctrl+C 能优雅退出,一上 Kubernetes 就失效,因为编排系统发的是 SIGTERM。

- 后台协程没有纳入 WaitGroup:HTTP 请求都排空了,进程却因为某个定时任务还在跑而迟迟不退出,最后被 SIGKILL 强杀。

- 关连接池太早:入口还没停,先把数据库连接池关了,在途请求在写库时报错。

- 忽略 WebSocket 和长连接:Shutdown 只等 HTTP 请求,WebSocket 需要单独在停机时主动关闭并通知客户端重连,否则客户端会一直挂在旧连接上。

- 没有给客户端重连策略:即使服务端做到了优雅停机,客户端也应该对可重试的读请求做有限次重试,对写请求用幂等键去重,而不是无限重试。


七、结论


优雅停机不是一个 Shutdown 调用,而是一条链:摘流量、等在途、停后台、关资源,每一步都要有超时兜底。把这条链做成服务模板里的默认行为,而不是每个服务各自临场拼凑,滚动发布时的偶发 502 才会真正消失。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。