导语:Node.js 优雅停机是区分“能跑”和“敢上生产”的分水岭——它决定了每一次发布、扩缩容或节点回收时,用户请求是平稳落地还是被拦腰斩断。本文从信号语义、连接生命周期到连接池释放,完整还原一套可直接落地的工业级停机方案。
一、为什么 Node.js 优雅停机是生产可靠性的底线
很多团队把精力全部押在“如何让服务跑起来”,却极少思考“如何让服务体面地停下来”。在 Kubernetes、Docker、AWS ECS 这类云原生基础设施中,实例被重启、被缩容、被滚动更新是每天都会发生的高频事件。宿主机在终止你的进程之前,会先发送一个 SIGTERM 信号,给应用一段“善后窗口期”,随后才补上无情的 SIGKILL。
如果进程没有注册任何信号处理器,Node.js 的默认行为是立即退出。此刻正在执行的信用卡扣款、订单写入、文件上传会瞬间断裂:HTTP 连接被重置,用户看到 502;数据库事务停留在半提交状态,留下脏数据。这不是理论风险,而是无数团队在第一次上 K8s 时踩过的真实坑。
优雅停机的本质是一次“受控的降级”:先切断入口流量,再排空存量工作,最后释放底层资源。它把不可预测的进程猝死,转化为可观测、可度量的有序退出流程。
企业级架构规范
flowchart TD
subgraph 接入层 ["接入层与信号入口 (Signal & Gateway Tier)"]
A["K8s / Docker 发送 SIGTERM"] --> B["process.on 信号捕获器"]
B --> C["就绪探针置为 NotReady"]
end
subgraph 应用层 ["应用服务与连接排空层 (Drain Tier)"]
C --> D["server.close 停止接受新连接"]
D --> E["等待存量 HTTP 请求完成"]
end
subgraph 资源层 ["基础设施与资源释放层 (Resource Tier)"]
E --> F["dbPool.end 排空数据库连接池"]
F --> G["清理定时器 / 消息队列消费者"]
end
subgraph 退出层 ["进程退出层 (Exit Tier)"]
G --> H["process.exit 0 正常退出"]
E --> I["超时兜底强制退出"]
end
二、信号语义拆解:SIGTERM 与 SIGINT 到底该怎么处理
2.1 两类信号的职责边界
在 Linux 进程模型中,SIGTERM 是“请你优雅退出”的礼貌请求,可被捕获;SIGKILL 是内核级强杀,无法拦截。容器编排系统默认先发 SIGTERM,等待 terminationGracePeriodSeconds(K8s 默认 30 秒)后补发 SIGKILL。而 SIGINT 通常来自终端 Ctrl+C,用于本地开发调试。
- SIGTERM:生产环境主信号,必须注册处理器,触发完整停机流程。
- SIGINT:本地开发信号,同样走停机逻辑,便于验证。
- SIGKILL:不可捕获,只能通过缩短停机耗时来避免被它“收尾”。
2.2 幂等与重入保护
信号可能重复到达(例如运维连按两次 Ctrl+C,或编排系统重试)。停机函数必须做幂等保护:用一个 isShuttingDown 标志位拦截重复调用,否则会出现连接池被重复关闭、回调重复执行等诡异问题。
不要在信号处理器里执行同步阻塞操作(如大文件同步写、同步日志刷盘)。信号处理器运行在主线程,阻塞会拖慢整个排空流程,反而更容易触发 SIGKILL 强杀。
三、工业级实现:Express + MySQL 连接池的 Node.js 优雅停机代码
下面这份实现覆盖了信号捕获、幂等保护、HTTP 连接排空、数据库连接池释放与超时兜底五个关键点,可直接作为团队模板使用。
const express = require('express');
const mysql = require('mysql2/promise');
const app = express();
const PORT = process.env.PORT || 3000;
const dbPool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'secretpassword',
database: 'production_db',
connectionLimit: 10
});
app.get('/checkout', async (req, res) => {
try {
await dbPool.query('INSERT INTO orders (status) VALUES (?)', ['completed']);
res.status(200).send('Order processed successfully!');
} catch (error) {
res.status(500).send('Database transaction failed');
}
});
const server = app.listen(PORT, () => {
console.log(`Express app listening on port ${PORT}`);
});
let isShuttingDown = false;
function shutdownGracefully(signal) {
if (isShuttingDown) return;
isShuttingDown = true;
console.log(`Received ${signal}. Starting graceful shutdown...`);
// 兜底:超过 25 秒仍未退出则强制结束,避免被 SIGKILL
const forceTimer = setTimeout(() => {
console.error('Graceful shutdown timed out, forcing exit.');
process.exit(1);
}, 25000);
forceTimer.unref();
// 1. 停止接受新连接,等待存量请求完成
server.close(async () => {
console.log('HTTP server closed. No longer accepting new requests.');
try {
// 2. 排空并关闭数据库连接池
await dbPool.end();
console.log('MySQL connection pool drained and closed.');
clearTimeout(forceTimer);
process.exit(0);
} catch (error) {
console.error('Error shutting down database pool:', error);
process.exit(1);
}
});
}
process.on('SIGTERM', () => shutdownGracefully('SIGTERM'));
process.on('SIGINT', () => shutdownGracefully('SIGINT'));
3.1 关键设计点解读
| 设计点 | 传统粗暴写法 | 优雅停机写法 | 架构收益 |
|---|---|---|---|
| 信号处理 | 无处理器,默认退出 | 注册 SIGTERM/SIGINT | 可控退出 |
| 新连接 | 继续接受直到进程死亡 | server.close 立即拒新 | 入口收敛 |
| 存量请求 | 被强制中断 | 等待回调完成 | 零请求丢失 |
| 数据库连接 | 随进程断裂 | dbPool.end 排空 | 事务完整性 |
| 超时兜底 | 无 | 25s 强制退出 | 避免 SIGKILL |
超时兜底时间必须小于 K8s 的 terminationGracePeriodSeconds(默认 30s)。建议设置为 25s,留出 5s 缓冲给进程真正退出,否则 SIGKILL 会在排空途中落下。
四、Kubernetes 场景下的 Node.js 优雅停机协同配置
代码层面的优雅停机只是半张答卷。在 K8s 中,Pod 被删除时,端点摘除(Endpoint 移除)与 SIGTERM 发送是并行发生的,存在一个短暂的时间窗:流量仍可能被路由到正在停机的 Pod。因此必须配合就绪探针与 preStop 钩子。
lifecycle:
preStop:
exec:
command: ["sleep", "5"]
terminationGracePeriodSeconds: 30
readinessProbe:
httpGet:
path: /healthz
port: 3000
periodSeconds: 5
preStop 的 5 秒 sleep 让 K8s 有时间把该 Pod 从 Service Endpoints 中摘除,避免新流量继续涌入;随后 SIGTERM 触发应用层排空。两者叠加,才能真正做到发布期间用户无感知。
如果你的健康检查接口在停机期间仍返回 200,K8s 会误判 Pod 健康并继续导流。务必在收到 SIGTERM 的第一时间将就绪探针置为失败(NotReady),这是很多团队遗漏的关键一步。
五、落地检查清单与常见误区
- ✅ 所有长连接资源(Redis、MQ 消费者、定时器、gRPC 客户端)都纳入停机释放链路。
- ✅ 停机日志打点完整,便于事后审计“是否真的排空”。
- ✅ 在预发环境用
kill -TERM实测,观察请求成功率曲线。 - ❌ 只在 SIGINT 上做处理,忽略 SIGTERM——生产环境根本收不到 SIGINT。
- ❌ 在 server.close 回调外提前调用 process.exit,导致排空逻辑被跳过。
Node.js 优雅停机不是锦上添花的技巧,而是后端可靠性的基础设施级能力。它用几秒钟的克制,换来了数据完整性与用户体验的双重保障。把它写进团队的服务模板,才是真正的工程化落地。
本文核心技术背景源自海外一线技术研究:
Mastering Graceful Shutdown in Node.js (Saurav T.B. Pandey / DEV Community)
