跳到正文

MongoDB 生产踩坑 ​

一句话:MongoDB 的默认值和自动机制在多数场景下够用,但写关注、分片键、文档大小、cache 和 oplog 这五个地方一旦配错,故障往往在切换或数据量上来时才暴露。

适用版本:MongoDB 7.0 / 8.0。默认值随版本变化,排查前先用命令确认实际配置。

为什么需要 ​

MongoDB 的默认值面向「单机或常规负载下先跑起来」,不是面向故障切换和十亿级数据。写关注决定主节点故障时丢不丢最近写入;分片键决定数据会不会全压在一个 shard;BSON 的 16MB 是硬限制;WiredTiger cache 只装得下热数据;oplog 是有上限的环形缓冲。这五个地方配错,平时可能完全正常,一到主从切换、数据量上涨或消费断连就集中暴露。

核心机制 ​

五类问题共用一条因果链:默认值或数据模型与真实负载不匹配,故障在边界条件被触发。

触发条件底层机制典型后果
主节点故障写关注 w:1 只等主节点落盘,未等多数派复制未复制的最近写入丢失
写入持续增长单调分片键让新 chunk 全落在最后一个 shard单 shard 热点、迁移成本高
数组持续追加BSON 单文档上限 16MB写入直接失败
无索引大范围扫描冷页挤占 WiredTiger cache热数据被换出,实例整体变慢
写入快于消费/断连过久oplog 环形覆盖旧记录从节点反复 initial sync、Change Stream token 失效

最小上手 ​

javascript
// mongosh 7.0/8.0:先确认实际生效的配置,而不是假设默认值
db.orders.getWriteConcern()
rs.status().members.map(m => ({ name: m.name, state: m.stateStr }))
rs.printReplicationInfo()
db.serverStatus().wiredTiger.cache["bytes currently in the cache"]
db.orders.find({ status: "paid" }).explain("executionStats")

预期:getWriteConcern() 显示集合实际写关注;printReplicationInfo() 给出 oplog 的时间窗口;explain 中若出现 COLLSCAN,说明该查询正在把冷页读进 cache。

一、写关注不足 → 主从切换丢最近写入 ​

现象:主节点故障,从节点升主后发现最近几秒的写入没了。

根因:写关注是 {w:1}(显式配置,或 PSA 拓扑的默认退化),主节点写完就返回,数据还没复制到多数派;主节点一挂,未复制的写入丢失。

先确认:db.orders.getWriteConcern() 看集合实际生效的写关注。

注意:MongoDB 5.0 起 PSS 副本集的默认写关注已是 majority,不要再假设「默认就是 w:1」;PSA(带 Arbiter)才会退化为 w:1。

处置:核心集合显式 {w:"majority", j:true, wtimeout:5000};日志/埋点等可重放数据才接受 w:1。切换后丢数据不是 bug,是写关注和复制模式的直接结果。

二、分片键选错 → 热点与迁移成本 ​

现象:某个 shard 的 CPU、磁盘和写入量是其他 shard 的数倍。

根因:分片键单调递增(ObjectId、自增 ID、created_at),新写入全部落在最后一个 chunk;或基数太低,chunk 无法继续分裂。

纠正一个过时说法:分片键不是绝对不能改。

  • 4.4+:sh.refineCollectionShardKey() 可追加后缀字段,缓解 chunk 无法分裂;
  • 5.0+:sh.reshardCollection() 可整体更换分片键。

两者都需要额外存储(约 1.2 倍集合大小,以官方文档为准)、IO 和切换窗口,reshard 提交阶段还会短暂阻塞写入,不是免费操作。

处置:上线前用真实量级数据验证写入分布和主要查询路由;分片键要高基数、非单调、覆盖主要查询。已经选错时,先评估 refine 是否够用,再考虑 reshard 或重建。

三、内嵌数组无限增长 → 文档超过 16MB ​

现象:写入报错 document exceeds maximum allowed size 16MB。

