管理和减少MongoDB磁盘空间使用的最佳实践

通过这份全面的最佳实践指南,优化您的MongoDB磁盘使用。学习压缩集合和索引、识别并删除不必要的索引、利用WiredTiger压缩功能的有效策略。了解如何实施数据归档、管理oplog大小,并主动监控磁盘空间以防止系统中断并提高性能。本文提供了可操作的见解和实际示例,帮助您的MongoDB部署保持精简高效。

管理和减少MongoDB磁盘空间使用的最佳实践

MongoDB磁盘使用问题通常在最糟糕的时刻变得紧迫:批处理作业运行时间超出预期,删除操作似乎没有释放空间,或者副本集成员开始警告卷几乎已满。修复方法很少是一个神奇的命令。您需要知道空间是实时数据、索引、可重用的WiredTiger空间、oplog、日志还是本地备份。

最安全的方法是先测量,减少不再需要存在的内容,然后才运行更重的维护操作,如压缩或成员重建。这个顺序可以防止您创建一个长时间维护事件却只回收很少空间的情况。

理解MongoDB磁盘空间消耗

MongoDB将磁盘空间用于以下几个组件:

  • 数据文件:存储集合中的实际BSON文档。
  • 索引文件:存储为支持高效查询执行而创建的B树索引。
  • 日志文件(WiredTiger):在写入操作应用到数据文件之前记录它们,确保数据持久性。这些是预分配的。
  • Oplog(操作日志):副本集中一个特殊的固定集合,记录所有写入操作。对复制至关重要。
  • 诊断数据:日志、mongod进程文件和其他系统相关信息。

随着时间的推移,由于更新、删除和文档增长(填充),集合和索引可能会变得碎片化或包含未使用的已分配空间,导致磁盘使用效率低下。即使数据库不再需要这些“空白空间”用于实时数据,操作系统也不会立即回收它们。

减少MongoDB磁盘空间的策略

1. 压缩集合和索引

压缩操作通过更有效地重写数据和索引文件来帮助回收未使用的磁盘空间。这在大量数据删除或更新后特别有用。

压缩集合

使用WiredTiger存储引擎(自MongoDB 3.2起默认),compact主要回收已删除文档的可用空间并整理集合碎片。它不会像MMAPv1的compact操作那样从头重建集合的数据文件。

db.runCommand({ compact: "myCollection" })

compact的注意事项:

  • compact操作可能消耗大量资源(CPU、I/O)并且需要很长时间,特别是对于大型集合。通常最好在维护窗口期间或在副本集的辅助成员上运行。
  • 磁盘要求和锁定行为因MongoDB版本、存储引擎和部署形态而异。在大型生产集合上运行之前,请查看您确切版本的文档。
  • 对于分片集群,在每个分片上独立运行compact

重建索引

索引也可能变得碎片化。重建索引可以回收空间并可能提高查询性能。

db.myCollection.reIndex()

reIndex()的注意事项:

  • reIndex()的行为在不同MongoDB版本中有所变化,并且在繁忙系统上仍然可能具有破坏性。请查看您版本的文档,在测试环境中测试,并尽可能通过副本集成员进行滚动操作。
  • compact类似,reIndex()在操作期间需要额外的磁盘空间。

repairDatabase(离线操作)

对于严重的碎片化或数据损坏,repairDatabase可以重建所有数据文件。这是一个离线操作,需要停止mongod实例。

mongod --repair

警告repairDatabase应作为空间回收的最后手段,因为如果处理不当,它可能是一个破坏性操作,并且可能需要很长时间。始终要有备份。

2. 优化索引

索引对性能至关重要,但会消耗大量磁盘空间。未使用或冗余的索引纯粹是开销。

识别并删除不必要的索引

定期审查您的索引,确保它们仍然需要。

  1. 列出集合的所有索引:
    
    

