第33章:MongoDB WiredTiger 存储引擎——从 B-Tree 到缓存淘汰

发布时间:2026/7/24 9:32:07

第33章:MongoDB WiredTiger 存储引擎——从 B-Tree 到缓存淘汰 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 用 BTreeWiredTiger 也用 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 参数。dockercompose-fmongodb-lab/docker-compose.ymlps3.2 分步实现步骤一查看 WiredTiger 核心指标// wt-stats.js —— 提取 WiredTiger 关键运行指标use adminvarsdb.serverStatus()print( WiredTiger 存储引擎指标 )// 1. 缓存varcaches.wiredTiger.cacheprint(缓存:)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. Checkpointvarwts.wiredTigerprint(\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_compressorsnappy}}})db.createCollection(products_zstd,{storageEngine:{wiredTiger:{configString:block_compressorzstd}}})db.createCollection(products_none,{storageEngine:{wiredTiger:{configString:block_compressornone}}})// 插入相同的数据各 10 万条vartestDoc{name:压缩测试商品,description:这是描述.repeat(50)}for(varcollof[products_snappy,products_zstd,products_none]){vardocs[]for(vari0;i100000;i){docs.push(Object.assign({_id:i,price:Math.random()*1000},testDoc))if(docs.length5000){db[coll].insertMany(docs,{ordered:false})docs[]}}}// 对比三个集合的存储大小[products_snappy,products_zstd,products_none].forEach(function(c){varstatsdb[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// 观察缓存压力指标是否改善varbeforedb.serverStatus().wiredTiger.cacheprint(调整前 eviction:,before[pages evicted by application threads])print(调整前 dirty:,(before[tracked dirty bytes in the cache]||0)/1024/1024,MB)// 调大 cacheSizeGB 后重新运行观察// 期望application thread eviction 降为 0dirty bytes 比例降低步骤四Checkpoint 行为观察与调优// checkpoint-monitor.js —— 观察 Checkpoint 的频率和影响// 查看 Checkpoint 配置varparamsdb.adminCommand({getParameter:*})print(Checkpoint 间隔(秒):,params[wiredTigerEngineRuntimeConfig].match(/checkpoint\(wait(\d)/)?.[1]||默认60)// checkpoint(wait60,log_size2GB) → 每 60 秒 或 Journal 达 2GB 时触发// 调优建议// 批量写入场景增大 log_size 以减少 checkpoint 频率// 低延迟场景减小 wait 以提高 checkpoint 频率减少单次脏页堆积量// 示例配置在 mongod.conf 中// storage:// wiredTiger:// engineConfig:// journalCompressor: snappy// checkpoint:// wait: 300 # 5 分钟// logSize: 4 # 4GB// 注意checkpoint 频率越低崩溃恢复时需要重放的 Journal 越多恢复时间越长步骤五JournalWAL机制理解// journal-explained.js —— Journal 是 WiredTiger 的预写日志// Journal 工作原理// 1. 写操作到达 → 先写入 Journal顺序写快// 2. 写入内存中的 B-Tree 页标记为 Dirty// 3. Checkpoint 周期性地将 Dirty 页刷入磁盘的数据文件// 4. 崩溃恢复时 → 读取最近的 Checkpoint 重放 Journal 恢复到崩溃前状态// 查看 Journal 统计varjournaldb.serverStatus().wiredTiger.logprint(Journal 写入(字节):,journal[total log buffer size bytes prepared]||N/A)print(Journal 同步:,journal[log sync operations]||N/A)// log sync operations 是 Journal 写入磁盘的次数// 频繁的 sync 意味着 writeConcern: jtrue 被大量使用// 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.jsWiredTiger 指标提取mongodb-lab/scripts/ch33-compression-bench.js压缩算法对比mongodb-lab/scripts/ch33-cache-tuning.js缓存调优实验mongodb-lab/scripts/ch33-checkpoint-monitor.jsCheckpoint 观察mongodb-lab/config/mongod-wt-tune.confWiredTiger 调优配置模板3.4 测试验证use admin// 1. 缓存指标可读varcachedb.serverStatus().wiredTiger.cacheprint(缓存最大:,cache[maximum bytes configured]?PASS:FAIL)print(淘汰数:,cache[pages evicted]?PASS:FAIL)// 2. 压缩算法对比有效varcolls[products_snappy,products_zstd,products_none]colls.forEach(function(c){varsdb[c].stats()print(c 存储:,s.storageSize,bytes,s.storageSize0?PASS:FAIL)})// 3. Checkpoint 信息可查varwtdb.serverStatus().wiredTigerprint(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 threadsCheckpoint周期性持久化一致性快照默认 60s 或 Journal 2GB 触发JournalWAL崩溃恢复的预写日志默认 100ms sync 一次压缩算法磁盘数据压缩snappy快/ zstd高压缩率/ none4.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_compressorzlib日均写入 5 亿条日志时 CPU 100%数据压缩率确实高30%但写入吞吐下降 70%。解决日志集合换用 snappyCPU 省 50%压缩率仍有 20%核心交易集合用 zstd。故障案例三透明大页未禁用导致内存碎片Linux 默认开启透明大页THPWiredTiger 的 4KB 页与 2MB 大页冲突——内存碎片严重、bytes currently in the cache远小于 cacheSizeGB 但 eviction 已发生。解决echo never /sys/kernel/mm/transparent_hugepage/enabledMongoDB 官方强烈建议禁用 THP。4.5 思考题如果 WiredTiger 缓存的 80% 都是脏页会有什么后果为什么 MongoDB 默认把脏页比例控制在 20% 以内writeConcern: {j:true}在 WiredTiger 层面实际做了什么操作为什么 j:true 比 w:1 慢 10-100 倍答案将在第 34 章末尾揭晓上一章思考题答案GDB 中打印std::vectorBSONObj用p vec.size()看大小p vec[0]看第一个元素。打印前 5 个用循环或 GDB Python 脚本。大 vector 可用p vec._M_impl._M_start5打印前 5 个元素的原始指针。MONGO_COMPILER_NOINLINE等宏用于精细控制编译器的内联和优化行为——在性能关键路径上过度内联会导致代码膨胀和指令缓存失效NOINLINE强制函数保持独立在调试模式下被内联的函数无法在 GDB 中打断点所以调试版本用--optoff禁用内联。延伸阅读与资源MongoDB 实战进阶与内核修炼python入门Rquests从菜鸟脚本到企业级SDK的网络实战圣经Milvus向量数据库实战修炼从 0 到 1精通向量检索与生产落地后端工程师的 AI 转型第一课Ollama 与私有化大模型实战10倍开发者的 Dify 魔法书从零构建全栈 AI 应用后端工程师转型AI第一课-Ollama 与私有化大模型实战大型语言模型(LLM) vLLM 高性能推理落地实战Agent开发之LlamaIndex 实战修炼与源码进阶大语言模型Transformers 实战修炼与源码剖析

相关新闻