根因:把会持续追加的数据(物流轨迹、评论、日志)内嵌进父文档。16MB 是 BSON 硬限制,不能调大。

处置:

  • 会无限增长的关联数据改为引用;
  • 需要首页展示的,用 Subset 只嵌最近 N 条,全量另存;
  • 监控 db.collection.stats().avgObjSize 和最大文档大小。

四、全表扫描把 WiredTiger Cache 打翻 ​

现象:一个查询突然变慢,随后所有查询都变慢,实例整体延迟上升。

根因:无索引查询触发 COLLSCAN,从磁盘读入大量冷页,把索引和热数据挤出 WiredTiger cache;其他查询被迫回磁盘,形成连锁反应。

处置:

  • 用 db.currentOp() 抓运行中的 COLLSCAN;
  • 用 system.profile / 慢日志定位查询,explain 确认缺哪个索引;
  • 先 kill 长扫描止血,再补索引;不要靠加内存掩盖。

五、oplog 窗口不够 → 从节点追不上 / Change Stream 断点失效 ​

现象 A:从节点长期 RECOVERING,或 initial sync 反复失败。

现象 B:Change Stream 报 resume token not found。

根因:oplog 是有上限的环形缓冲,写入太快或断连太久,旧记录被覆盖。从节点 initial sync 期间如果 oplog 被覆盖,只能重新开始;Change Stream 的 resume token 指向已被覆盖的位置,也只能重刷。

术语纠正:从节点不是「加载 RDB」(那是 Redis 的术语)。MongoDB 从节点走 initial sync:复制集合数据、建索引、再追 oplog。进程崩溃恢复才是 checkpoint + journal replay,两者不要混为一谈。

处置:

  • Linux 64 位 oplog 默认约磁盘 5%、最少 990MB(以官方文档为准),这是下限不是建议值;按「写入速率 × 最长断连时间 × 2」复核;
  • rs.printReplicationInfo() 看 oplog 时间窗口,rs.printSecondaryReplicationInfo() 看 lag;
  • Change Stream 消费者持久化 token,并让 oplog 窗口覆盖最长消费中断。

生产动作 ​

  • 先确认默认值:db.collection.getWriteConcern()、rs.status()、sh.status()、db.serverStatus().wiredTiger.cache。
  • 慢查询:db.currentOp() 抓长操作;system.profile 按 millis 和 planSummary 聚合。
  • 复制:rs.printReplicationInfo() 看窗口,rs.printSecondaryReplicationInfo() 看延迟。
  • 分片:sh.status() 看 chunk 分布、jumbo chunk 和 balancer 窗口。

边界 ​

  • 这些坑的共同点是「配置和模型问题」,不是靠加机器能根治;先改配置/模型,再扩容量。
  • w:"majority" 降低丢数据风险,但不等于强一致读;需要读己之写还要配 readConcern:"majority" 和 primary 读偏好。
  • reshard 能改分片键,但有成本;不要把它当成日常调参手段。
  • oplog 调大只是延长窗口,不能替代备份和 Change Stream 的幂等消费。

讲清楚 ​

MongoDB 生产事故大多不是代码写错,而是默认值和数据模型在故障时才暴露。写关注不足会在主从切换时丢最近写入,先看集合实际生效的写关注;分片键单调递增会造成热点,4.4+ 能 refine、5.0+ 能 reshard,但都要额外存储和窗口;内嵌数组无限长会撞 16MB;无索引查询会把 cache 打翻,拖慢整个实例;oplog 窗口不够会让从节点反复 initial sync、让 Change Stream 的 token 失效。

排查顺序是:先用命令确认实际配置,再看 slow query / currentOp / rs.status / sh.status,最后才考虑扩容。把「从节点加载 RDB」这类 Redis 术语套到 MongoDB 上,说明还没分清 initial sync、journal 恢复和 Change Stream 三件事。

把一门技术讲透到既懂也会用