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 cache 和 pages 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 调优适用:
- 写入密集型应用——cacheSizeGB 和工作集匹配度是关键。
- 存储成本敏感——用 zstd 压缩最大化磁盘利用率。
- 延迟敏感——调小 checkpoint 间隔避免毛刺,用 snappy 降低 CPU。
- 崩溃恢复时间敏感——调小 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 思考题
- 如果 WiredTiger 缓存的 80% 都是脏页,会有什么后果?为什么 MongoDB 默认把脏页比例控制在 20% 以内?
writeConcern: {j:true}在 WiredTiger 层面实际做了什么操作?为什么 j:true 比 w:1 慢 10-100 倍?
(答案将在第 34 章末尾揭晓)
上一章思考题答案:
GDB 中打印
std::vector<BSONObj>:用p vec.size()看大小,p vec[0]看第一个元素。打印前 5 个用循环或 GDB Python 脚本。大 vector 可用p vec._M_impl._M_start@5打印前 5 个元素的原始指针。
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 实战修炼与源码剖析

微信公众号: 架构师日常笔记 欢迎关注!
浙公网安备 33010602011771号