返回
创建于
状态
公开
在 Node.js 中实现 优雅停机(Graceful Shutdown),核心思路是:当监听到退出信号时,停止接收新请求,处理完当前正在进行的任务,最后再关闭进程。 如果不做优雅停机,直接 kill -9 或 OOM 爆炸,正在写入数据库的操作会中断,导致数据不一致或任务卡在“生成中”状态。 实现优雅停机的 4 个步骤
- 监听系统信号 Node.js 进程通过 process.on() 监听操作系统发出的信号。最常见的是:
- SIGTERM: 生产环境(如 Kubernetes, Docker, PM2)要求进程关闭时发送的信号。
- SIGINT: 用户在终端按下 Ctrl+C 时发送的信号。
- 停止接收新请求 如果是 Web 服务器(如 Express/Koa),首先调用 server.close()。这会通知服务器不再接受新的 TCP 连接,但会保持现有连接直到它们处理完毕。
- 处理存量任务(清理工作) 在完全退出前,关闭数据库连接、清理定时器、等待正在执行的异步逻辑(如生成报表)完成。
- 强制超时退出 为了防止某些任务由于死循环或超时导致进程永远无法退出,必须设置一个“硬超时”阈值。 标准代码模板 以下是一个基于 Express 和 MongoDB (Mongoose) 的标准实现: const express = require('express'); const mongoose = require('mongoose');
const app = express(); const server = app.listen(3000, () => console.log('Server running on 3000'));
// 模拟一个长耗时任务 app.get('/long-task', async (req, res) => { await new Promise(resolve => setTimeout(resolve, 10000)); // 10秒任务 res.send('Task Done!'); });
1// 优雅停机逻辑
2 function gracefulShutdown(signal) {
3 console.log(`收到 ${signal} 信号,准备关闭...`);
4
5 // 1. 设置硬超时(例如 30 秒后强制退出)
6 const forceExitTimeout = setTimeout(() => {
7 console.error('部分任务处理超时,强制退出!');
8 process.exit(1);
9 }, 30000);
10
11 // 2. 停止接收新请求
12 server.close(async (err) => {
13 if (err) {
14 console.error('关闭服务器出错:', err);
15 process.exit(1);
16 }
17 console.log('HTTP 服务器已关闭,不再接收新请求。');
18
19 try {
20 // 3. 清理外部资源(如数据库连接)
21 await mongoose.connection.close();
22 console.log('数据库连接已安全断开。');
23
24 // 4. 清除硬超时定时器并正常退出
25 clearTimeout(forceExitTimeout);
26 console.log('所有任务处理完毕,安全退出。');
27 process.exit(0);
28 } catch (dbErr) {
29 console.error('关闭数据库失败:', dbErr);
30 process.exit(1);
31 }
32 });
33 }
34
35 // 监听信号
36 process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
37 process.on('SIGINT', () => gracefulShutdown('SIGINT'));
38
39 进阶场景处理
40 1. 配合消息队列 (RocketMQ/Bull)
41 如果你在消费者中处理任务,优雅停机时需要:
42 * 调用 consumer.pause() 或 unsubscribe()。
43 * 等待当前正在处理的消息(MessageInFlight)执行完 ACK。
44 2. 处理未捕获的异常
45 除了信号,代码崩溃也应尽量优雅:
46 process.on('uncaughtException', (err) => {
47 console.error('未捕获的异常:', err);
48 gracefulShutdown('uncaughtException');
49 });
50
51 3. 容器化环境 (Docker/K8s)
52 在 Docker 中,如果你的启动脚本是 CMD npm start,Node.js 可能收不到信号(因为它不是 PID 1)。建议:
53 * 使用 CMD ["node", "app.js"](JSON 数组形式,直接启动 Node)。
54 * 或者在 K8s 中配置 terminationGracePeriodSeconds(默认为 30s),确保容器有足够时间执行上述逻辑。
55 针对你“任务卡死”问题的建议
56 在 gracefulShutdown 的逻辑里,你可以增加一步:
57 将当前内存中正在处理的任务 ID 记录回数据库,标记为“中断”或“待重启”。这样服务器重启后,你的“补偿定时任务”可以精准地锁定这些被中断的任务,而不是通过时间戳去模糊猜测。