症状
部署期 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);
});