1. 问题现象:删了数据为何磁盘没瘦身?

当我们在MongoDB中执行db.collection.deleteMany({})或删除整个集合时,会发现磁盘空间并未立即释放。这就像把衣柜里的衣服扔进垃圾桶,但垃圾桶还在原地占着空间。例如:

// 示例1:删除日志集合中30天前的数据(MongoDB 5.0)
db.access_log.deleteMany({ 
    "timestamp": { $lt: new Date(Date.now() - 30*24*60*60*1000) } 
});

执行后通过db.stats()查看存储情况:

{
    "db" : "logs",
    "collections" : 12,
    "dataSize" : 104857600,  // 剩余数据量100MB
    "storageSize" : 536870912 // 实际占用空间512MB
}

明明删除了70%的数据,存储空间却纹丝不动。这种"虚胖"现象主要源于MongoDB的存储设计机制。


2. 技术原理:存储引擎的"衣柜管理术"

MongoDB的WiredTiger存储引擎采用写时复制(Copy-on-Write)机制。当执行删除操作时:

  1. 数据块被标记为"可覆盖"
  2. 新数据优先写入空白区域
  3. 原存储空间保持"占位"状态

这种设计带来了两个关键特性:

  • 预分配机制:默认预分配数据文件(如collection-123.wt
  • 碎片化存储:删除操作导致存储碎片,类似硬盘的"文件碎片"

3. 解决方案实战:四步回收空间

3.1 方案1:全量修复(适合小型数据库)
// 示例2:修复数据库(MongoDB 4.4+)
use admin
db.runCommand({ 
    repairDatabase: 1,
    preserveUUID: true // 保持集合UUID不变
});

执行效果

  • 重建所有集合文件
  • 需要剩余空间=原数据库大小×2
  • 生产环境慎用(会阻塞读写)
3.2 方案2:集合压缩(推荐方案)
// 示例3:压缩订单集合(MongoDB 5.0+)
db.runCommand({
    compact: 'orders',
    force: true,       // 主节点强制执行
    paddingFactor: 1.0 // 取消预留空间
});

参数说明

  • paddingFactor:调整文档增长预留空间(1.0表示不预留)
  • force:在主节点直接执行
3.3 方案3:副本集滚动维护(生产环境推荐)
for member in rs.conf().members:
    mongosh --host $member --eval """
        rs.stepDown(300);
        db.adminCommand({ compact: 'sensor_data' });
        sleep(10000);  // 等待压缩完成
    """
3.4 方案4:分片集群处理
// 示例5:分片集群压缩(MongoDB 5.0+)
sh.getShards().forEach(function(shard){
    var conn = new Mongo(shard.host);
    conn.getDB('admin').runCommand({
        compact: 'customer_profiles',
        shardName: shard._id
    });
});

4. 关联技术:存储引擎的隐藏技能

WiredTiger的压缩参数配置:

# mongod.conf
storage:
  wiredTiger:
    engineConfig:
      cacheSizeGB: 2
      journalCompressor: snappy  # 日志压缩算法
    collectionConfig:
      blockCompressor: zstd     # 集合数据压缩算法

支持的三级压缩:

  1. 字段级压缩(Per-field)
  2. 文档级压缩(Per-document)
  3. 块级压缩(Per-block)

5. 应用场景分析

场景类型 推荐方案 典型业务案例
开发测试环境 全量修复 本地功能测试数据库
中小型生产库 定时集合压缩 电商订单表按月归档
大型分片集群 分片滚动维护 物联网设备数据存储
超高频写入场景 调整paddingFactor参数 实时日志分析系统

6. 技术方案优缺点对比

方案 优点 缺点
全量修复 彻底解决碎片问题 需要双倍磁盘空间
集合压缩 精准控制目标集合 主节点性能抖动
副本集滚动维护 不影响业务连续性 维护窗口期较长
分片处理 适合超大规模数据 需要协调多个分片节点

7. 操作注意事项

  1. 备份先行:执行前必须进行mongodump或文件系统快照
  2. 避开高峰:选择业务低峰期执行压缩操作
  3. 监控指标
    // 示例6:实时监控压缩进度
    db.currentOp().inprog.forEach(op => {
        if(op.command && op.command.compact) {
            printjson(op.progress);
        }
    });
    
  4. 分片集群需要先平衡器

8. 最佳实践总结

  1. 预防优于治理:合理设计文档结构,避免频繁增删改
  2. 定期维护:建议每月执行一次碎片率检查
    // 示例7:碎片率计算
    var stats = db.orders.stats();
    var fragmentation = (stats.storageSize / stats.size).toFixed(2);
    print('当前碎片率:', fragmentation * 100 + '%');
    
  3. 容量规划:保持至少30%的磁盘空闲空间
  4. 版本升级:MongoDB 5.0+的压缩效率提升40%