1. 项目背景

业务场景:本地生活电商的 DBA 发现一个诡异的现象——高峰期 MongoDB 的磁盘 IOPS 飙升至 15000,但数据写入量明明只有 5000 IOPS。多出来的 10000 IOPS 是哪里来的?查看 WiredTiger 的统计指标后发现——pages evicted by application threads 数量飙升,意味着 WiredTiger 缓存放不下工作集,频繁地把脏页刷到磁盘来腾空间给新数据,而这些被淘汰的页很快又被查询读回来——造成了大量的"读-淘汰-再读"的磁盘抖动。

还有一个隐藏成本——用了 snappy 压缩算法,CPU 使用量比预期高了 30%,而实际数据体积只节省了 20%。如果换用 zstd 压缩,能多节省 15% 的空间但 CPU 再涨 20%——怎么权衡?

痛点:WiredTiger 是 MongoDB 的"内核",但多数人对它只知其名。缓存怎么淘汰、B-Tree 怎么组织、Journal 怎么保证崩溃恢复、压缩算法怎么选——这些存储引擎的内部机制如果不理解,就只能在 serverStatus 的数字面上看问题,看不懂根因。

2. 项目设计

小胖(看着 Grafana 里 WiredTiger 的 eviction 曲线):大师!缓存淘汰(eviction)是啥?为什么缓存满了不直接报错而是要淘汰旧数据?

大师:WiredTiger 的缓存就像你办公桌上的书架——桌面上只能放固定数量的书(缓存大小),当你需要看一本新书时,如果桌面满了,你得先拿走一本不常用的书(淘汰)。淘汰的策略是:优先扔掉最久没用的(LRU),但如果那本书被修改过还没存回书库(脏页),需要先"写回"(刷盘)才能丢。

技术映射:WiredTiger 的缓存淘汰机制:

  • Clean Page 淘汰:直接从内存中丢弃(无 IO)——因为数据在磁盘上已有最新副本。
  • Dirty Page 淘汰:必须先写入磁盘再丢弃(产生 IO)——因为内存中的版本比磁盘新。

小胖:那我看到的 10000 额外 IOPS 就是脏页淘汰产生的?

大师:对。当你的工作集大于 WiredTiger 缓存时,脏页淘汰成为主要磁盘 IO 来源——这叫工作集膨胀。监控的关键是 wiredTiger.cache.tracked dirty bytes in the cachepages evicted by application threads。如果 eviction rate 持续 > 0,说明缓存不够用。

小白:B-Tree 呢?MySQL 用 B+Tree,WiredTiger 也用 B-Tree,有什么不同?

大师:相同的都是"平衡多路查找树"——数据有序存储,按页组织,通过指针连接。WiredTiger 的 B-Tree 特色在于:

  • 页内记录(Row-store)和列式存储(Column-store) 两种格式——MongoDB 用 Row-store。
  • 页的大小默认为 4KB——比 InnoDB 的 16KB 小得多,适应更多小文档的场景。
  • 磁盘镜像(Disk Image)和内存镜像(In-Memory)不同——磁盘上页是压缩的,读到内存后才解压。所以 WiredTiger 的内存消耗 = 未压缩的页数量 × 4KB。

技术映射:WiredTiger 页 = B-Tree 的节点。每个页在磁盘上是压缩存储的,在内存中是未压缩的。缓存中同时存在 Clean(只读不写)和 Dirty(已修改未刷盘)两种页状态。

小胖:那 Checkpoint 是什么?跟 MySQL 的 checkpoint 一样吗?

大师:Checkpoint 是 WiredTiger 创建一致性快照的机制。每 60 秒(默认)或 Journal 文件达到 2GB 时触发一次。Checkpoint 做两件事——把所有脏页刷盘、更新元数据——使数据库进入一个新的一致性状态。崩溃恢复时,WiredTiger 从最近的 Checkpoint 开始,重放 Journal 日志恢复之后的操作。

技术映射:Checkpoint 是 WiredTiger 持久化的核心环节。Checkpoint 期间的 CPU 和磁盘 IO 会短时间内升高——这就是 MongoDB 每 60 秒会出现一个"性能毛刺"的根源。

大师(总结):记住四组概念——B-Tree 是数据组织方式,缓存淘汰是内存管理机制,Journal 是崩溃恢复保障,Checkpoint 是周期性持久化。四者构成 WiredTiger 的完整运转闭环。

3. 项目实战

3.1 环境准备

使用 Docker MongoDB 并挂载自定义配置以调优 WiredTiger 参数。

docker compose -f mongodb-lab/docker-compose.yml ps

3.2 分步实现

