主题
MongoDB 存储模型与 WiredTiger
一句话:WiredTiger 用「内存缓存 + journal 预写日志 + checkpoint 定期落盘」换写入吞吐和崩溃恢复能力;代价是内存要按 cache 单独规划,持久化语义要看写关注和 journal 配置。
适用版本:MongoDB 7.0 / 8.0。WiredTiger 自 3.0 起成为默认引擎;MMAPv1 已在 4.2 移除。
为什么需要
文档数据库如果每次写入都直接改磁盘页,随机 IO 会把吞吐压死;如果只写内存不管磁盘,进程一崩就丢数据。存储引擎要同时回答三个问题:
- 写操作怎样尽快返回,又不至于丢太多数据?
- 内存里的脏页什么时候、以什么顺序落盘?
- 崩溃后从哪里开始恢复,恢复到哪个一致点?
WiredTiger 的答案是:写内存页并记 journal,后台 checkpoint 定期把一致快照落盘;恢复时从最近 checkpoint 开始重放 journal。这套机制和 MySQL 的 redo log + checkpoint 思路相近,但细节不同。
核心机制
BSON 文档
- BSON 是二进制 JSON,除字符串、数字、布尔、数组、对象外,还有
Date、ObjectId、Binary、Decimal128、Timestamp等类型。 - 单个用户文档最大 16MB,嵌套深度最大 100 层;超过 16MB 要用 GridFS 或引用化拆分。
_id默认是 12 字节 ObjectId:4B 秒级时间戳 + 5B 机器/进程随机值 + 3B 自增计数器。前 4 字节让它大致按创建时间有序,对 B 树索引友好。
并发控制与锁粒度
WiredTiger 采用文档级并发控制:
- 数据库和集合上拿的是意向锁(IS / IX),表示「我打算在更细粒度上加锁」,不是把整库或整集合锁住。
- 真正的写冲突发生在文档级;两个事务改不同文档可以并行,改同一文档时后写者检测到冲突并失败重试。
- 读操作走 MVCC 快照,不阻塞写;写也不阻塞读。只有少数管理操作(如某些 DDL、rename)需要更粗的锁。
所以「WiredTiger 是库级锁 / 集合级锁」是错的;「文档级锁比 MySQL 行锁更细」也只对了一半——粒度相当,差异主要在冲突检测和重试方式。
缓存、journal 与 checkpoint
- Cache:默认大小为
max(256MB, 50% × (RAM - 1GB))(MongoDB 3.4 起);可用--wiredTigerCacheSizeGB或配置项覆盖。缓存满时后台 eviction 线程把干净页或脏页淘汰到磁盘。 - Journal:WiredTiger 的预写日志,默认开启(
storage.journal.enabled=true)。普通写入按journalCommitInterval批量提交,默认 100ms;写关注带j:true时,每次写操作都会等待 journal fsync 后才返回。 - Checkpoint:至少每 60 秒做一次,把当前内存中的一致快照写入数据文件;journal 增长到约 2GB 时也会触发(具体阈值以官方文档为准)。checkpoint 成功后,checkpoint 之前的 journal 才可以安全回收。
- 恢复:启动时找到最近一个完整 checkpoint,重放该 checkpoint 之后的 journal,得到崩溃前已提交的写入。没有 journal 或 journal 损坏,就只能恢复到上一个 checkpoint。
压缩与索引
- 集合默认使用 Snappy 块压缩;可选
zlib、zstd,压缩率更高但 CPU 开销更大。 - 索引默认开启前缀压缩,减少重复键的存储。
- 索引本身是 WiredTiger B 树,索引页同样经过 cache 和 checkpoint,因此索引不是「直接存在磁盘上」,索引热页也占 cache。
最小上手
bash
# 单机启动,显式限制 cache 为 4GB(生产按内存规划)
mongod --dbpath /data/db --wiredTigerCacheSizeGB 4 --fork --logpath /var/log/mongod.log
mongosh "mongodb://localhost:27017"javascript
// 看 cache 使用与 eviction
db.serverStatus().wiredTiger.cache
// 看集合的存储大小、压缩与索引大小
db.products.stats()
// 建一个使用 zstd 压缩的集合(7.0/8.0)
db.createCollection("events", {
storageEngine: { wiredTiger: { configString: "block_compressor=zstd" } }
})
// 确认当前 journal 配置
db.adminCommand({ getCmdLineOpts: 1 }).parsed.storage判断是否健康:cache.used 长期贴近 cache.maximum bytes configured、evicted pages 持续增长,说明 cache 偏小或存在全表扫描;先查慢查询,再考虑加内存。
生产动作
- cache 规划:按
max(256MB, 50% × (RAM - 1GB))起步;容器部署要确认实际可用内存,不要把宿主机内存当成容器限额。 - journal:核心集合写关注用
{w:"majority", j:true, wtimeout:5000};j:false只适合可重放的导入类数据。 - checkpoint:不要手动干预,监控
db.serverStatus().wiredTiger里的 checkpoint 次数与耗时即可。 - 压缩:日志/事件类集合用
zstd换磁盘;高频更新集合先用默认 Snappy,避免 CPU 成为瓶颈。 - 恢复演练:定期在副本环境模拟 kill -9,确认能从 checkpoint + journal 恢复,而不是只验证正常重启。
- 监控:
wiredTiger.cache的 used/max、eviction 次数、wiredTiger.log的写放大;副本集还要看 oplog 窗口。
边界
- 16MB 是硬限制:不是「建议值」,超了直接写入失败;大对象拆成引用或 GridFS。
- cache 不是越大越好:WiredTiger 自己管缓存,OS page cache 还要留给数据文件;把 90% 内存给 cache 可能适得其反。
- journal 不等于永不丢数据:
j:true保证 journal 落盘,但主节点故障后的数据是否保留还取决于写关注是否到多数派。 - checkpoint 不是备份:它只是崩溃恢复的一致点;误删、逻辑损坏仍要靠备份或延迟从节点。
- 文档级并发不等于无锁:同一文档的高频更新仍会冲突重试,热点文档要拆或改用原子操作符。
讲清楚
WiredTiger 的核心是三层:内存 cache、journal、checkpoint。写操作先改内存页并追加 journal,普通情况下 journal 每 100ms 批量提交;checkpoint 至少每 60 秒把一致快照写进数据文件。崩溃后从最近 checkpoint 重放 journal,所以恢复点取决于 checkpoint 和 journal 是否完好。
锁粒度是文档级,数据库和集合上只有意向锁;读走 MVCC 快照,不阻塞写。cache 默认是
max(256MB, 50% × (RAM-1GB)),索引页也占 cache,所以「索引多」和「文档大」都会挤压热数据。生产上先把 cache、journal、checkpoint 三件事分开看:cache 决定常态性能,journal + 写关注决定丢不丢数据,checkpoint 决定恢复要多久。