db.myCollection.getIndexes() ``` 2. 监控索引使用情况: 使用$indexStats、查询计划、性能分析器和您的应用程序工作负载历史。集合统计显示索引大小,但它们不能证明索引是否有用。 3. 识别重复或冗余的索引: 例如,在{ a: 1, b: 1 }上的索引使得在{ a: 1 }上的索引对于可以使用复合索引的查询是冗余的。在{ a: 1, b: 1 }上的索引也被在{ a: 1, b: 1, c: 1 }上的索引覆盖,用于仅涉及ab的查询。

一旦识别,删除未使用的索引:

db.myCollection.dropIndex("indexName")

提示:在应用到生产环境之前,始终在测试环境中测试删除索引的影响。

使用部分索引

部分索引仅索引集合中满足指定过滤表达式的文档。这减少了索引的文档数量,节省了磁盘空间并提高了写入性能。

db.orders.createIndex(
   { customerId: 1, orderDate: -1 },
   { partialFilterExpression: { status: "active" } }
)

此索引将仅包含status为"active"的文档,如果大多数订单是历史的、取消的、归档的或不在热路径中,则减小其大小。重要的不是单词"active";而是索引您的应用程序每天实际查询的子集的习惯。

从磁盘空间分类开始,而不是清理命令

当MongoDB磁盘空间增长时,第一个错误是直接跳到compactrepair或删除旧数据。这些操作可能有帮助,但它们也可能产生负载,在某些情况下锁定,或者隐藏几周的真实问题。首先回答三个问题:

  • 哪个文件系统正在填满:数据库路径、日志路径、日志路径还是备份卷?
  • 是实时数据在增长,还是在删除和更新后已分配但未使用的空间在增长?
  • 增长来自集合、索引、oplog、日志、诊断数据还是快照?

快速的第一遍通常如下所示:

df -h
du -h --max-depth=1 /var/lib/mongodb | sort -h
du -h --max-depth=1 /var/log/mongodb | sort -h

然后从shell内部检查MongoDB:

db.adminCommand({ listDatabases: 1 })
db.getSiblingDB("app").stats()
db.getSiblingDB("app").orders.stats()

storageSizetotalIndexSizedataSize讲述不同的故事。如果dataSize在增长,您可能有一个数据生命周期问题。如果storageSize远大于dataSize,您可能正在查看删除后可重用的内部空间。如果totalIndexSize相对于dataSize很大,那么在您触及压缩之前,索引设计值得关注。

理解MongoDB可以回收和不能回收什么

使用WiredTiger,删除文档通常使空间可供MongoDB重用。它并不总是立即将空间返回给操作系统。这种行为在紧急清理期间会让人惊讶:他们删除了一大批,运行df -h,几乎看不到改善。

这并不意味着删除失败。这意味着MongoDB通常可以重用该空间用于未来的插入和更新。如果目标是停止增长,删除或归档旧数据可能就足够了。如果目标是缩小文件系统,因为卷几乎已满或主机正在缩小,您可能需要压缩、重新同步副本集成员或进行转储和恢复式的重建。

对于生产系统,我通常将工作分为两个轨道。第一个轨道是即时安全:添加磁盘、移除明显的日志积累、暂停有风险的批处理作业或将备份移出数据库卷。第二个轨道是真正的减少:修复保留策略、移除未使用的索引,并且仅在知道字节去向之后才重建存储。

在整理碎片之前先修复数据保留策略

如果您的应用程序永远保留请求日志、事件、会话、通知、作业记录或分析文档,无论您如何仔细压缩,磁盘使用都会回来。MongoDB为您提供了几个实用的选项。

对于基于简单时间戳过期的数据,TTL索引通常是最清晰的答案:

db.sessions.createIndex(
  { expiresAt: 1 },
  { expireAfterSeconds: 0 }
)

该索引在expiresAt中存储的日期之后删除文档。它对于会话、临时令牌、短期导入作业或缓存的API响应很有用。它不能替代业务保留规则。TTL监视器在后台运行,所以不要期望秒级删除,也不要在需要审批工作流才能删除的数据上使用TTL。

对于业务记录,归档而不是盲目删除。一个常见的模式是:

  1. 将早于保留窗口的文档复制到更便宜的存储或归档数据库。
  2. 验证计数和重要字段的样本。
  3. 从主集合中以小批量删除。
  4. 在作业运行时监控复制延迟和磁盘指标。

小批量很重要。一个巨大的删除可能会造成复制压力,填满日志,并且如果某人意识到过滤器错误,会使回滚更加困难。一个更安全的批处理作业可能一次删除几千个文档,短暂休眠,并通过_id或时间戳记录进度。

while (true) {
  const result = db.events.deleteMany({
    createdAt: { $lt: ISODate("2025-01-01T00:00:00Z") },
    archived: true
  });

  print(`deleted ${result.deletedCount}`);
  if (result.deletedCount === 0) break;
  sleep(500);
}

在实际的生产脚本中,添加一个限制模式而不是在整个范围内使用deleteMany,记录每个批次,并在复制延迟或磁盘I/O超过阈值时自动停止。

对听起来太简单的索引建议要小心

删除未使用的索引是减少MongoDB磁盘空间的最佳方法之一,但“未使用”需要上下文。一个索引可能在安静的一周看起来未使用,但对于月末报告、后台对账或罕见的客户支持工作流仍然至关重要。

使用$indexStats查看访问模式:

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

然后将结果与应用程序代码、计划作业、仪表板和支持查询进行比较。如果一个索引自上次重启以来未被使用,这是一个信号,而不是定论。在删除之前,检查服务器是否最近重启过,以及工作负载样本是否包含重要的作业。

还要注意重叠的复合索引。如果您有以下索引:

{ customerId: 1 }
{ customerId: 1, createdAt: -1 }
{ customerId: 1, createdAt: -1, status: 1 }

您可能能够移除一个,但只有在检查排序顺序、查询过滤器以及较短的索引是否支持不同的访问模式之后。MongoDB可以使用复合索引的左前缀,但这并不意味着最大的索引总是免费的替代品。更大的索引消耗更多的内存和写入I/O,所以保留适合工作负载的索引,而不是看起来最完整的索引。

在副本集上进行大型收缩操作时首选重新同步

对于大型副本集,回收操作系统磁盘空间的最干净方法通常是逐个重建辅助节点。基本思路是:

  1. 确认您有健康的复制和当前的备份。
  2. 移除或停止一个辅助节点。
  3. 清除其本地数据目录。
  4. 让它从主节点或其他健康成员重新同步。
  5. 对下一个辅助节点重复。
  6. 在维护窗口期间降级主节点,最后重建旧的主节点。

这种方法比运行命令慢,但更容易推理,因为每个重建的成员基于当前数据写入新的存储文件。它还避免了在生产流量下尝试压缩每个集合。它不是免费的:初始同步可能对网络和磁盘造成很大压力,并且您需要足够的剩余成员来在重建一个成员时保持副本集安全。

对于独立的MongoDB服务器,您没有这种奢侈。在这种情况下,计划一个维护窗口,进行经过测试的备份,并考虑mongodump/mongorestore或文件系统级迁移到新卷。不要仅仅因为想要更小的数据目录而选择mongod --repair。将修复视为恢复工具,而不是常规的维护工作。

也要关注Oplog、日志和备份

并非所有MongoDB磁盘压力都来自集合。在副本集中,oplog是一个固定集合,所以它不应该无限增长,但其配置的大小仍然很重要。如果太小,辅助节点可能在维护期间落后。如果在小磁盘上远大于所需,可能会浪费空间。有意识地审查它:

db.getSiblingDB("local").oplog.rs.stats()

MongoDB日志也可能在慢查询日志、调试详细程度或应用程序错误循环变得嘈杂时填满磁盘。使用日志轮换,并尽可能将数据库日志与存储数据的小卷分开。

备份是另一个常见的惊喜。团队有时为了方便将mongodump运行到同一主机,然后想知道为什么在备份窗口期间磁盘警报触发。存储在相同文件系统上的备份算不上真正的备份,并且可能在已经风险较高的操作期间将MongoDB推向更糟糕的中断。将备份流式传输到对象存储、备份服务器或单独的挂载卷。

MongoDB磁盘满的实用操作手册

如果磁盘已经超过90%,放慢速度并按此顺序工作:

  1. 确认MongoDB是否仍在接受写入以及副本集是否健康。
  2. 如果平台允许,添加临时磁盘容量。这通常比紧急删除更安全。
  3. 移动或轮换过大的日志和本地备份文件。
  4. 停止非必要的、写入量大的批处理作业。
  5. 使用db.stats()和集合stats()识别最大的集合和索引。
  6. 仅归档或删除具有明确保留规则的数据。
  7. 在系统稳定后计划压缩、重新同步或恢复。

最好的长期修复是乏味的:保留规则、索引审查、磁盘警报和经过测试的重建程序。MongoDB很乐意重用内部可用空间,但操作员仍然需要决定哪些数据值得存储在快速存储上,哪些可以移动到其他地方。