深蓝黑背景封面。左侧是 WiredTiger 写入路径示意:灰色卡片「写请求」向右经箭头进入青色描边卡片「WiredTiger 缓存(内存 B-tree · 脏页)」,再经标注「checkpoint 检查点」的箭头落进紫色描边卡片「数据文件」;写请求下方另有一条支路向下进入紫色描边卡片「journal 日志(WAL)· 先写盘」,旁注「崩溃后按日志重放」。路径下方是三枚圆角胶囊标签「文档级锁」「MVCC 快照」「snappy 压缩」,再下面一行灰色小字「旧引擎 MMAPv1 只有集合级锁,写入互相阻塞」。右侧大标题「MongoDB 的存储引擎」「WiredTiger 是怎么工作的」分两行,副标题「缓存 + 日志 + 检查点:写入路径三段式」,小注「相对旧引擎:锁从集合级细化到文档级」。左上标注「面试经典 · 数据库」,右上圆角胶囊「Day 07 · 12-04」。 图 1:本文封面,WiredTiger 的写入路径三段式。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料整理,与任何公司的当前招聘无关。「MongoDB 的存储特性与内部原理」这类题,面试官真正想听的是你能不能以 WiredTiger 为骨架讲清三件事:写入路径怎么走、并发怎么控、磁盘怎么省。以下机制描述均以 MongoDB 官方文档口径为准。

一句话结论:WiredTiger 用「内存缓存吸收写、journal 日志保崩溃、checkpoint 定一致」的三段式写入路径,用 MVCC 加文档级并发换掉旧引擎的集合级锁,再用压缩拿 CPU 换 I/O。

写入路径:缓存、日志、检查点。

写请求先进入内存里的 B-tree 缓存页,页面被标脏,此时数据文件一个字节都没动。与此同时,每次写操作追加一条 journal 记录——先写日志的 WAL 套路,journal 默认用 snappy 压缩,缓冲的记录默认每 100 毫秒刷一次盘。真正批量落盘的是 checkpoint:MongoDB 把 WiredTiger 配置为每 60 秒把一致性快照写进数据文件一次(默认最大检查点体积 2 GB 日志)。崩溃恢复的链条因此很清晰:数据文件保证一致到最后一个 checkpoint,更近的修改靠重放 journal 补齐。新 checkpoint 写完前旧 checkpoint 依然有效,所以即使落盘途中宕机,也能回到上一个有效检查点。

缓存管理:默认拿物理内存的一半。

WiredTiger 内部缓存的默认大小取「0.5 × (RAM − 1 GB)」和 0.256 GB 的较大者——4 GB 内存的机器就是 1.5 GB。这个缓存是整个 mongod 实例共享的,不按库或集合切分。读请求先查缓存,未命中才读数据文件并回填;脏页由后台线程择机驱逐,驱逐前必须保证对应日志已落盘。缓存之外还有第二层:操作系统的文件系统缓存,MongoDB 数据文件以压缩格式躺在里面,等于免费的二级缓存。注意一个常见误区:单机跑多个 mongod 实例时默认缓存假设会被打破,必须手工下调。缓存顶住之后还有一道闸:存储引擎对并发事务数本身有上限——读写票据(read/write tickets)各不超过 128,新版本会根据负载动态下调基线,票据排队变长正是集群过载的早期信号。

并发控制:MVCC 加文档级,这是相对 MMAPv1 最大的改动。

WiredTiger 用多版本并发控制:每个操作开始时拿到数据的一份时间点快照,读写各看各的快照,互不阻塞。写操作是文档级并发控制——多个客户端可以同时修改同一集合的不同文档。多数读写走乐观并发控制,全局、库、集合三个层级只保留意图锁,两个操作真撞上时由冲突方报 write conflict,MongoDB 透明重试。旧引擎 MMAPv1 是集合级锁:一个写入锁住整个集合,其他读写全部排队,高并发写入场景直接被打垮。MMAPv1 的思路是把数据文件直接 mmap 进虚拟内存、缓存管理完全甩给操作系统,自己不维护缓存也没有压缩;WiredTiger 则把缓存、快照、压缩全部收回引擎手里。从集合级到文档级,就是这道题的题眼。

压缩:用 CPU 换存储和 I/O。

WiredTiger 对集合数据默认开 snappy 块压缩,可选更狠的 zlib 或 zstd(时序集合默认就是 zstd);索引统一用前缀压缩——相邻索引键共享的公共前缀只存一份。官方文档明说取舍:压缩以额外 CPU 为代价最小化存储占用,对多数负载默认设置就是平衡点。还有一个细节值得提:数据在 WiredTiger 内部缓存里是未压缩的表示,落到磁盘和文件系统缓存里才是压缩格式,所以「压缩省内存」的说法只对索引前缀压缩成立,集合数据省的是磁盘和 I/O。

深蓝黑背景概念图。顶部一行小字「概念图 · WiredTiger 存储引擎」,主标题「缓存吸收写,日志保崩溃,检查点定一致」。第一区是读写流向图:写路径从「写请求」流入「内存缓存 B-tree」,一条支路向下进「journal 日志」保证可重放,主路经「检查点(约 60 秒或 2 GB 日志触发一次)」批量落进「数据文件」;读路径从「读请求」先查缓存,未命中才读数据文件并回填缓存。第二区是三张机制卡:「缓存管理」卡写默认缓存约为物理内存的一半(0.5 × (RAM − 1 GB)),脏页由后台线程驱逐、先记日志再复用;「文档级并发」卡写 MVCC 让读写各看自己的快照互不阻塞,锁粒度细化到文档级,旧引擎 MMAPv1 是集合级锁;「压缩」卡写数据块默认 snappy、可选 zlib 或 zstd,索引用前缀压缩,省内存也省盘。底部总结条:写先进缓存与日志、检查点批量落盘,MVCC 加文档级锁让读写互不阻塞,压缩用 CPU 换 I/O;最底一行小字补充检查点是崩溃恢复的起点,更早的数据靠 journal 重放补齐。 图 2:读写流向、三张机制卡与崩溃恢复链条。

面试现场的答题结构。

按「写入路径 → 并发模型 → 压缩与缓存 → 对比旧引擎」推进。追问预案两个。一,60 秒才 checkpoint 一次,宕机丢不丢数据:不丢已提交的写——journal 默认 100 毫秒刷盘且可配,重放补齐 checkpoint 之后的修改;真要对单条写做强保证,写关注里开 j:true 让日志落盘再返回。二,为什么缓存默认只给一半内存:另一半留给文件系统缓存和 mongod 自身,压缩的数据文件在文件系统缓存里依然是压缩态,等于花一份内存缓存两份内容。答到「checkpoint 是一致性边界,journal 负责 checkpoint 之间的窟窿」这层,持久化设计就讲透了。你在生产上调过 WiredTiger 的缓存或压缩参数吗?踩过什么坑?欢迎聊聊。