错误码 1630 并不是一个可以被忽略的业务校验错误。它更像一个信号,说明当前事务已经跨了分片,并且协调过程出现了明显延迟。MongoDB 从 4.2 开始支持多文档事务,在分片集群中这类事务需要 mongos 作为协调节点,把事务分成 prepare、commit 等阶段发送给多个分片。只要其中一个分片响应变慢、发生主从切换或者锁等待堆积,整个事务的执行时间就会被拉长,最终在客户端表现为超时或 1630。

一、跨分片事务为什么会拖慢整体性能
MongoDB 的单分片事务只需要在一个副本集内完成,写入文档后直接在主节点本地记录事务状态。而跨分片事务完全不同:会话被 mongos 接收后,mongos 必须作为协调者,向所有参与分片发送 prepare 命令。每个分片先在自己的本地存储引擎中执行写入并持有对应的行级锁,然后返回 prepare 结果。协调者只有收齐所有分片的成功应答,才会发送 commit。如果任何一个分片因为网络抖动、锁竞争或主从切换而延迟应答,其他分片也必须一直占着锁等待,整个事务就变成了木桶效应,最慢的那个分片决定最终耗时。
这个过程中的锁持有时间比单分片事务长得多。跨分片事务开始后,参与分片上的文档会被写锁保护,直到 commit 或 abort 结束。假如事务里包含一个未带分片键的查询,MongoDB 可能不得不扫描该集合的所有分片,锁竞争会被进一步放大。其他写入请求只能等待,连接池中的会话开始堆积,反映到用户侧就是接口响应时间从几十毫秒涨到几秒甚至更久。
另外,跨分片事务的网络往返次数也明显增加。一次典型的两阶段提交至少包含 prepare、prepare 应答、commit、commit 应答等多个步骤。如果 mongos 到分片之间的网络存在跨机房或跨可用区延迟,原本可以忽略的几毫秒 RTT 会被成倍放大。这也是为什么云上多可用区部署时,跨分片事务的性能下降更容易暴露。
二、故障码1630通常暴露哪些问题
故障码 1630 在实际排障中很少被当作一个独立的业务错误处理。它更像一个复合信号:协调者已经无法在规定时间内完成事务协调,或者参与分片返回了不一致的状态。日志里出现 1630 时,往往同时能看到事务超时、锁等待、WriteConflict 或参与分片重新选举等信息。如果把注意力只放在客户端捕获到的 1630 上,很容易忽略真正的根因。
常见触发条件可以归纳为几类。第一类是事务范围过大,比如把订单创建、库存扣减、积分变更、审计日志写入全部塞进同一个跨分片事务,文档分布在不同分片上,协调成本直接上升。第二类是查询没有带上分片键,导致 mongos 无法将请求精确定位到目标分片,只能广播到多个分片。第三类是事务持续时间过长,超过 transactionLifetimeLimitSeconds 的默认 60 秒后,协调者会主动中止事务,相关错误被包装后在客户端表现为 1630。第四类是参与事务的分片发生了主从切换,新的主节点没有保留原事务上下文,协调者收到 NoSuchTransaction 或状态不一致。
除此之外,锁等待超时也容易被误判为 1630。MongoDB 对事务等待锁的最大时间有单独参数控制,默认值很小。跨分片事务在竞争热点文档时,如果无法在很短时间内拿到锁,事务会直接失败。把参数调大虽然能减少失败,但也会让锁持有时间变长,需要根据业务容忍度权衡。
因此,看到 1630 后不要急着重试。先确认这个错误是从哪个 mongos 抛出,再看参与分片在对应时间段的日志,重点检查是否有 stepDown、slow commit 或 long-running lock wait。
三、用currentOp和日志定位慢事务
定位跨分片事务性能问题的第一步,是找到当前正在执行或者卡住的事务。可以连接任意 mongos,执行 currentOp 命令,过滤 transaction 字段存在且操作处于活跃状态的会话。通过 secs_running 排序,可以找出运行时间最长的那个事务。下面的脚本会输出 opid、描述、运行秒数、事务参数以及是否正在等待锁。
var runningTxns = db.currentOp({
"$ownOps": false,
"active": true,
"transaction": { "$exists": true }
}).inprog;
runningTxns.sort(function(a, b) {
return b.secs_running - a.secs_running;
});
runningTxns.forEach(function(op) {
printjson({
opid: op.opid,
desc: op.desc,
secs_running: op.secs_running,
transaction: op.transaction,
waitingForLock: op.waitingForLock
});
});
拿到 opid 后,可以继续用 killOp 谨慎终止已经运行过久的事务,但终止前要确认不会造成业务数据不一致。更好的方式是结合 mongos 日志。事务协调过程中通常会出现 TransactionCoordinator 相关日志,记录参与分片、准备阶段耗时、决定提交还是中止等关键信息。日志中如果反复出现 prepare 阶段超过数秒,基本可以判断是存储层压力或网络问题,而不是应用逻辑本身。
除了 currentOp 和日志,还可以观察 serverStatus 中的 transactions 指标。currentActive 表示当前活跃事务数量,totalAborted 和 totalCommitted 的比值可以辅助判断事务成功率和失败趋势。如果 totalAborted 在业务高峰快速上升,说明集群承受不住当前事务负载,需要降低跨分片事务频率或缩小范围。
四、优化跨分片事务的落地策略
最有效的优化方向,是尽量避免真正的跨分片事务。如果业务上无法避免多集合写入,至少可以在分片键设计时让相关文档落在同一个分片。比如订单库中订单主表和订单明细表都使用 user_id 或者 order_id 的前缀作为分片键,那么同一个用户或订单的多条记录大概率会路由到同一分片,事务就不再需要跨分片协调。这个方案需要从建库初期就考虑,如果已经上线,可以用 refineCollectionShardKey 或重建集合的方式调整,但要评估迁移成本。
第二个方向是缩小事务范围。事务外能完成的查询和写入就不要放进事务内,尤其不要把全表扫描或者大范围聚合放到事务中。只保留真正需要原子性的操作,比如库存扣减和订单状态更新。Node.js 驱动中可以使用 withTransaction 自动重试瞬态错误,但要注意事务回调和回调内部所有数据库操作都必须传递同一个 session。示例代码如下。
const session = client.startSession();
try {
await session.withTransaction(async function() {
const orders = client.db('shop').collection('orders');
const inventory = client.db('shop').collection('inventory');
await orders.updateOne(
{ _id: orderId, userId: userId },
{ $set: { status: 'paid' } },
{ session: session }
);
await inventory.updateOne(
{ sku: sku, userId: userId },
{ $inc: { stock: -1 } },
{ session: session }
);
}, {
readConcern: { level: 'snapshot' },
writeConcern: { w: 'majority' }
});
} finally {
await session.endSession();
}
第三个方向是适度调整事务相关参数。如果业务确实需要更长事务,可以将 transactionLifetimeLimitSeconds 提高到 120 秒甚至更多,但这样做会延长锁持有时间,需要配合监控。对于锁等待过于敏感的场景,可以适度增大 maxTransactionLockRequestTimeoutMillis,让事务在热点文档上等待更久而不是立即失败。参数调整前应在测试环境压测,不能只凭日志告警直接放大。
如果跨分片事务已经严重影响核心链路,还可以考虑改成补偿事务或最终一致性方案。比如库存扣减成功后,通过消息队列异步创建积分记录,失败时用补偿任务反向处理。这种方案牺牲了强一致性,但能显著降低跨分片协调压力,适合对实时一致性要求不高的业务。
五、建立长期监控和预防机制
跨分片事务性能问题不会只出现一次,所以需要在监控体系里增加针对事务的指标。可以采集 mongos 和分片节点的 serverStatus,提取 transactions.currentActive、transactions.totalStarted、transactions.totalAborted 等字段,按时间序列展示。当 active 事务持续超过一定数量,或者 abort 比例突变时,触发告警。
同时要关注锁等待指标。高并发写入下,分片上的锁等待时间可以直接反映事务竞争程度。通过监控 maxLockWaitTime 或定期执行 currentOp 统计 waitingForLock 为 true 的会话数量,可以提前发现热点分片。很多性能问题在用户投诉前就已经能从这些指标中看出趋势。
最后,预防比事后优化更重要。每次新增集合或调整写入逻辑时,都应确认事务是否会跨分片,并在代码评审阶段检查事务内查询是否带上了分片键。对于已经存在的慢事务,要定期复盘慢日志和事务日志,把频繁跨分片的事务链路改造为单分片事务或异步流程。这样才能避免 1630 故障码反复出现。