25. MongoDB WiredTiger 存储引擎深入

WiredTiger 快照隔离与 MVCC、Cache 与 Page 结构、Checkpoint 机制、snappy/zstd 压缩、磁盘文件布局、索引构建与引擎调优参数、碎片整理

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高中当前默认,兼顾压缩比与速度
none1: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.turtlecheckpoint 指针备份一致性参考

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 / 换页
checkpointDelaySecs60调大减少刷盘尖峰崩溃恢复变长
journalCommitIntervalMs100调大提升吞吐丢日志窗口变大
blockCompressorzstd按数据特性选择高 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 或压缩配置,往往是引入问题而非解决问题。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「mongodb」更多文章

  1. 27. MongoDB 多租户与隔离架构设计
  2. 26. MongoDB 监控与可观测性实践
  3. 24. MongoDB 数据迁移与同步实战