sh.status() 显示均衡但实际可能严重倾斜,因其仅统计 chunk 数量而非数据量;需直连分片查 stats() 验证,并预分片时禁用自动分裂、暂停 balancer、严格按范围分片键执行 splitat 和 movechunk。

sh.status() 看起来均衡,实际可能严重倾斜——这是 MongoDB 分片集群里最隐蔽也最常被误判的问题。根本原因不是 balancer 没工作,而是它只看 chunk 数量,不看 chunk 里装了多少数据。
为什么 sh.status() 会骗人
它默认只统计每个分片上 chunk 的个数,一个分片有 120 个 chunk,不代表它只存了 120 份小数据;很可能其中 119 个是空的或极小,1 个是 jumbo chunk(超 64MB 且无法分裂),95% 数据全压在那一个 chunk 所在分片上。
这种情况在范围分片(如 {ts: 1})+ 时间集中写入时高频出现:所有新写入都落在最新 chunk,旧 chunk 长期不动,balancer 根本不触发迁移——因为“chunk 数”没超标。
-
sh.status({verbose: true})在 4.4+ 版本可能显示近似docs和size,但只是采样估算,不可信 - 真正可靠的数据量必须直连分片查:
mongo --host shard0001:27018→use db→db.coll.stats({scale: 1024*1024}) - 重点关注
size(原始数据 MB)、storageSize(磁盘占用 MB)、count(文档数)三项
手动预分片前必须停掉三件事
预分片不是“切得越多越匀”,错一步后续几乎无法自动修复。
- 确认分片键区分度足够:
db.collection.distinct("shard_key").length应接近文档总数;否则预分再多 chunk,数据仍堆在同一个值上 - 禁用自动分裂:
sh.disableAutoSplit("db.coll"),否则预分后立刻被 auto-split 打乱节奏 - 暂停 balancer:
sh.stopBalancer(),否则它会在你执行splitAt过程中抢着迁移,导致 chunk 归属混乱
用 splitAt + moveChunk 控制初始分布
适用于范围分片键(如 {ts: 1}),不适用于哈希分片键(如 {_id: "hashed"})。
-
splitAt只能在主分片上执行;bounds必须严格递增、覆盖全集;shard参数必须是从sh.status()输出中复制的真实分片 ID(别手敲) - 示例(按时间均分三段,指定归属):
use admin db.runCommand({splitAt: "db.coll", bounds: [{ts: ISODate("2024-01-01")}, {ts: ISODate("2024-07-01")}], shard: "shard0001"}) db.runCommand({splitAt: "db.coll", bounds: [{ts: ISODate("2024-07-01")}, {ts: ISODate("2025-01-01")}], shard: "shard0002"}) db.runCommand({splitAt: "db.coll", bounds: [{ts: ISODate("2025-01-01")}, {ts: ISODate("2025-07-01")}], shard: "shard0003"}) - 执行完后,务必查
config.chunks确认记录已生成:db.getSiblingDB("config").chunks.find({ns: "db.coll"}).sort({min: 1})
预分后不验证 = 白干
漏掉验证环节,大概率上线后才发现还是热分片。
- 逐个直连分片执行
db.coll.stats(),横向比对size和count,确认是否落在预期范围内 - 检查是否存在 orphaned 文档:
db.runCommand({cleanupOrphaned: "db.coll"})(需在对应分片上执行) - 重新启用 balancer 前,先等 5 分钟观察:
sh.startBalancer()后用sh.isBalancerRunning()确认已启动,再盯 10 分钟看 config.migrations 是否有新记录
最容易被忽略的是:预分片只解决“初始分布”,如果分片键本身单调递增(如时间戳),后续持续写入仍会再次倾斜——这时得考虑换哈希分片,或加随机后缀改造分片键。











