staleconfigerror(code 13388)是mongos路由缓存过时的信号,需在应用层捕获并限次(2–3次)、幂等、退避重试;必须复用同一clientsession,禁止无脑新建session,否则可能命中更旧mongos导致循环stale,且重试应配合flushrouterconfig运维操作确保拓扑收敛。

StaleConfigError 不是代码 bug,而是 mongos 路由缓存与集群实际拓扑不一致的信号;必须在应用层捕获 code: 13388 并做有限、幂等、带退避的重试,否则事务会卡死或无限重试。
为什么重试必须限制次数且不能无脑新建 session
mongos 在收到 StaleConfig 后会自动刷新路由并重试——但这是在 mongos 内部发生的,客户端无感知。如果应用层也无脑重试,就可能叠加多层重试逻辑(比如 mongos 自身重试 10 次 + 应用再重试 3 次),最终拖长事务时间甚至触发 killOp 失效问题。
- 必须复用同一个
ClientSession:新建 session 可能分配到另一个更旧的 mongos 实例,再次命中 stale 配置 - 重试上限设为 2–3 次:超过说明集群拓扑变更未收敛,应告警而非继续轮询
- 每次重试前
sleep(200 + Math.random() * 300)ms:避免所有客户端同时刷请求压垮 config server - 只对幂等操作重试:用
upsert替代insert,用$setOnInsert控制字段写入时机,防止重复插入脏数据
Node.js 驱动中如何正确捕获和响应 13388 错误
mongodb driver v4+ 默认不为分布式事务开启自动重试,StaleConfig 会直接抛出异常,需手动判断错误码。
- 检查
error.code === 13388,而不是匹配字符串"StaleConfig"(字符串可能被本地化或截断) - 不要在
catch块里重新startTransaction():应复用原有 session 的withTransaction上下文 - 示例关键逻辑:
await session.withTransaction(async () => {
try {
await collection.insertOne({ x: 1 }, { session });
} catch (err) {
if (err.code === 13388) {
await new Promise(r => setTimeout(r, 300));
throw err; // 触发 withTransaction 内置重试
}
throw err;
}
});
Python(PyMongo)里容易忽略的 session 状态陷阱
PyMongo 的 ClientSession 在抛出 StaleConfig 后会进入 invalid 状态,即使重试也不再生效——这是最常踩的坑。
- 不能依赖
session.has_ended判断:它只反映是否显式调用过end_session(),不表示 session 是否仍可用 - 必须检查
session._server_session is not None,或捕获InvalidSession异常 - 推荐做法:每次重试都新建 session(但注意要控制并发量),或在事务外封装一层 session 管理器,失败时主动 discard 并重建
- 别把重试逻辑写在
with session.start_transaction():块内:该语法糖不支持重试,一旦异常就退出上下文
真正关键的运维配合点:flushRouterConfig 不能只靠“等”
应用层重试只是兜底,根本解法是让 mongos 尽快拿到新配置。但 flushRouterConfig 命令不是万能的——它只刷新当前连接的 mongos,而生产环境往往有多个 mongos 实例在负载均衡后。
- 每次执行
sh.moveChunk、sh.addShard或sh.removeShard后,必须对**所有 mongos 实例**逐个执行db.runCommand({ flushRouterConfig: 1 }) - 不能只在某台 mongos 上执行就认为“全集群已刷新”:每个 mongos 缓存独立,不共享
- 腾讯云 DBBrain 等平台的“强制刷新路由”按钮,底层就是批量调用该命令,但要注意权限是否覆盖全部 mongos 节点
- 如果集群 chunks 超过 50 万,
flushRouterConfig可能阻塞 10 秒以上,期间所有请求 hang 住——这时要考虑分批迁移或升级到 MongoDB 6.0+ 的增量路由优化机制