步骤一:查看 WiredTiger 核心指标

// wt-stats.js —— 提取 WiredTiger 关键运行指标
use admin
var s = db.serverStatus()

print("=== WiredTiger 存储引擎指标 ===")

// 1. 缓存
var cache = s.wiredTiger.cache
print("缓存:")
print("  配置最大: " + (cache["maximum bytes configured"] / 1024 / 1024 / 1024).toFixed(2) + " GB")
print("  当前使用: " + (cache["bytes currently in the cache"] / 1024 / 1024 / 1024).toFixed(2) + " GB")
print("  脏数据: " + ((cache["tracked dirty bytes in the cache"] || 0) / 1024 / 1024).toFixed(0) + " MB")
print("  使用率: " + (cache["bytes currently in the cache"] / cache["maximum bytes configured"] * 100).toFixed(1) + "%")

// 2. 页淘汰
print("\n页淘汰:")
print("  淘汰(应用线程): " + (cache["pages evicted by application threads"] || 0))
print("  淘汰(后台线程): " + (cache["pages evicted by eviction server"] || 0))
print("  因淘汰而刷脏: " + (cache["pages currently queued for eviction"] || 0))
// 如果 application thread eviction > 0,缓存压力大——应用线程被迫介入淘汰

// 3. Checkpoint
var wt = s.wiredTiger
print("\nCheckpoint:")
print("  耗时(ms): " + (wt["checkpoint"]["most recent time msecs"] || "N/A"))
print("  上次时间: " + (wt["checkpoint"]["last checkpoint"] || "N/A"))

// 4. 事务
print("\n事务:")
print("  事务冲突: " + (wt["transaction"]["transaction conflicts"] || 0))
print("  写冲突: " + (wt["transaction"]["write conflicts"] || 0))

// 5. 压缩
print("\n压缩统计:")
print("  页读入: " + (wt["block-manager"]["blocks read"] || 0))
print("  页写出: " + (wt["block-manager"]["blocks written"] || 0))
print("  预读: " + (wt["block-manager"]["blocks pre-loaded"] || 0))

步骤二:压测不同压缩算法

目标:对比 snappy、zlib、zstd 在不同场景下的表现。

// compression-benchmark.js —— 压缩算法对比测试

// 创建三个相同的集合,分别用不同压缩算法
// 注意:集合的压缩算法在创建时指定(通过 storageEngine 选项)

// 在 mongod 的配置文件中指定默认压缩:
// wiredTiger:
//   collectionConfig:
//     blockCompressor: snappy   # 或 zlib / zstd / none

// 或者在创建集合时指定:
db.createCollection("products_snappy", {
  storageEngine: { wiredTiger: { configString: "block_compressor=snappy" } }
})
db.createCollection("products_zstd", {
  storageEngine: { wiredTiger: { configString: "block_compressor=zstd" } }
})
db.createCollection("products_none", {
  storageEngine: { wiredTiger: { configString: "block_compressor=none" } }
})

// 插入相同的数据各 10 万条
var testDoc = { name: "压缩测试商品", description: "这是描述".repeat(50) }
for (var coll of ["products_snappy", "products_zstd", "products_none"]) {
  var docs = []
  for (var i = 0; i < 100000; i++) {
    docs.push(Object.assign({ _id: i, price: Math.random() * 1000 }, testDoc))
    if (docs.length === 5000) {
      db[coll].insertMany(docs, { ordered: false })
      docs = []
    }
  }
}

// 对比三个集合的存储大小
["products_snappy", "products_zstd", "products_none"].forEach(function(c) {
  var stats = db[c].stats()
  print(c + ": " + (stats.storageSize / 1024 / 1024).toFixed(1) + " MB (压缩" +
        (stats.storageSize / (stats.size || 1) * 100).toFixed(0) + "%)")
})

// 结果解读:
// snappy: 压缩快(CPU低)、压缩率中等 → 适合高吞吐低延迟场景
// zstd: 压缩率高(CPU高)、压缩慢 → 适合存储优先、读大于写的场景
// none: 无压缩、CPU零开销 → 适合纯内存工作集(数据远小于缓存大小)

步骤三:缓存大小调优实验

目标:观察不同 cacheSizeGB 下的淘汰行为。

# 在 mongod 启动参数中调整缓存大小
# docker run ... mongo:8.0 --wiredTigerCacheSizeGB 1.0
# 或修改 /etc/mongod.conf:
# storage:
#   wiredTiger:
#     engineConfig:
#       cacheSizeGB: 1.0

