写过 MongoDB 的人对 db.users.find({age: {$gt: 18}}) 都很熟,但很少有人能说清这条查询从发出到返回,中间到底走了几段路。在接口层,它就是「查文档」;可一旦你要排查「为什么这条查询慢」,就必须知道它卡在链路的哪一段。

这一篇不展开索引和优化器的细节(那是后面几篇的主线),而是把一条 find 的完整执行路径铺开。后面再看索引、事务、复制时,才知道这些机制分别站在链路的什么位置。

先把机制边界说清楚

一条 find 在 MongoDB 里不是被一个黑盒直接执行,而是经过五段:driver 序列化、网络层鉴权、解析与投影、优化器选计划、执行计划调用 WiredTiger。其中优化器决定走 IXSCAN(索引扫描)还是 COLLSCAN(全表扫描),是查询性能的分水岭。

写操作(insert/update/delete)的链路在前四段几乎一样,差别在第五段:写操作要同时维护集合页、索引页、oplog 和 journal,所以写比读贵,也比读更容易抖动。

整体路径

一条 find 怎么穿过 mongod 到 WiredTiger

上面这张图先把主线铺开:driver 把查询序列化成 BSON 发给 mongod,网络层完成鉴权和请求入队,解析层把查询转成内部表示并提取投影/排序/limit,优化器在多个候选计划里选成本最低的,执行计划按计划调用 WiredTiger 访问 B-tree 索引和集合页,最后把结果组装返回。

读 MongoDB 这类系统,最重要的是别只停在命令接口,要继续追问它在存储引擎里访问了多少页、在主路径上走多远、结果集有多大。

读路径:五段路逐段拆

1. driver 层。 应用调用 find,driver 把查询条件、投影、排序序列化成 BSON,走 Wire Protocol 的 opMsg 发给 mongod。这一段的成本主要在序列化和网络往返,连接池配置不当会让请求排队,看起来像「MongoDB 慢」。

2. 网络层。 mongod 收到请求后做鉴权(SCRAM)、检查连接状态、把请求交给工作线程。高并发下这一段会出现线程竞争,mongostat 里的 queuedReaders 就是这里。

3. 解析层。 把 BSON 查询条件转成内部查询对象,提取投影(只要哪些字段)、排序、limit、skip。解析本身不贵,但投影会影响后续结果组装的成本。

4. 优化器。 这是读路径的核心。优化器根据查询条件、可用索引、统计信息,在多个候选计划里选一个。如果没有合适索引,被迫走 COLLSCAN——这就是全表扫描,文档越多越慢。优化器有时会同时跑多个候选计划,看谁先返回一批结果(trial period),再选定。

5. 执行计划 + WiredTiger。 选定计划后,执行器按计划调用 WiredTiger:先在 B-tree 索引里定位(IXSCAN),再根据需要回集合页读完整文档(FETCH)。这一段的成本取决于索引是否命中、数据是否在 Cache、要读多少页。每读一批文档,执行器会 yield(让出),避免长查询占住资源。

写路径:读路径之外的四本账

写操作前四段和读几乎一样,但第五段要同时维护四样东西,这是写比读贵的根本原因:

  • 集合页:文档本身的存储位置。
  • 索引页:每个被影响的索引都要更新。索引越多,写越贵。
  • oplog:复制集的复制日志,每个写都要记一条,供 Secondary 拉取。
  • journal:WiredTiger 的预写日志,保证崩溃可恢复。

这就是为什么 MongoDB 的写入延迟和「索引数量」「是否开启复制」「journal 刷盘策略」强相关。一个有 10 个索引的集合,每次写要做 11 次页修改;一个复制集节点,每次写还要追加 oplog 和 journal。写关注(writeConcern: {w: "majority"})会让写延迟再翻一倍,因为它要等多数节点确认。

取舍与边界

这套链路的短板是问题会跨段传播:

  • driver 连接池小 → 请求在客户端就排队,mongod 根本没收到。
  • 优化器选错计划 → 本该走索引的走了 COLLSCAN,CPU 飙升。
  • WiredTiger Cache 不足 → 索引页和数据页频繁换入换出,磁盘 IO 上升。
  • 结果集太大 → 即使查询很快,网络回传和应用反序列化也会拖慢整体。
  • 写入抖动 → 多半是 checkpoint、journal 刷盘或 oplog 追加在某个时刻集中发生。

版本演进上,MongoDB 4.4 引入了 hedged reads(同时发到两个节点读)、5.0 改进了优化器的稳定性(减少计划抖动)、7.x 持续优化时间序列和列压缩。但「driver → mongod → WiredTiger」这条五段链路的骨架始终稳定。

典型问题:用机制化例子排查

可以用一个机制化例子理解:一条 find 看起来简单,但耗时可能来自 driver 往返、请求排队、优化器选错索引、WiredTiger 读盘、大结果集回传。排查时先把链路拆开,比直接怀疑「MongoDB 慢」更可靠。

可以落到这些动作:

  • 排查慢查询先用 explain("executionStats") 看走的是 IXSCAN 还是 COLLSCAN,看 totalKeysExaminedtotalDocsExamined
  • mongostatqueuedReaders/queuedWriters,判断瓶颈在网络层还是执行层。
  • db.currentOp() 看长查询,结合 WiredTiger 的 cache 使用率判断是否内存抖动。
  • 写抖动要看 journal 刷盘间隔(storage.journal.commitIntervalMs,默认 100ms)和 checkpoint 频率。
  • 区分「查询慢」和「结果回传慢」——加 limit 和投影往往比加索引更直接。

收束:链路是排障的总地图

一条查询的执行链路,是 MongoDB 排障的总地图。先看清 driver、mongod 五段路和 WiredTiger 访问的分工,再谈索引、事务、复制,才不会在黑盒外面绕圈。下一篇会沿着优化器这条线深入,拆索引结构。


关于十三Tech

我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。

我相信 AI 是程序员的最佳搭档,也希望帮助每一位开发者更好地驾驭 AI。

如果你想继续跟完这套「图解 MongoDB」,欢迎关注公众号 「十三Tech」。后续会按文档模型、索引优化、存储引擎、高可用和分片集群这条线更新。

十三Tech公众号二维码