MongoDB 故障码 2870 常常让运维人员感到困惑,因为它不像网络超时或认证失败那样有明确的连接层表现。该错误码通常与查询执行阶段的索引选择失败有关,尤其当集合经历大量写入、删除或分批导入后,索引统计信息没有及时更新,查询优化器仍按照旧的数据分布选择索引,最终导致实际执行时扫描的文档数量远超预期。监控系统如果对扫描行数与返回行数的比值设置了阈值,就会在同一时间窗口内捕获到异常,并把操作标记为 2870 风险操作。

从存储引擎角度看,WiredTiger 在执行查询前会读取 B-tree 索引的统计信息,包括键值分布、页数量、条目数等。这些统计信息不是每次写入都实时更新,而是通过后台任务周期性刷新。如果索引上的数据分布发生剧烈变化,例如时间序列数据按天切换、订单状态批量更新或大量文档过期删除,旧统计信息可能让优化器选择一个看起来选择性很好、实际已经严重倾斜的索引。此时监控模块会观察到较高的读取放大,进而触发保护性中断,返回 2870 错误码。
一、故障码 2870 的真实含义与触发条件
故障码 2870 并不是官方文档中单独定义的网络类错误码,在实际运维中它更多表现为查询执行器或监控保护逻辑抛出的内部状态码。它的核心特征是:操作没有因为语法错误被拒绝,而是在执行过程中被判定为资源消耗异常,随后被中止。这个判定过程依赖监控线程对当前操作扫描文档数、返回文档数、执行时间以及锁等待情况的综合评估。如果某条查询选择的索引导致 docsExamined 与 nReturned 的比值达到几百甚至上千倍,就会被视为异常。
触发 2870 常见于以下场景:复合索引字段顺序不符合查询条件,导致只能利用前缀部分;单字段索引选择性过低,例如状态字段只有三个值却承担大量过滤;索引统计信息过期,优化器错误放弃一个本来更优的索引;以及分片集群中单一分片出现热点,而监控系统未能及时感知该分片的索引负载。理解这些触发条件后,排查方向就应该从单纯看连接池状态,转向分析查询计划与监控指标之间的对应关系。
要确认一个操作是否因索引问题被中止,可以先通过 currentOp 查看活动操作,再检查 system.profile 中记录的执行统计。以下示例从当前操作中过滤包含 2870 字符的错误信息:
db.getSiblingDB("admin").aggregate([
{ $currentOp: { allUsers: true, idleConnections: false } },
{ $match: { "command": { $exists: true }, "errmsg": /2870/ } }
])
这段代码不属于普通业务查询,它需要管理员权限,并且只在出现持续异常时使用。通过它可以看到哪个命名空间、哪条查询、哪个索引被选中,以及当前已扫描的对象数量,这些信息是后续优化索引的依据。
二、如何用监控信号提前发现 2870 风险
监控并不是等故障发生后再去翻日志,而是要在索引利用率开始下降、读取放大逐步升高时发出预警。需要重点关注的指标包括:docsExamined 与 nReturned 的比值、索引命中率、慢查询数量、操作平均延迟以及 WiredTiger 缓存压力。其中读取放大比值最直观,如果一条查询只返回 10 条文档,却扫描了 20000 条文档,即使执行时间暂时可以接受,也应该视为索引异常的前兆。
索引命中率可以通过 serverStatus 中的 index 计数器观察,但不能只看命中次数,还要结合未命中后全表扫描或全集合扫描的比例。慢查询日志则可以提供具体命令和执行计划摘要。开启 profiling 后,MongoDB 会把超过阈值的操作写入 system.profile 集合,排查时可直接查询:
db.setProfilingLevel(1, { slowms: 100 })
db.system.profile.find({
"millis": { $gt: 100 },
"ns": "mydb.orders"
}).sort({ "ts": -1 }).limit(10)
除此之外,监控系统还应采集每一次操作的计划摘要,而不是只记录执行时间。因为某些索引问题不会立即拖慢单次查询,却会持续占用 CPU 和磁盘 IO。通过定期分析 explain 输出中的 winningPlan 和 rejectedPlans,可以判断优化器是否在多个候选索引之间频繁摇摆。如果同一个查询在不同时间选择了不同索引,且执行时间波动很大,说明索引统计信息不稳定,这正是 2870 风险升高的信号。
三、通过索引调整消除 2870 故障
当监控确认某条查询存在读取放大后,下一步是调整索引而不是简单加内存或升配。首先要检查现有索引是否覆盖查询条件、排序字段和投影字段。对于多条件组合查询,单列索引往往只能部分命中,需要创建复合索引,并按照等值条件优先、排序字段其次、范围条件最后的顺序排列字段。例如订单查询经常按用户编号过滤并按创建时间倒序,可以创建:
db.orders.createIndex(
{ userId: 1, status: 1, createdAt: -1 },
{ background: true, name: "idx_user_status_created" }
)
创建索引后,应再次执行原查询并使用 explain("executionStats") 查看 totalDocsExamined 和 nReturned。优化目标不是让所有查询都变成覆盖索引,而是把读取放大控制在可接受范围内。对于某些特殊查询,也可以通过 hint 指定索引临时绕过优化器的错误选择,但这只是应急手段,长期还是要解决索引统计信息偏差问题。
如果索引本身已经存在,但统计信息严重过期,单纯等待后台刷新可能不够,可以对该索引执行重新构建或使用 collStats 检查其大小和分布。索引重建会重新扫描集合,更新 B-tree 统计信息,帮助优化器恢复正确判断。此外,如果集合存在大量删除操作,建议关注 storageStats 中的 freeStorage 情况,因为空洞过多也会影响索引遍历效率。
四、建立索引与监控的闭环机制
单次修复只能解决当前问题,要避免 2870 反复出现,需要让索引变更和监控告警形成闭环。在每次发布涉及查询模式变化的代码前,应该先在预发环境用接近生产的数据分布做执行计划对比,并记录 docsExamined 基线。上线后用监控系统持续追踪该指标,一旦偏离基线超过 50%,就触发索引复核任务。
自动化巡检可以按天分析 indexStats,找出长期未被访问的索引并评估是否删除。冗余索引不仅浪费存储空间,还会增加写入成本和优化器选择错误索引的概率。使用以下命令可以查看某个集合的索引访问情况:
db.orders.aggregate([
{ $indexStats: {} },
{ $project: {
name: "$name",
accesses: "$accesses.ops",
since: "$accesses.since"
} }
]).sort({ accesses: 1 })
当 accesses 长期为 0 或增长极慢时,说明该索引很可能已经不再被查询使用,删除它可以降低维护开销。删除索引后要重新观察慢查询和读取放大指标,防止某些冷查询突然活跃。另一方面,如果监控发现某条查询的延迟在索引删除后明显上升,说明该索引并非冗余,需要恢复或调整为更合适的复合索引。
MongoDB 故障码 2870 的最终解决不是靠一个临时命令,而是依靠索引设计与监控反馈的持续配合。索引决定了查询执行的路径,监控则负责在路径偏航时发出信号。只有把两者放在同一个分析框架里,才能在 2870 出现时快速定位根因,并在它再次出现前通过索引优化和统计信息维护将其消除。