# 建议:缓存在物理内存的 50%-70% 之间
# 8GB 内存服务器:cacheSizeGB 设为 4-5.5
# 32GB 内存服务器:cacheSizeGB 设为 16-22
// 观察缓存压力指标是否改善
var before = db.serverStatus().wiredTiger.cache
print("调整前 eviction:", before["pages evicted by application threads"])
print("调整前 dirty:", (before["tracked dirty bytes in the cache"] || 0) / 1024 / 1024, "MB")

// 调大 cacheSizeGB 后重新运行观察
// 期望:application thread eviction 降为 0,dirty bytes 比例降低

步骤四:Checkpoint 行为观察与调优

// checkpoint-monitor.js —— 观察 Checkpoint 的频率和影响

// 查看 Checkpoint 配置
var params = db.adminCommand({ getParameter: "*" })
print("Checkpoint 间隔(秒):", params["wiredTigerEngineRuntimeConfig"].match(/checkpoint=\(wait=(\d+)/)?.[1] || "默认60")
// checkpoint=(wait=60,log_size=2GB) → 每 60 秒 或 Journal 达 2GB 时触发

// 调优建议:
// 批量写入场景:增大 log_size 以减少 checkpoint 频率
// 低延迟场景:减小 wait 以提高 checkpoint 频率(减少单次脏页堆积量)
// 示例配置(在 mongod.conf 中):
// storage:
//   wiredTiger:
//     engineConfig:
//       journalCompressor: snappy
//     checkpoint:
//       wait: 300           # 5 分钟
//       logSize: 4          # 4GB
// 注意:checkpoint 频率越低,崩溃恢复时需要重放的 Journal 越多,恢复时间越长

步骤五:Journal(WAL)机制理解

// journal-explained.js —— Journal 是 WiredTiger 的预写日志

// Journal 工作原理:
// 1. 写操作到达 → 先写入 Journal(顺序写,快)
// 2. 写入内存中的 B-Tree 页(标记为 Dirty)
// 3. Checkpoint 周期性地将 Dirty 页刷入磁盘的数据文件
// 4. 崩溃恢复时 → 读取最近的 Checkpoint + 重放 Journal = 恢复到崩溃前状态

// 查看 Journal 统计
var journal = db.serverStatus().wiredTiger.log
print("Journal 写入(字节):", journal["total log buffer size bytes prepared"] || "N/A")
print("Journal 同步:", journal["log sync operations"] || "N/A")
// log sync operations 是 Journal 写入磁盘的次数
// 频繁的 sync 意味着 writeConcern: j=true 被大量使用

// Journal 性能影响:
// - MongoDB 默认每 100ms 自动 sync 一次 Journal
// - j:true 会在每次写入时立即 sync(强制刷新到磁盘)
// - 对延迟敏感的写入接口避免使用 j:true,除非需要绝对数据安全

步骤六:源码追踪——WiredTiger 缓存淘汰路径

// 源码关键路径(在 GDB 中追踪)
// 文件:src/mongo/db/storage/wiredtiger/wiredtiger_record_store.cpp
// 函数调用链:
// WiredTigerRecordStore::insertRecord()
//   → WT_SESSION::insert()       // 调用 WT 底层 API
//     → __wt_btree_insert()      // B-Tree 插入
//       → __wt_cache_eviction()  // 如果需要腾空间 → 淘汰

// 在 GDB 中打断点:
// (gdb) break __wt_cache_eviction
// 观察淘汰被触发的时机和淘汰的页面数量

3.3 完整代码清单

文件 用途
mongodb-lab/scripts/ch33-wt-stats.js WiredTiger 指标提取
mongodb-lab/scripts/ch33-compression-bench.js 压缩算法对比
mongodb-lab/scripts/ch33-cache-tuning.js 缓存调优实验
mongodb-lab/scripts/ch33-checkpoint-monitor.js Checkpoint 观察
mongodb-lab/config/mongod-wt-tune.conf WiredTiger 调优配置模板

3.4 测试验证

use admin

// 1. 缓存指标可读
var cache = db.serverStatus().wiredTiger.cache
print("缓存最大:", cache["maximum bytes configured"] ? "PASS" : "FAIL")
print("淘汰数:", cache["pages evicted"] ? "PASS" : "FAIL")

// 2. 压缩算法对比有效
var colls = ["products_snappy", "products_zstd", "products_none"]
colls.forEach(function(c) {
  var s = db[c].stats()
  print(c + " 存储:", s.storageSize, "bytes", s.storageSize > 0 ? "PASS" : "FAIL")
})

// 3. Checkpoint 信息可查
var wt = db.serverStatus().wiredTiger
print("Checkpoint:", wt.checkpoint ? "PASS" : "FAIL")

print("\n=== WiredTiger 验证完成 ===")

4. 项目总结

4.1 WiredTiger 核心概念速查

概念 作用 关键参数/指标
B-Tree 页 数据存储的最小单元 默认 4KB/页,内存中未压缩
缓存(Cache) 工作内存中缓存的 B-Tree 页 cacheSizeGB(默认 = RAM×50%-1GB)
脏页淘汰 腾出内存给新数据 pages evicted by application threads
Checkpoint 周期性持久化一致性快照 默认 60s 或 Journal 2GB 触发
Journal(WAL) 崩溃恢复的预写日志 默认 100ms sync 一次
压缩算法 磁盘数据压缩 snappy(快)/ zstd(高压缩率)/ none

4.2 适用场景

WiredTiger 调优适用

  1. 写入密集型应用——cacheSizeGB 和工作集匹配度是关键。
  2. 存储成本敏感——用 zstd 压缩最大化磁盘利用率。
  3. 延迟敏感——调小 checkpoint 间隔避免毛刺,用 snappy 降低 CPU。
  4. 崩溃恢复时间敏感——调小 checkpoint 间隔减少 Journal 重放量。

4.3 注意事项

注意事项 说明
cacheSizeGB 不能等于物理内存 留至少 1-2GB 给操作系统和文件系统缓存
压缩算法创建后不可更改 只能在新建集合时指定,或通过 reshard 重建
Checkpoint 期间性能毛刺 大量脏页刷盘的 IO 高峰,是正常的
eviction > 0 不一定坏事 后台 eviction server 正常淘汰 < 应用线程被迫淘汰才是问题

4.4 常见踩坑经验

故障案例一:cacheSizeGB 设太小导致写入卡死

某团队在 64GB 服务器上设了 cacheSizeGB: 2,写入高峰期 95% 的请求在等待 eviction 完成——应用线程被迫停在 __wt_cache_eviction 中等待脏页刷盘,P99 延迟 30 秒。解决cacheSizeGB 调到 40GB(内存的 60%),eviction 几乎消失。

故障案例二:zlib 压缩导致 CPU 爆满

某大数据团队对所有集合用了 block_compressor=zlib,日均写入 5 亿条日志时 CPU 100%,数据压缩率确实高(30%)但写入吞吐下降 70%。解决:日志集合换用 snappy(CPU 省 50%,压缩率仍有 20%),核心交易集合用 zstd。

故障案例三:透明大页未禁用导致内存碎片

Linux 默认开启透明大页(THP),WiredTiger 的 4KB 页与 2MB 大页冲突——内存碎片严重、bytes currently in the cache 远小于 cacheSizeGB 但 eviction 已发生。解决echo never > /sys/kernel/mm/transparent_hugepage/enabled,MongoDB 官方强烈建议禁用 THP。

4.5 思考题

  1. 如果 WiredTiger 缓存的 80% 都是脏页,会有什么后果?为什么 MongoDB 默认把脏页比例控制在 20% 以内?
  2. writeConcern: {j:true} 在 WiredTiger 层面实际做了什么操作?为什么 j:true 比 w:1 慢 10-100 倍?

(答案将在第 34 章末尾揭晓)

上一章思考题答案

  1. GDB 中打印 std::vector<BSONObj>:用 p vec.size() 看大小,p vec[0] 看第一个元素。打印前 5 个用循环或 GDB Python 脚本。大 vector 可用 p vec._M_impl._M_start@5 打印前 5 个元素的原始指针。

  2. MONGO_COMPILER_NOINLINE 等宏用于精细控制编译器的内联和优化行为——在性能关键路径上,过度内联会导致代码膨胀和指令缓存失效,NOINLINE 强制函数保持独立;在调试模式下,被内联的函数无法在 GDB 中打断点,所以调试版本用 --opt=off 禁用内联。

延伸阅读与资源

NumPy 从入门到生产落地:全链路实战指南(科学计算/向量化)
Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统
Redis 实战修炼与原理进阶
Python 3实战精进:从脚本到高并发订单引擎
MongoDB 实战进阶与内核修炼
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经
Milvus向量数据库实战修炼:从 0 到 1精通向量检索与生产落地
后端工程师的 AI 转型第一课:Ollama 与私有化大模型实战
10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用
后端工程师转型AI第一课-Ollama 与私有化大模型实战
大型语言模型(LLM) vLLM 高性能推理落地实战
Agent开发之LlamaIndex 实战修炼与源码进阶
大语言模型Transformers 实战修炼与源码剖析

posted on 2026-08-18 15:36  一天不进步,就是退步  阅读(5)  评论(0)    收藏  举报