process.exit() 会丢消息,因为其强制终止进程导致未手动 ack/nack 的消息滞留 unacked 状态,连接断开后 RabbitMQ 虽可能重入队,但消费者已退出无人处理;同时正在执行的异步业务(如数据库写入)中断引发数据不一致。

为什么 process.exit() 会丢消息?
直接调用 process.exit() 或收到 SIGINT/SIGTERM 后立刻退出,RabbitMQ 消费端大概率会丢消息——因为 channel.consume() 默认启用 noAck: false,但如果你没手动 ack 或 nack,消息会一直卡在 unacked 状态;而进程一杀,连接断开,RabbitMQ 会把所有 unacked 消息重新入队(或根据 requeue 设置决定),但此时消费者已死,没人再取,等于“逻辑上丢失”。更糟的是,如果业务逻辑正在处理中(比如写数据库、调第三方 API),强行中断会导致数据不一致。
停机前必须做完三件事
优雅停机不是“等当前消息处理完”,而是:停止拉新、确保手头消息终态明确、安全关闭连接。对应到 RabbitMQ 客户端(以 amqplib 为例):
-
取消消费订阅:调用
channel.cancel(consumerTag),让 broker 停止派发新消息(这一步比关 channel 更早,避免新消息进内存) -
等待未完成的 handler 执行完毕:用一个计数器(如
inFlight = 0)跟踪正在处理的消息数,inFlight++在consume回调开头,inFlight--在ack/nack后(注意:必须在 callback 内部、且无论成功失败都要减) -
关闭 channel 和 connection:等
inFlight === 0后,再await channel.close()→await connection.close()
connection.on('close') 不可靠,别依赖它
amqplib 的 connection.on('close') 是被动监听,触发时机不可控:可能在你刚发 close() 就触发,也可能延迟几秒;而且它不保证 channel 已真正关闭。正确做法是主动 await channel.close() 和 connection.close() 的 Promise(v0.10+ 支持返回 Promise),并配合超时控制:
async function closeChannelGracefully(channel, timeout = 5000) {
if (!channel || channel.closed) return;
const closePromise = channel.close();
const timer = setTimeout(() => closePromise.catch(() => {}), timeout);
try {
await closePromise;
} finally {
clearTimeout(timer);
}
}
同理处理 connection.close()。别在 close 后还往 channel 上发 ack,会报 Channel closed 错误。
Miller (mlr) 是一个命令行工具,用于查询、整形和重新格式化名称索引数据,如 CSV、TSV、JSON 和 JSON Lines。它将 awk、sed、cut、join 和 sort 的功能整合到一个专为结构化数据处理而构建的单一工具中。
信号监听要早注册,且只响应一次
Node.js 进程信号(SIGINT、SIGTERM)必须在建立连接后、开始 consume 前就监听,否则可能错过信号。同时需用布尔标记防重复执行:
let shuttingDown = false;
function handleShutdown() {
if (shuttingDown) return;
shuttingDown = true;
console.log('Shutting down gracefully...');
// 执行上面三步:cancel → 等 inFlight → close
}
process.once('SIGINT', handleShutdown);
process.once('SIGTERM', handleShutdown);
注意:如果用了 cluster 或 PM2,主进程发信号给 worker 时,worker 内部仍需自己监听 SIGINT;PM2 的 kill-timeout 配置也要大于你预估的最长停机时间(比如设为 10s)。
最易被忽略的是:handler 内部异步操作(比如 await db.insert())没做错误兜底,导致 inFlight 永远不减。务必在 try/catch 的 finally 块里做 inFlight--,哪怕 ack 失败也得减——否则停机永远卡住。

















