← 中文资源

简体中文版本

Node.js — 部署时 SIGTERM 杀死在途请求

修复滚动部署期间的 502 尖峰:正确处理 SIGTERM——停止接新连接、关闭空闲 keep-alive 连接、等完在途请求再按时限退出。

阅读英文原文

症状

  部署期 502 尖峰遵循严格节奏:恰好在第一个 pod 终止时开始,持续到滚动完成。本地三条命令即可复现完整故事:后台起服务、发一个 30 秒慢请求、随即 kill -TERM——curl 收到 reset,生产问题在零编排器参与下复现。另一半故事是挂起:server.close() 只是不再接受新连接并等待存量结束,而 keep-alive 套接字可能好几分钟都开着,进程撑爆宽限期后被 SIGKILL。两种结局丢的都是同一批在途请求,负载均衡将其记为每次部署必现的 502。

诊断思路

  看 pod 日志里 SIGTERM 与 exit 之间有没有 drain 记录,没有消息的空档说明关停路径形同虚设或已挂死;在 SIGTERM 时打印 server 连接数,若 server.close 之后计数不降,元凶就是滞留的 keep-alive 套接字;本地慢请求加 SIGTERM 复现可把编排器变量完全隔离。健康检查在关停期间仍返回健康、均衡器持续派新流量,是常被忽略的放大器。

根因

  SIGTERM 到达时服务器仍在服务流量。没有显式处理器时进程立即死亡;天真的 server.close() 则因 keep-alive 套接字超出宽限期而被 SIGKILL 收尾。两种情况都在途请求被丢弃,负载均衡表现为每次部署的 502 突发。

修复与验证

  把关停做成序列而非单次调用:先翻转 shuttingDown 标志让健康检查返回失败(均衡器与就绪探针立刻停止路由);server.close() 停止监听;closeIdleConnections() 强制收割空闲 keep-alive 连接(Node 18.2+);用“在途计数归零或固定时限”的 Promise.race 等待存量完成;关闭数据库、缓存、队列连接池;在宽限预算内以退出码 0 结束。drain 期间响应加 Connection: close 头,客户端不会复用垂死套接字。验证时在恒定负载下做真实滚动部署,均衡器侧断言 502 为零、连接重置为零、吞吐仅按摘除容量比例下降;进程侧记录关停时间线核对每一步,且整体落在宽限期内;再用慢请求类重跑部署,证明超时路径干净完成而不是被 SIGKILL。

代码示例

node server.js &
curl -m 30 localhost:3000/slow &   # in-flight during shutdown
kill -TERM %1                        # SIGTERM mid-request
let shuttingDown = false;

app.use((req, res, next) => {
  if (shuttingDown) res.setHeader('Connection', 'close');
  next();
});

process.on('SIGTERM', async () => {
  shuttingDown = true;
  server.close();               // stop accepting
  server.closeIdleConnections(); // kill keep-alive idlers (Node 18.2+)
  await Promise.race([
    drainInFlight(10_000),      // your counter or tracker
    delay(20_000),              // deadline under the 30s grace
  ]);
  await closePools(db, redis, mq);
  process.exit(0);
});