必须立刻停写并重建分片键,因数据分布极度倾斜无法靠balancer或chunksize调整解决;重点检查shard_key字段distinct值数量及_id单调性,哈希分片对取值少的字段无效,复合键若高基数字段实际值少仍会倾斜;停写后需确保新分片键字段存在且类型一致、用mongorestore --drop导入、预分片前禁用auto-split和balancer;jumbo chunk需用sh.status(true)识别,分可分割与不可分割两类,后者须refine或重建;历史数据含大量null时须先清洗再重建。

数据分布极度倾斜不是靠调 balancer 或改 chunkSize 能救回来的,必须立刻停写、定位根因、针对性重建分片键。拖得越久,jumbo chunk 越多,迁移越卡死。
怎么快速确认是不是分片键本身崩了
别信 sh.status() 里 chunk 数量均衡就万事大吉——它不反映实际数据重量。重点查两件事:
- 执行
db.collection.distinct("shard_key_field").length,如果结果 - 跑聚合看头部集中度:
db.collection.aggregate([{$group: {_id: "$shard_key_field", count: {$sum: 1}}}, {$sort: {count: -1}}, {$limit: 10}]),前10项加起来占总文档数 >30%,就是典型“三四个值扛全量” - 特别注意
_id字段:如果业务用的是自增整数或时间戳生成_id,那天然就是单调递增陷阱,写入永远堆在最新 chunk
为什么哈希分片键此时大概率没用
哈希只打散物理位置,不凭空变出新值。原始字段只有 5 个取值,{"field": "hashed"} 后还是最多落到 5 个 chunk 上——数据照样堆死。
- 复合键如
{tenant_id: "hashed", created_at: 1}更危险:若tenant_id实际只有 3 个有效值,那每个租户的所有时间数据都锁死在同一分片,毫无分散效果 - 所有范围查询(
{$gt: x, $lt: y})退化为全分片扫描,QPS 直接跳崖 - 哈希后无法按原始值做等值路由,运维排查、备份恢复都变复杂
停写后必须立刻做的三件事
重建分片键不是导出再导入那么简单,漏一步就会残留脏数据或直接报错:
- 执行
sh.shardCollection("db.coll", {"new_shard_key": 1})前,必须确保该字段在所有文档中都存在且类型一致,否则报错cannot shard collection with missing or inconsistent shard key value - 导入必须用
mongorestore --drop,漏掉--drop会导致旧数据残留,新旧数据混杂引发逻辑错误 - 预分片前务必先停掉两件事:
sh.disableAutoSplit("db.coll")和sh.stopBalancer(),否则刚切好的 chunk 立刻被 auto-split 拆乱,或被 balancer 抢着迁移错位
最容易被忽略的隐形坑:jumbo chunk 卡死迁移
一旦出现标记为 jumbo 的 chunk,balancer 就彻底放弃迁移——它连拆都拆不动。这类 chunk 分两种:
-
可分割的 jumbo:chunk 内含多个唯一分片键值,但因 chunkSize 设置过大未触发自动分裂。手动执行
sh.splitAt("db.coll", {shard_key_field: "some_value"})强制切开 -
不可分割的 jumbo:整个 chunk 只有一个分片键值(比如全是
"A"),再怎么 split 都无效。此时只能用refineCollectionShardKey给分片键加后缀字段,或直接重建集合 - 检查 jumbo chunk 必须用
sh.status(true),普通sh.status()不显示 jumbo 标记
真正难的不是操作步骤,而是判断要不要动——如果新分片键字段在历史数据里大量为 null 或空字符串,MongoDB 会把所有 null 归为同一分片键值,等于换汤不换药。得先清洗数据,再重建。











