WiredTiger 是 MongoDB 自 3.2 起默认的存储引擎,也是绝大多数生产集群的性能地基。它的核心设计可概括为三句话:以 B-tree 组织数据与索引、以多版本并发控制(MVCC)实现快照隔离、以检查点(checkpoint)与预写日志(WAL)保证持久性。理解这些机制,才能真正看懂 db.serverStatus() 中的 WiredTiger 指标,也才能在内存紧张、写入放大、碎片膨胀等生产故障中做出正确决策。
本文将沿着 WiredTiger 的数据路径——从写请求如何进入内存、如何在页(page)间组织,到何时刷盘、如何压缩、如何调优——逐层展开。
1. 存储引擎的职责与架构
MongoDB 的存储引擎层位于查询执行器之下,负责文档的持久化、索引维护、并发控制与崩溃恢复。WiredTiger 是一个独立的嵌入式存储库,通过 KV 抽象(key-value)为 MongoDB 提供表(table)、游标(cursor)与事务(transaction)接口。
// 确认当前存储引擎
db.serverStatus().storageEngine
// { name: "wiredTiger", supportsCommittedReads: true, ... }
// 或通过配置确认
// mongod.conf -> storage.engine: wiredTiger
架构上,WiredTiger 主要包含四部分:
- B-tree 与 LSM:默认 B-tree 表布局,支持 LSM 可选
- Cache 层:内存页缓存(page cache)与内部缓冲
- 事务层:MVCC 版本控制、快照与冲突检测
- 检查点与日志:checkpoint + write-ahead log 提供持久性
| 层次 | 职责 | 对应 mongod 配置 |
|---|---|---|
| Cache | 页的读缓存与写缓冲 | storage.wiredTiger.engineConfig.cacheSizeGB |
| 事务 | 快照隔离、版本链 | writeConcern / readConcern 在驱动层 |
| 压缩 | 块级压缩 | blockCompressor / journalCompressor |
| 检查点 | 周期性持久化 | checkpointDelaySecs(启动参数) |
2. 快照隔离与 MVCC
WiredTiger 使用多版本并发控制为读写提供快照隔离(snapshot isolation):每个读事务看到一个一致性的快照,写事务通过版本链与冲突检测保证可串行化。
// 会话启用快照读:整个事务内看到一致的数据视图
const session = db.getMongo().startSession()
session.startTransaction({ readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } })
session.getDatabase("shop").orders.insertOne({ _id: 1, status: "new" })
session.commitTransaction()
在存储引擎内部,一次文档更新并不立即覆盖旧值,而是生成新版本:
逻辑文档版本链(概念示意)
v1(original) <- v2(updated by txnA) <- v3(updated by txnB)
[uncommitted] [committed, active]
- 读事务读取"小于等于自身快照时间戳的最大已提交版本"
- 写事务在提交时若发现与当前冲突,返回
WriteConflict,应用层重试
// 冲突示例:两个事务同时更新同一文档,后提交者冲突
// 处理方式:捕获 WriteConflict 后重试整个事务
try {
session.commitTransaction()
} catch (e) {
if (e.code === 112 /* WriteConflict */) {
print("WriteConflict,重试事务")
// 重新读取、重算、再次提交
}
}
重要:WiredTiger 的 MVCC 是"引擎内多版本",与 MongoDB 顶层的事务 API 并不等价。引擎内版本链保证引擎层的一致性,而跨文档事务、因果一致性等语义由 MongoDB 的会话与逻辑时间戳负责。
3. Cache 与 Page 结构
WiredTiger 的所有数据访问都经过内存 Cache,Cache 以页(page)为最小管理单位。理解页的构成与生命周期,是看懂缓存指标的前提。
3.1 页的三种形态
- Leaf page(叶子页):存放实际 key-value 条目
- Internal page(内部页):存放指向子页的指针与边界键,构成 B-tree 的索引层
- Root page:B-tree 根
写入数据时,修改先落在内存页上(dirty page),页内更新通过"追加 + 重写"的 copy-on-write 完成,从而支持无锁读。
// 观察缓存状态
db.serverStatus().wiredTiger.cache
// {
// "bytes currently in the cache": 3865470566,
// "maximum bytes configured": 8589934592,
// "tracked dirty bytes in the cache": 104857600,
// "pages currently held in the cache": 118236,
// "pages read into cache": 5237760,
// "pages evicted by application threads": 22173,
// "pages evicted by the application freeing spare pages": 901,
// "pages written from cache": 1782234
// }
关键指标:tracked dirty bytes 对应尚未刷盘的脏数据量;pages evicted by application threads 升高说明应用线程被迫参与驱逐,通常是 Cache 过小的信号。
3.2 页驱逐与刷盘
当 Cache 接近上限,WiredTiger 会启动后台驱逐(eviction),把干净的读缓存页与可持久化的脏页移出。驱逐由后台 eviction worker 与必要时的前台线程共同完成。
Cache 压力传导(概念)
写入速率高 -> dirty pages 增多 -> 后台刷盘跟不上 ->
应用线程被迫参与驱逐/刷盘 -> 写延迟上升
| 指标 | 健康区间 | 异常表现 |
|---|---|---|
| dirty percentage | < 20% | 写阻塞、延迟抖动 |
| eviction 驱逐页数 | 平稳 | 突增 = 缓存不足 |
| cache hit ratio | > 95%(读密集) | 命中率低 = 缓存偏小 |
4. Checkpoint 机制与持久性
WiredTiger 通过两种机制保证崩溃恢复:周期性 checkpoint 与预写日志(WAL/journal)。checkpoint 是"全量一致性快照",journal 是"增量日志",二者配合实现崩溃后最多丢失一个 journal 缓冲窗口的数据。
4.1 Checkpoint 生命周期
默认每 60 秒(或 --wiredTigerCheckpointDelaySecs 指定)创建一个 checkpoint:把内存中当前一致性的脏页刷到磁盘,并在 WiredTiger.turtle 中记录最新的 checkpoint 元数据。
// 查看 checkpoint 统计
db.serverStatus().wiredTiger.transaction
// {
// "transaction checkpoint most recent time (msecs)": 380,
// "transaction checkpoint total time (msecs)": 1250,
// "transaction checkpoint min time (msecs)": 120,
// "transaction checkpoint max time (msecs)": 2450
// }
4.2 Journal(WAL)
每次写操作先追加到 journal(默认 128MB 预分配文件),达到提交条件后才会向应用确认。writeConcern: { j: true } 要求日志落盘后才返回,w: majority 则要求在多数副本提交。
# mongod.conf:journal 与压缩
storage:
journal:
enabled: true
commitIntervalMs: 100 # 默认 100ms 批量提交
wiredTiger:
engineConfig:
journalCompressor: zstd # 日志压缩算法
| 持久性级别 | writeConcern | 语义 | 性能开销 |
|---|---|---|---|
| 最终 | { w: 1 } | 主节点内存确认 | 最低 |
| 日志持久 | { w: 1, j: true } | 日志落盘 | 中 |
| 多数派 | { w: “majority” } | 多数节点提交 | 高 |
| 多数派+日志 | { w: “majority”, j: true } | 多数节点日志落盘 | 最高 |
5. 压缩算法:snappy 与 zstd
WiredTiger 支持块级压缩,MongoDB 4.2+ 默认采用 zstd(此前为 snappy)。压缩发生在页被刷盘时,因此压缩的 CPU 成本分摊在后台,而读取未压缩的页则直接使用内存副本。
# mongod.conf:压缩配置
storage:
wiredTiger:
collectionConfig:
blockCompressor: zstd # 集合数据页压缩(默认 zstd)
indexConfig:
prefixCompression: true # 索引键前缀压缩
prefixCompressionMinIndexKeys: 4
engineConfig:
journalCompressor: zstd # journal 压缩
| 压缩器 | 压缩比 | CPU 成本 | 适用场景 |
|---|---|---|---|
| snappy | 中 | 低 | 早期默认,CPU 敏感 |
| zstd | 高 | 中 | 当前默认,兼顾压缩比与速度 |
| none | 1:1 | 无 | 数据已加密/不可压缩场景 |
// 运行时查看各集合压缩设置
db.getSiblingDB("admin").command({
getParameter: 1,
"storage.wiredTiger.collectionConfig.blockCompressor": 1
})
经验:日志型、时序型数据压缩比通常高达 5~10 倍,非常值得 zstd;而对已压缩的图片/视频二进制数据,压缩纯属浪费 CPU,应考虑
blockCompressor: none或拆分存储。
6. 内存与磁盘文件布局
一个集合在磁盘上对应多个 WiredTiger 文件,理解这些文件有助于定位磁盘占用与备份范围。
# 数据目录典型布局(/data/db)
# collection-2000-789123456.wt 集合数据表
# index-2000-789123456.wt 集合索引表
# WiredTiger 元数据指针
# WiredTiger.basecfg 基础配置
# WiredTiger.lock 锁文件
# WiredTiger.turtle checkpoint 元数据
# WiredTiger.wt WiredTiger 内部表
# journal/WiredTigerLog.0000000001 预写日志
// 查看集合占用与存储统计
db.orders.stats({ scale: 1024 * 1024 }) // 单位 MB
// {
// "size": 512, // 逻辑文档大小
// "storageSize": 128, // 磁盘实际占用(压缩后)
// "capped": false,
// ...
// }
size 与 storageSize 的差距直观反映压缩收益:storageSize 明显小于 size 是正常的。若 storageSize 持续大于实际数据、且大量删除后不回落,说明存在碎片。
| 文件 | 作用 | 关注的运维项 |
|---|---|---|
| collection-*.wt | 数据表 | 定期 compact |
| index-*.wt | 索引表 | 删除未用索引 |
| journal/* | WAL | 大小与目录监控 |
| WiredTiger.turtle | checkpoint 指针 | 备份一致性参考 |
7. 级联删除与索引构建
7.1 级联删除(Cascading Delete)
删除文档时,WiredTiger 并不会立即释放磁盘空间,而是通过异步的级联删除机制回收页。频繁的批量删除会导致"墓碑页"(tombstones)堆积,表现为 storageSize 居高不下。
// 删除后观察 storageSize 是否回落
db.orders.deleteMany({ expired: true })
db.orders.stats().storageSize // 可能仍维持高位
// 主动释放:compact(低峰期执行)
db.runCommand({ compact: "orders" })
7.2 索引构建
MongoDB 4.2+ 的索引构建采用"混合构建"(hybrid build):在构建期间用临时状态跟踪增量写入,构建完成后合并,全程不需要阻塞写入。早期版本的前台/后台构建选项已被弃用。
// 生产环境创建索引:副本集逐个节点构建,避免主从阻塞
db.orders.createIndex({ "customerId": 1, "createdAt": -1 })
// 指定选项
db.orders.createIndex(
{ "customerId": 1, "createdAt": -1 },
{ name: "cust_created", background: true, // 4.2+ 已无意义,保留兼容
collation: { locale: "en", strength: 2 } }
)
// 大集合构建限流:collMod 与临时降级写入节奏
// 实践上通过调整 maxIncomingConnections / 业务侧限流完成
警告:在数据量很大的集合上创建索引会占用大量 CPU 与磁盘 I/O,副本集场景应让其逐节点完成(构建默认在 Secondary 进行),避免同时多节点全速构建。
8. 引擎调优参数
WiredTiger 的绝大多数参数已经自适应,生产上值得手工调整的集中在缓存与检查点两块。
8.1 cacheSizeGB
默认值约为 (总内存 - 1GB) / 2。设置过大(接近总内存)会导致操作系统换页;设置过小则命中率下降。
# mongod.conf
storage:
wiredTiger:
engineConfig:
cacheSizeGB: 28 # 64GB 专用机示例
// 运行时验证缓存上限
db.serverStatus().wiredTiger.cache["maximum bytes configured"] / (1024 * 1024 * 1024)
8.2 检查点间隔
MongoDB 5.3+ 支持 --wiredTigerCheckpointDelaySecs 调整 checkpoint 间隔。调大可减少周期性刷盘尖峰,但会延长崩溃恢复时间与 journal 回放窗口。
# systemd/启动参数
# mongod --wiredTigerCheckpointDelaySecs=300 ...
| 参数 | 默认 | 调优方向 | 风险 |
|---|---|---|---|
| cacheSizeGB | (RAM-1GB)/2 | 按实际工作集上调 | OOM / 换页 |
| checkpointDelaySecs | 60 | 调大减少刷盘尖峰 | 崩溃恢复变长 |
| journalCommitIntervalMs | 100 | 调大提升吞吐 | 丢日志窗口变大 |
| blockCompressor | zstd | 按数据特性选择 | 高 CPU |
8.3 观察与验证
调优后必须用指标验证,而不是"感觉变快了"。重点观察:dirty percentage、pages evicted by application threads、cache hit ratio、写延迟 p95。
db.serverStatus().wiredTiger.cache["dirty percentage in the cache"]
db.serverStatus().wiredTiger.cache["pages evicted by application threads"]
db.serverStatus().opcounters // 各类操作计数趋势
9. 碎片整理与运维实践
长期运行的集群会因删除、更新与页分裂产生碎片,WiredTiger 的 compact 命令可以在线回收空间(期间对集合有短暂操作延迟,建议低峰执行)。
// 压缩单个集合
db.runCommand({ compact: "orders" })
// 全库压缩(逐个集合,耗时较长,谨慎)
db.getMongo().getDBNames().forEach(dbname => {
const d = db.getSiblingDB(dbname)
d.getCollectionNames().forEach(coll => {
try { d.runCommand({ compact: coll }) } catch (e) { /* 跳过无法压缩的集合 */ }
})
})
运维检查清单:
- 每月观察
storageSize与indexSize增长,判断碎片率 - 大范围删除后安排低峰 compact
- 监控 journal 目录磁盘占用,防止写满
- 版本升级前阅读 WiredTiger 变更说明,通常伴随参数与文件格式变化
结语:WiredTiger 的绝大多数默认值都经过了充分调校,生产调优的正确姿势是"先量化瓶颈,再动参数,最后用指标闭环验证"。盲目照搬别人的 cacheSizeGB 或压缩配置,往往是引入问题而非解决问题。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。