优雅关闭:进程退出方式决定用户看到 502 还是正常响应
收到终止信号立刻 exit 会切断在途请求。正确顺序是停止接收新连接、等在途请求跑完、再释放资源、最后退出。
滚动发布时出现零星 502,几乎都是关闭顺序错了。进程收到终止信号后直接退出,在途请求被斩断,负载均衡器那一侧就记录成失败。
正确的四步
- 停止接收新连接:从负载均衡的健康检查中摘除自己,或关闭监听套接字
- 等在途请求完成:带一个超时上限
- 释放资源:数据库连接、消息队列、临时文件
- 退出
第 1 步和第 2 步必须分开,这是关键。先摘流量再排空,中间需要一个观察窗 —— 让负载均衡器真正停止转发,通常要等一到两个健康检查周期。
一个可用的结构
let shuttingDown = false;
const inflight = new Set<Promise<unknown>>();
process.on('SIGTERM', async () => {
shuttingDown = true;
server.close(); // 1. 停止接受新连接
await sleep(3000); // 让负载均衡器摘掉自己
await Promise.race([ // 2. 等在途请求,带超时
Promise.allSettled([...inflight]),
sleep(25000),
]);
await db.end(); // 3. 释放资源
process.exit(0); // 4. 退出
});
// 健康检查据此返回非 200
app.get('/healthz', (req, res) =>
res.status(shuttingDown ? 503 : 200).send('ok'));
顺序里的三个常见错误
| 错误 | 后果 |
|---|---|
先 process.exit() 再关连接 |
在途请求被切断,502 |
| 不摘健康检查就退出 | 负载均衡器仍转发,同样 502 |
| 无超时地等 | 一个卡住的请求让进程永不退出,发布卡住 |
第三条最阴:必须给排空设上限。排空超时就强制退出,让负载均衡器把失败转成重试。
容器里的额外一步
容器必须把终止信号转发给实际进程。如果入口是 shell 脚本且没写 exec,信号会发给 shell,应用进程收不到,只能在超时后被 SIGKILL。
CMD ["node", "server.js"] # 直接 exec 形式,不要 shell 包装
kubernetes 侧还有 terminationGracePeriodSeconds,必须大于你的排空超时,否则 SIGKILL 会在排空完成前到达。
验证方法
别只靠日志。发布时对服务持续发请求,统计非 2xx 的比例:
while true; do curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/; done
滚动重启期间应当全是 200。出现任何 502,说明四步里有一步没做对。
关闭是一个流程,不是一行 exit。停止接收、排空、释放、退出,缺一步就会在发布时留下 502。

评论
…