优雅关闭:进程退出方式决定用户看到 502 还是正常响应

收到终止信号立刻 exit 会切断在途请求。正确顺序是停止接收新连接、等在途请求跑完、再释放资源、最后退出。

滚动发布时出现零星 502,几乎都是关闭顺序错了。进程收到终止信号后直接退出,在途请求被斩断,负载均衡器那一侧就记录成失败。

正确的四步

  1. 停止接收新连接:从负载均衡的健康检查中摘除自己,或关闭监听套接字
  2. 等在途请求完成:带一个超时上限
  3. 释放资源:数据库连接、消息队列、临时文件
  4. 退出

第 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。

← 返回文章列表

评论

…