3. 容量增长来源分析

在讨论 MongoDB 的容量治理之前,必须先回答一个基础问题:磁盘上多出来的容量,究竟是谁占的?很多团队看到磁盘告警时,第一反应是"业务数据涨了",但真实情况往往更复杂。一个集合的物理占用,通常由四部分叠加而成:文档本身、索引、碎片空洞,以及副本集/分片集群带来的结构性放大。只有把这四类来源拆开量化,后续的清理、压缩、分片或扩容决策才有依据。

一、文档增长:最直观但并非唯一来源

文档增长是最容易理解的来源。假设一个订单集合 orders,单文档平均 1.2 KB,日增 200 万条,那么每天新增的原始文档数据约为 2.4 GB。但要注意,MongoDB 的 BSON 文档在磁盘上并不是"1.2 KB 就是 1.2 KB":字段名会随每条文档重复存储,嵌套数组会带来额外开销,长字符串、二进制字段(如 Base64 图片)会显著抬高单文档体积。

可以用 db.orders.stats() 观察几个关键字段:size 表示逻辑文档总字节数,avgObjSize 表示平均文档大小,storageSize 表示实际分配的存储空间。若 storageSize 明显大于 size,说明存在压缩差异或碎片;若 avgObjSize 持续上升,则要警惕大字段(如日志、快照、富文本)被写入主集合。

二、索引膨胀:容易被低估的"隐形容量"

索引是容量增长中最容易被忽视的部分。每个索引都是一棵独立的 B-tree,需要单独占用磁盘。索引大小与索引键长度、文档数量、键的基数直接相关。例如在 orders 上建一个 { customerId: 1, createdAt: -1 } 的复合索引,若 customerId 是 24 位 ObjectId 字符串、createdAt 是 8 字节日期,单条索引项约 40~60 字节,加上 B-tree 内部节点和填充因子,2000 万文档的该索引可能占用 1.5~2.5 GB。

更危险的是"索引数量失控":一个集合上挂了 8~10 个索引,索引总大小超过文档本身并不罕见。可以用下面的命令快速盘点:

db.orders.aggregate([{ $indexStats: {} }])
db.orders.stats().indexSizes

indexSizes 会列出每个索引的字节数,$indexStats 还能看到每个索引的访问次数(accesses.ops)。如果某个索引长期 ops 为 0,它就是纯粹的容量负担,应优先评估删除。

三、碎片化:删除与更新留下的"空洞"

MongoDB 的 WiredTiger 存储引擎以页为单位管理数据。当文档被删除或更新变大时,页内会出现未使用空间;当文档更新变小,原位置可能留下空洞。这些空洞不会自动归还操作系统,而是留在数据文件中,形成碎片化。典型表现是:storageSize 远大于 size,且 db.collection.stats().wiredTiger["block-manager"]["file bytes available for reuse"] 数值较大。

碎片化的常见诱因包括:高频删除历史数据、文档字段频繁增删、大量更新导致文档迁移。它不会让逻辑数据变多,但会让物理文件持续膨胀。治理手段包括 compact 命令、重建集合(dump/restore 或 $out 重写)、以及调整文档模型减少原地更新。

四、副本集与分片集群的容量放大效应

这是最容易被低估的一层。副本集中,每个从节点都保存一份完整数据。一个 3 节点副本集,若主节点数据为 500 GB,则集群总磁盘需求约为 1.5 TB(不含 oplog)。oplog 本身也是容量来源:它按时间窗口滚动,写入量越大、窗口越长,oplog 占用越高。可以用 db.getReplicationInfo() 查看 oplog 大小与时间跨度。

分片集群的放大更复杂。每个分片通常自身又是一个副本集,因此"分片数 × 副本因子"会成倍放大物理容量。此外,分片集群还需要 config server 副本集、mongos 路由层(通常不持久化业务数据,但需要元数据),以及跨分片查询时的临时排序/合并开销。若分片键选择不当,还可能出现数据倾斜:某个分片承载了 60% 的数据,导致整体扩容无法线性分摊。

MongoDB 物理容量构成与放大链路 逻辑数据 → 索引 → 碎片 → 副本/分片放大 文档数据 size / avgObjSize BSON 字段名重复 大字段 / 数组开销 索引膨胀 indexSizes 逐项统计 键长 × 文档数 × 基数 冗余索引 / 低命中索引 碎片化空洞 storageSize > size 删除 / 更新留下空洞 compact / 重写集合 集群放大 副本因子 分片数 oplog 容量叠加示意(以 3 节点副本集、2 分片为例) 文档 500G 索引 180G 碎片 70G oplog 40G 单分片单副本 ≈ 790G 放大计算: 单分片 3 副本 = 790G × 3 ≈ 2.37T 2 分片集群 = 2.37T × 2 ≈ 4.74T(不含 config server) 若分片倾斜 60/40,热点分片实际承载 ≈ 2.84T 关键判断指标 • storageSize / size 比值 > 1.5 → 碎片 • indexSizes 总和 > size → 索引过重 • 分片间数据量偏差 > 20% → 倾斜

五、把四类来源落到一次容量盘点中

实际治理时,建议按下面的顺序做一次盘点,而不是直接扩容:

  1. 先看逻辑数据:用 db.stats() 和 db.collection.stats() 拿到 dataSize、storageSize、indexSize,算出"文档 : 索引 : 碎片"的大致比例。
  2. 再查索引明细:用 indexSizes 排序,找出最大的 3~5 个索引;结合 $indexStats 的 accesses.ops 判断是否可删。
  3. 评估碎片:对比 storageSize 与 size,若比值超过 1.5 且近期有大量删除,考虑在业务低峰执行 compact 或重建集合。
  4. 最后算集群放大:用"单分片单副本容量 × 副本因子 × 分片数"估算总需求,并检查分片间数据分布是否均衡。
一个实用的经验公式:集群总磁盘需求 ≈ 逻辑数据 × (1 + 索引比 + 碎片比) × 副本因子 × 分片数 × 1.2(安全余量)。其中索引比通常为 0.3~0.8,碎片比在健康状态下应低于 0.2。

六、常见误区与判断依据

第一个误区是"删了数据磁盘就会降"。MongoDB 删除文档后,空间通常不会立即归还操作系统,而是被标记为可复用。第二个误区是"索引越多查询越快"。索引会拖慢写入、占用内存和磁盘,必须用访问统计来验证价值。第三个误区是"分片一定能线性扩容"。如果分片键基数低或单调递增,数据会集中到少数分片,扩容效果大打折扣。

判断容量增长是否健康,可以盯住三个信号:storageSize / size 是否持续走高、indexSize / dataSize 是否超过 1、分片间 dataSize 偏差是否超过 20%。一旦越界,就应回到本文的四类来源逐一排查,而不是盲目加盘。