递增ID导致范围分片热点:新数据全写入最后一个chunk,因片键值持续增大且均衡器不干预写入倾斜;哈希分片可均匀分布但牺牲范围查询;复合片键是折中方案,需高基数前缀并严格匹配索引与分片键。

范围分片遇上递增ID,新数据全挤在最后一个chunk
因为MongoDB范围分片按片键值大小切块,而递增ID(如自增整数、ObjectId、毫秒时间戳)的值永远比已有数据大,所有新文档都落入当前最大值区间——也就是最后一个chunk所在分片。这个chunk默认不拆、不迁,写入就卡死在那里。
常见错误现象包括:
-
sh.status()显示某分片 chunk 数极少但size占比超 80% -
mongostat中该分片的netIn和conn持续飙高,其他分片 idle -
db.currentOp({ secs_running: { $gt: 2 } })显示大量 insert 卡在acquiringLock
均衡器不会主动救火,它只管“静态”不均
MongoDB 均衡器(balancer)的设计逻辑是:只迁移已存在、且大小超标或分布不均的 chunk,但对“正在疯狂追加”的最大 chunk 视而不见——它认为这是正常行为,不是问题。
这意味着:
-
sh.splitAt()只切逻辑块,新 chunk 还落在同一分片上 - 不配
sh.moveChunk(),数据根本不动 - 即使手动拆分,若没立刻迁移,写入压力仍在原地累积
- 迁移大 chunk(如 >128MB)会阻塞写入,风险不可控
哈希分片能破局,但代价明确
用 { _id: "hashed" } 或 { timestamp: "hashed" } 作为分片键,MongoDB 会对字段值做哈希运算,把 1、2、3、1000 打散成完全不相关的哈希值,强制写入均匀落到各分片。
但它不是银弹:
- 点查(
find({ _id: ObjectId("...") }))仍快,mongos 精准路由 - 范围查(
find({ timestamp: { $gt: ..., $lt: ... } }))必须广播到所有分片 -
$min/$max、.sort({ _id: 1 })失效,结果需合并 - 哈希后原始值不可排序、不可范围判断,字段语义丢失
复合片键是折中解,但设计门槛高
如果业务强依赖时间范围查询,又想避免热点,得引入高基数离散字段作前缀,比如 { service: 1, timestamp: 1 }。
关键实操条件:
- 必须先为该组合建索引:
db.logs.createIndex({ "service": 1, "timestamp": 1 }) - 分片命令要严格匹配:
sh.shardCollection("logging.logs", { "service": 1, "timestamp": 1 }) - service 字段取值必须足够多且分布均匀;若只有 "api" 和 "web" 两个值,照样热点
- 查某服务某时段日志能走索引前缀,但查“全部服务最近一小时”仍是全分片扫描
真正容易被忽略的是:chunk 分布均匀 ≠ 负载均衡。一个分片可能 chunk 数正常,却因缺失索引、高频点查固定 ID、或 mongos 路由缓存过期,悄悄成为读写瓶颈。得靠 db.currentOp() 和实时 mongostat --host mongos 验证,不能只看 sh.status()。











