优雅关闭:进程退出前那几秒,决定了发版会不会掉请求

举报
yd_238022924 发表于 2026/11/02 03:59:39 2026/11/02
【摘要】 滚动发布时偶发 502、重复消费,根因常是进程收到终止信号后走得太快。本文讲清优雅关闭的四步顺序、K8s 里摘流与 SIGTERM 的时序竞态、消费端的幂等兜底,以及怎么验证它真的生效。

优雅关闭:进程退出前那几秒,决定了发版会不会掉请求

滚动发布时偶发 502、客户端收到 connection reset、队列里出现重复消费——这几类问题的根因常常是同一个:进程收到终止信号后,走得太快。

容器编排默认给的宽限期是 30 秒,但真正决定成败的不是这个数字,而是进程在收到信号到真正退出之间做了什么。很多服务的实现只有一行:收到信号就 exit(0)。于是正在处理的请求被硬生生切断,连接池里的空闲连接没有正常关闭,消息队列里已经取出但还没确认的消息在重连后被重复投递。

一、标准顺序:先停止接客,再把手上活干完

无论用什么语言,顺序都应该是一样的:

  1. 停止接收新请求:关闭监听端口,或把健康检查标记为不就绪;
  2. 等待在途请求完成:跟踪活跃请求数,归零再继续;
  3. 释放下游资源:数据库连接池、Redis 连接、消息消费通道;
  4. 强制兜底:设一个硬超时,超时后无条件退出,避免进程永远卡住。

用 Node 写出来大概是这样:

let shuttingDown = false;
let inflight = 0;

server.on('request', (req, res) => {
  if (shuttingDown) {
    res.writeHead(503, { Connection: 'close' });
    return res.end();
  }
  inflight++;
  res.on('finish', () => inflight--);
});

process.on('SIGTERM', async () => {
  shuttingDown = true;
  server.close();                       // 不再 accept 新连接
  const deadline = Date.now() + 20000;  // 给自己留 20s
  while (inflight > 0 && Date.now() < deadline) {
    await new Promise((r) => setTimeout(r, 50));
  }
  await db.end();                       // 关连接池
  process.exit(inflight > 0 ? 1 : 0);
});

有几个细节值得注意:server.close() 只是不再接受新连接,已有的 keep-alive 连接仍然可以继续发请求,所以还要配合「 shuttingDown 时直接回 503 」这道闸;res.on('finish') 才是请求真正写完的时刻,用 'close' 统计会偏早,容易在响应还没发完时就退出。

二、编排环境里更难的是时序竞态

在 Kubernetes 里,SIGTERM 和「从 Service 端点摘除」是两条并行且不保证先后的通道。常见翻车场景是:端点还没摘干净,流量仍被转发过来,而进程已经停止监听——这一小段时间里进来的请求全部失败。

工程上的处理办法是在 preStop 里先睡几秒,给负载均衡和 iptables 规则一点时间收敛:

lifecycle:
  preStop:
    exec:
      command: ["sh", "-c", "sleep 5"]
terminationGracePeriodSeconds: 30

preStop 的时间要算进宽限期里:睡 5 秒、等待在途请求 20 秒,加起来不能超过 30 秒,否则会被 SIGKILL 打断,前面做的都白费。这个预算建议写进配置注释,否则半年后没人记得为什么是 30。

三、消费端比服务端更容易被忽略

HTTP 服务至少还有「请求完成」这个明确信号,消息消费者没有。消费者收到终止信号时,手里可能正拿着一条已经取出、尚未确认的消息。如果此时直接退出,broker 会在超时后重新投递,于是业务侧看到重复执行。

两个要点:

  • 先停止拉取,再处理完手头的。批量拉取的场景下,别用「收到信号就 abort」的实现,要等当前批次确认完;
  • 业务本身要幂等。优雅关闭只能把重复概率降到很低,降到零要靠幂等键或去重表——这是最后的保险,不能省。

四、怎么验证它真的生效了

写完代码不代表生效,至少做两件事:

第一,发版时压着流量看错误率。用固定并发持续打请求,同时触发一次滚动发布;错误率应该为 0,或者只出现可以解释的 503。

第二,看退出码和日志时序。正常退出应该是 0;如果是 1 或 137,说明是被超时逼退或直接 SIGKILL,前面那套逻辑根本没跑完。日志里「开始关闭」到「进程退出」的时间差,就是这套机制的真实预算。

优雅关闭不是性能优化,而是正确性的一部分。它平时完全看不出来,只在发版、扩缩容、节点回收这些时刻兑现价值——而这几件事在任何生产环境里都会发生。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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