排查分片集群添加新分片后数据不均衡的原因

为应对业务增长,向 MongoDB 分片集群添加新分片是常见的水平扩容操作。但有时新分片添加后,数据和负载并未自动均衡,导致新分片近乎空闲,旧分片压力依旧,扩容效果未达预期。本文提供一套系统的诊断流程,从均衡器状态、数据分布特征两个层面,定位数据不均衡问题,提供可参考的解决方案,以恢复集群的均衡状态。

诊断思路

数据能否在新旧分片间均衡分布,核心依赖于 MongoDB 的内置均衡器(Balancer)。均衡器通过自动迁移数据块(Chunk)来工作。因此,排查数据不均衡问题,应围绕均衡器的工作状态及影响其迁移决策的因素展开。

推荐采用以下自上而下的分层排查逻辑:

  1. 检查均衡器状态:确认数据迁移的基础——均衡器是否已启用并处于活动状态。

  2. 分析数据可迁移性:排查是否存在因集合自身属性(如未分片、数据量未达均衡触发阈值、含jumbo chunk)而导致均衡器无法迁移的数据。这是数据量不均的常见直接原因。

实施步骤

诊断均衡器(Balancer)状态

目的:确认作为数据迁移核心组件的均衡器是否已启用、正在运行,并配置了合理的活动窗口。Chunk 的迁移完全依赖均衡器来完成,如果均衡器未正常工作,数据将无法从旧分片迁移到新分片。

1. 检查均衡器是否已全局启用

  • 检查方法

    mongosh 中执行 sh.getBalancerState() 返回 false,则表示均衡器未启用。

  • 解决方案

    若均衡器未启用,参考管理MongoDB均衡器Balancer开启。

2. 检查均衡器是否在活动窗口内

均衡器仅在设定的活动窗口期(Active Window)内执行 Chunk 迁移。如果当前时间不在活动窗口内,均衡器将不会工作。

  • 检查方法

    db.getSiblingDB("config").settings.find({_id:"balancer"})

    返回示例:

     { _id: 'balancer', activeWindow: { start: '02:00', stop: '04:00' } }

    如果当前时间不在activeWindow内,则均衡器不会工作或者系统判断无需均衡。

  • 解决方案:修改活动窗口

    如果活动窗口设置不当(例如过短或与业务高峰重叠),建议将窗口期设置为时长足够的业务低峰时段。

    例如,设置窗口期为每日凌晨 2 点到 6 点:

    db.getSiblingDB("config").settings.updateOne(
      { _id: "balancer" },
      { $set: { activeWindow : { start : "02:00", stop : "06:00" } } },
      { upsert: true }
    )

3. 确认是否处于初始数据均衡过程

刚添加分片后,系统需要时间将存量数据从旧分片迁移至新分片,此期间数据不均衡是正常现象,数据均衡所需时间与数据量级相关。

  • 检查方法

    通过 sh.isBalancerRunning() 确认均衡进度。

    // 检查均衡器是否正在运行迁移任务
    sh.isBalancerRunning()

如果 sh.isBalancerRunning() 返回 true说明均衡正在进行中。

分析数据可迁移性

排查是否存在因集合自身属性而无法被均衡器自动迁移的数据。

1. 检查是否存在大量未分片集合

一个数据库内可以同时存在分片集合和非分片集合。非分片集合的数据会完整存放在其主分片(Primary Shard)上,均衡器无法对其进行迁移。如果大型集合未被分片,其数据将永远无法分布到新分片上。

PixPin_2025-11-20_10-54-27

  • 检查方法

    使用以下脚本统计指定数据库中,未分片集合占用的存储空间。该脚本会找出所有未分片的集合,并累加其存储大小。

    说明

    该脚本会遍历库内所有集合并执行 stats() 命令,可能对数据库造成性能影响。为降低风险,脚本内置了 sleep(200) 延时。如果库内集合数量比较多,建议将sleep时长进一步提升到2000或者更高来避免可能的业务影响。

    // 脚本:统计指定数据库中未分片集合的存储占用
    // 将 "myDB" 替换为需要检查的数据库名称
    var dbName = "myDB";
    
    var unshardSize = 0;
    var totalSize = 0;
    
    var unshardedCollections = [ ];
    
    
    db.getSiblingDB(dbName).getCollectionNames().forEach(function(collName) {
        var stats = db.getSiblingDB(dbName).getCollection(collName).stats();
        
        // 根据 stats.sharded 字段判断并累加存储大小
        if (!stats.sharded) {
            unshardSize += (stats.storageSize || 0);
            unshardedCollections.push(dbName + "." + collName);
        }
        totalSize += (stats.storageSize || 0);
        
        // 每次查询后短暂休眠,避免对数据库造成过大压力
        sleep(200);
    });
    
    print("--- " + dbName + " ---");
    print("Unsharded Collections Count: " + unshardedCollections.length);
    print("--------------------");
    print("Unsharded Storage Size (Bytes): " + unshardSize);
    print("Total Storage Size (Bytes): " + totalSize);
    print("--------------------");
    print("Unsharded Collections List:");
    printjson(unshardedCollections);
    
  • 解决方案:参考设置数据分片以充分利用Shard性能

2. 检查分片集合数据量是否过小(MongoDB 6.0+)

MongoDB 6.0.3开始。均衡器只在集合中分片间最大数据量差值达到一定阈值(默认为chunk size3倍)后才开始迁移。如果一个分片集合的总数据量小于均衡阈值,均衡器不会对其进行处理,导致其数据集中在单个分片上。

  • 检查方法

    使用 db.collection.getShardDistribution() 查看集合在各分片上的分布。如果一个分片集合的数据大部分集中在单个分片上,且分片间最大数据量差值远小于均衡触发阈值,则可能属于此情况。

  • 解决方案

    对于这类小数据量的分片集合,可适当调小其 chunkSize,触发 Chunk 拆分和迁移。

    // 示例将 test_db.test_coll 集合的 chunkSize 调整为 16MB,请根据集合数据分布情况调整合适的chunkSize
    db.adminCommand({
      configureCollectionBalancing: "test_db.test_coll",
      chunkSize: 16 // 单位为 MB,6.0+版本的默认值为128MB。
    })
    

    等待迁移完成后可通过db.collection.getShardDistribution()查看数据分布情况是否符合预期。

操作影响与风险

  • 均衡性能开销:数据均衡过程中的 Chunk 迁移(moveChunk)和范围删除(RangeDeleter)会消耗源分片和目标分片的 CPU 及 I/O 资源。为避免影响线上业务,建议将均衡器活动窗口设置在业务低峰期。

  • ChunkSize 配置风险:在MongoDB 6.0之前的版本,将 chunkSize 设置得过小,可能导致 Chunk 数量过多,增加元数据管理的开销,并可能引发过于频繁的均衡活动。应根据集合的总数据量大小和增长率合理设置。