1. 为什么你的分片集群会"偏科"?

当我们在MongoDB分片集群中突然发现某个分片服务器的磁盘使用率飙升到90%,而其他节点还有大量空闲空间时,就像班级里有个学生突然吃掉了整个披萨,而其他同学只能干瞪眼。这种数据分布不均的常见原因包括:

  1. 分片键选择不当:比如使用时间戳作为分片键,导致所有新数据都写入最后一个分片
  2. 热点数据集中:某个数值范围的数据被频繁访问(如特定用户ID段)
  3. 数据删除后遗留空洞:大量删除操作导致chunk碎片化
  4. 新增分片后的适应期:新加入的分片需要时间接收迁移的数据

最近我们生产环境就遇到一个典型案例:使用{userId:1}作为分片键的用户表,由于某个广告活动导致特定用户段(50000-60000)的数据量暴增10倍,造成该分片负载激增。

2. 三步诊断法:找出不平衡的元凶

2.1 快速检查命令

# 连接mongos节点
mongo --host mongos01:27017

# 查看分片状态
sh.status()

# 检查均衡器状态
use config
db.locks.findOne({_id: "balancer"})

# 查询chunk分布
db.chunks.find({ns: "mydb.users"}).sort({shard:1})

2.2 C#监控示例(使用MongoDB.Driver)

using MongoDB.Driver;

var client = new MongoClient("mongodb://mongos01:27017");
var configDb = client.GetDatabase("config");

// 获取各分片chunk数量
var chunks = configDb.GetCollection<BsonDocument>("chunks");
var pipeline = new BsonDocument[]
{
    new BsonDocument("$group", 
        new BsonDocument{
            {"_id", "$shard"},
            {"count", new BsonDocument("$sum", 1)}
        })
};

var result = chunks.Aggregate<BsonDocument>(pipeline).ToList();
foreach(var doc in result)
{
    Console.WriteLine($"分片 {doc["_id"]} 包含 {doc["count"]} 个chunk");
}

2.3 关键指标阈值

  • 单个分片chunk数量超过其他分片2倍
  • 最大分片数据量超过最小分片50%
  • 分片间磁盘使用率差异超过30%

3. 平衡手术:让数据流动起来

3.1 自动平衡模式

# 启用平衡器(默认已开启)
sh.startBalancer()

# 设置平衡时间窗口(避免高峰时段)
use config
db.settings.update(
   { _id: "balancer" },
   { $set: { activeWindow : { start : "23:00", stop : "6:00" } } },
   { upsert: true }
)

3.2 手动干预六式

当自动平衡不够时,试试这些招数:

第一式:增大chunk尺寸

# 临时修改chunk大小为128MB
use config
db.settings.save({ _id: "chunksize", value: 128 })

# 永久修改需配置文件中设置
mongos --chunkSize 128

第二式:手动分裂大chunk

sh.splitAt("mydb.users", {userId: 30000})
sh.splitFind("mydb.users").min({userId: 1}).max({userId: 100000})

第三式:定向迁移chunk

sh.moveChunk("mydb.users", {userId: 25000}, "shard2")

第四式:紧急刹车

sh.stopBalancer()
sh.setBalancerState(false)

第五式:分片标签管理

sh.addShardTag("shard2", "hot")
sh.addTagRange("mydb.users", {userId: 0}, {userId: 100000}, "hot")

第六式:终极武器——清空重置

# 先排空分片数据
db.adminCommand({ removeShard: "shard3" })

# 数据迁移完成后重新加入集群
sh.addShard("shard3.example.net:27017")

4. 实时监控的三大法宝

4.1 内置工具三板斧

# 实时迁移监控
db.currentOp(true).inprog.forEach(
  function(op) { 
    if(op.desc.indexOf("RangeDeleter") != -1) printjson(op) 
  }
)

# 性能分析
db.adminCommand({ getLog: "global" })

# 慢查询检测
db.setProfilingLevel(1, { slowms: 50 })

4.2 C#监控脚本示例

using MongoDB.Driver;

var client = new MongoClient("mongodb://mongos01:27017");
var adminDb = client.GetDatabase("admin");

// 获取迁移任务队列
var balancerStatus = adminDb.RunCommand<BsonDocument>(
    new BsonDocument { { "balancerStatus", 1 } });

Console.WriteLine($"当前迁移队列长度: {balancerStatus["numQueuedMigrations"]}");
Console.WriteLine($"最近错误: {balancerStatus["lastError"] ?? "无"}");

// 检查分片容量差异
var shards = client.GetDatabase("config").GetCollection<BsonDocument>("shards");
foreach(var shard in shards.Find(new BsonDocument()).ToList())
{
    var status = adminDb.RunCommand<BsonDocument>(
        new BsonDocument { { "dbStats", 1 }, { "scale", 1024*1024*1024 } });
    Console.WriteLine($"分片 {shard["_id"]} 数据量: {status["dataSize"]:N2} GB");
}

5. 什么情况下需要手动平衡?

  • 业务高峰前准备:如电商平台在双11前预平衡
  • 分片硬件升级后:新SSD分片需要快速填充数据
  • 热点事件处理:突发新闻导致特定数据暴增
  • 分片键变更过渡期:新旧分片键交替阶段

6. 技术选择的双刃剑

优点

  • 自动平衡机制简单易用
  • 手动控制提供应急手段
  • 标签分片实现精细控制

缺点

  • 平衡操作带来额外IO压力
  • 超大chunk可能导致临时性能下降
  • 手动操作存在误操作风险

7. 必须牢记的三大纪律

  1. 备份先行:操作前务必执行mongodump
  2. 避开高峰:选择业务低峰期执行迁移
  3. 渐进调整:每次调整chunk大小不超过2倍

8. 总结与经验分享

经过多次实战,我们总结出分片平衡的"三三法则":

  • 三次监控:操作前、中、后全程监控
  • 三不原则:不盲动、不贪快、不侥幸
  • 三备方案:回滚计划、熔断机制、应急手册

记住,良好的分片设计比后期平衡更重要。就像盖房子,地基打好了,后期装修才省心。下次设计分片方案时,不妨多花点时间分析数据分布模式,选择一个能自然分散数据的分片键,这比任何平衡技巧都管用。