2. MongoDB存储引擎与容量构成

要治理 MongoDB 的容量增长,第一步不是删数据,而是搞清楚“磁盘上的字节到底花在了哪里”。MongoDB 默认使用 WiredTiger 存储引擎,它把数据组织成一套分层结构,磁盘占用由数据文件、索引文件、日志文件和元数据共同构成。理解这套模型,才能判断容量增长是正常业务写入、索引膨胀,还是日志与碎片在悄悄吃掉空间。

WiredTiger 的存储模型:从文档到磁盘块

WiredTiger 采用 B 树组织数据,但它的磁盘布局与内存视图并不一一对应。逻辑上,一个集合(collection)对应一个或多个数据文件;物理上,数据被切分为若干 extent(区段),每个 extent 内部再划分为固定大小的页(page)。默认情况下,WiredTiger 的页大小为 4KB,但压缩后实际写入磁盘的块可能更小。文档在内存中以 BSON 形式存在,写入磁盘时会被序列化并压缩,因此“文档大小”与“磁盘占用”之间隔着压缩率、页填充率和碎片三层变量。

WiredTiger 还维护一个重要的概念:checkpoint(检查点)。每隔约 60 秒或写入 2GB 日志后,WiredTiger 会把内存中的脏页刷盘,形成一个一致的检查点。检查点对应的数据文件是“稳定”的,而两次检查点之间的新写入先进入日志(journal),再在后续检查点中合并进数据文件。这意味着磁盘上同时存在:已合并的数据文件、尚未合并的日志文件,以及可能尚未回收的旧检查点数据。

WiredTiger 磁盘占用构成与写入路径 内存层(WiredTiger Cache) BSON 文档 未压缩 · 可变大小 脏页(Dirty Page) 4KB 页 · 待刷盘 索引节点 B 树 · 内存映射 Journal Buffer 128KB 缓冲 磁盘层(数据目录 /data/db) 集合数据文件 collection-*.wt extent → 4KB page Snappy/zstd 压缩 含碎片与未回收空间 索引文件 index-*.wt B 树键值对 前缀压缩 每个索引独立文件 Journal 日志 journal/WiredTigerLog.* 预写日志(WAL) 默认 100MB/文件 检查点后回收 元数据与诊断文件 WiredTiger.wt · WiredTiger.turtle · diagnostic.data · _mdb_catalog.wt 磁盘占用 = 数据文件 + 索引文件 + Journal + 元数据 + 未回收碎片

压缩机制:为什么磁盘占用不等于文档大小

WiredTiger 支持三种压缩层级:集合级压缩、索引级压缩和日志压缩。集合默认使用 Snappy 块压缩,索引默认使用前缀压缩,日志默认不压缩(但可通过 storage.wiredTiger.engineConfig.journalCompressor 改为 zlib 或 zstd)。块压缩以页为单位进行,压缩后的块大小通常为原大小的 30%~70%,具体取决于数据特征。

压缩率不是越高越好。zstd 压缩率高于 Snappy,但 CPU 开销更大;如果业务写入密集且 CPU 是瓶颈,盲目切换 zstd 可能拖慢写入。一个实用的判断方法是:先用 db.collection.stats() 查看 storageSize 与 size 的比值,若比值长期高于 0.7,说明压缩效果有限,可考虑调整压缩算法或检查数据是否已高度随机化(如加密字段、UUID 主键)。

// 查看集合压缩与空间统计(示例输出)
db.orders.stats()
// size: 2.4 GB          —— 未压缩逻辑大小
// storageSize: 1.1 GB   —— 压缩后磁盘占用
// compressionRatio: 2.18
// totalIndexSize: 680 MB
// avgObjSize: 512 B

上例中,数据文件压缩后约 1.1GB,索引约 680MB,索引占数据文件的比例超过 60%。这说明索引是容量治理中不可忽视的部分。每个索引都是一棵独立的 B 树,索引键越长、基数越高,索引文件越大。复合索引的字段顺序、是否包含冗余字段,都会直接影响索引体积。

索引与日志对磁盘占用的实际影响

索引对磁盘的占用可以粗略估算:索引大小 ≈ 文档数 × 索引键平均长度 × 填充因子。WiredTiger 的 B 树页默认填充率约为 50%~80%,这意味着索引文件通常比“纯键值数据”大 1.3~2 倍。一个 1000 万文档的集合,若每个文档有一个 12 字节的 ObjectId 主键索引和一个 40 字节的复合索引,仅索引就可能占用 1GB 以上。

日志文件的影响则与写入模式相关。MongoDB 默认每 100MB 生成一个 journal 文件,检查点完成后旧日志可被回收。但在持续高写入场景下,journal 目录可能同时存在多个 100MB 文件,瞬时占用可达数百 MB 到数 GB。如果发现 journal 目录持续增长且不回收,通常意味着检查点被阻塞,常见原因是磁盘 I/O 饱和或大量写操作导致脏页积压。

快速定位容量构成:在 mongosh 中执行 db.stats(1024*1024) 查看数据库级 dataSize、storageSize、indexSize;再对每个集合执行 db.collection.stats(),按 storageSize 降序排列,即可找出占用最大的集合。journal 目录大小可直接用 du -sh /data/db/journal 观察。

碎片与空间回收:被忽视的容量黑洞

WiredTiger 删除文档后,空间不会立即归还操作系统,而是标记为可复用。频繁的插入、删除、更新会导致数据文件和索引文件内部出现碎片,表现为 storageSize 远大于实际有效数据。此时即使删除了大量数据,磁盘占用也可能纹丝不动。

判断碎片程度的实用指标是 db.collection.stats().wiredTiger["block-manager"]["file bytes available for reuse"]。如果该值占 storageSize 的比例超过 20%,说明碎片已经比较明显。回收方式有两种:一是对集合执行 compact 命令,二是通过滚动重建(将数据迁移到新集合再 rename)。compact 会阻塞该集合的读写,必须在维护窗口执行;滚动重建对业务影响更小,但需要额外磁盘空间。

容量构成项 典型占比(示例) 主要影响因素 治理手段
集合数据文件 45%~60% 文档大小、压缩算法、碎片率 调整压缩、compact、TTL 清理
索引文件 25%~40% 索引数量、键长度、填充因子 删除冗余索引、优化复合索引顺序
Journal 日志 5%~15% 写入速率、检查点频率、磁盘 I/O 调大检查点间隔、提升磁盘吞吐
元数据与诊断 <2% 集合/索引数量、诊断数据保留 控制集合数量、清理诊断文件

把上述构成放在一起看,容量治理的顺序就清晰了:先确认数据文件与索引的占比,再判断压缩是否合理,最后检查 journal 与碎片。只有知道每一类占用“应该有多大”,才能判断当前磁盘占用是否异常,进而决定是优化索引、调整压缩,还是执行空间回收。