递增分片键(如{timestamp:1}或{_id:1})必然导致单点写入,因其按范围分片,新文档始终追加到最大值所在chunk,而该chunk固定位于最后一个分片,MongoDB不主动拆分追加中的chunk,均衡器也不干预写入热点。

为什么递增分片键必然导致单点写入
用 { timestamp: 1 } 或 { _id: 1 } 这类递增字段做分片键,MongoDB 会把所有新文档按值大小顺序塞进「当前最大 chunk」——它永远落在最后一个分片上。这不是负载不均,是设计使然:范围分片只看值区间,不预判写入节奏。
现象很直接:sh.status() 显示某个分片 chunk 数极少但数据量占 80%+,mongostat 里它的 netIn 和 conn 持续飙高,其他分片 idle。
- 根本限制:MongoDB 不会主动拆「正在追加」的 chunk,哪怕它已超 256MB
- 均衡器(balancer)默认不干预写入热点,只负责已有 chunk 的迁移
- 即使手动
sh.splitAt(),新 chunk 还是落在同一分片,除非立刻sh.moveChunk()
哈希分片如何打破写入连续性
哈希分片不是“随机”,而是对分片键值做确定性哈希(如 CRC32 或内部哈希函数),再按哈希值范围分块。原始值相近的文档(比如 userId: "u1001" 和 "u1002")哈希后大概率落到不同分片。
命令极简:sh.shardCollection("mydb.users", { "userId": "hashed" })。MongoDB 自动建哈希索引,无需提前 ensureIndex。
- 效果:写入压力天然打散到全部分片,CPU、磁盘 IO、连接数基本齐平
- 代价:所有
$gt/$lt/$in查询必须广播到全部分片,网络开销翻倍 - 注意:
userId哈希后无法排序、无法范围扫描,sort({ userId: 1 })会失效
哈希分片的适用边界在哪
它只在「点查为主 + 写入有明显时间/序号倾向」时是解药。一旦业务强依赖时间窗口查询(如“查昨天所有订单”),哈希分片会让查询变慢且耗资源。
- 适合场景:
find({ userId: "u123" })、find({ _id: ObjectId("...") })、登录态校验 - 不适合场景:
find({ createdAt: { $gte: ISODate("..."), $lt: ISODate("...") } })、日志归档导出 - 折中方案:复合分片键
{ "randomBucket": 1, "createdAt": 1 },用randomBucket打散写入,保留createdAt支持范围查询(但需确保randomBucket基数足够高)
迁移旧集合时最容易忽略的三件事
线上切分片键不是改个配置就能跑,尤其从范围键切到哈希键,实际操作中常卡在细节。
- 不能直接
sh.shardCollection到已有数据的集合——必须先清空或新建集合再导入 - 哈希分片后,原
unique索引若含非分片键字段,必须删掉重建,否则插入失败 - 应用层要确认所有查询都带上了哈希分片键字段;漏掉
userId的find({ status: "active" })会退化为全分片广播
最麻烦的其实是监控:哈希后 sh.status() 看起来均匀了,但得盯住 mongos 的 queryTargeting 指标,确认实际路由没走偏——毕竟分片键选错,问题只是藏得更深了。


















