自增ID或时间戳作分片键必然导致热点,因范围分片将新文档全路由至最大值所在chunk,该chunk固定驻留单一分片且均衡器不主动拆分或迁移;需改用高基数复合键(如{"service":1,"timestamp":1})或哈希分片(如{"user_id":"hashed"})分散写压力。

为什么自增ID或时间戳作分片键必然导致热点
因为范围分片按片键值连续切 chunk,所有新文档都落在最大值区间,而这个区间默认驻留在某个分片上。均衡器不会主动拆分或迁移它——除非你手动干预。现象是:sh.status() 显示某分片 chunk 数极少但数据量占 80%+,mongostat 看到该分片 netIn 和 conn 持续飙升,其他分片几乎空闲。
怎么选分片键才能真正分散写入压力
核心是“高基数 + 写入均匀 + 查询友好”。不能只看字段是否存在,得看它在实际写入流中的分布质量:
- 避免纯
{ "timestamp": 1 }或{ "_id": 1 }:日志、订单、设备上报等场景下,新值天然单调递增,等于把所有写请求定向投递给同一个分片 - 优先用复合片键,例如
{ "service": 1, "timestamp": 1 }:前导字段(service)必须取值足够多且分布均匀(如微服务名、主机名、设备类型),才能把时间维度“打散” - 点查为主时直接哈希,例如
{ "user_id": "hashed" }:MongoDB 自动建索引,写入绝对均匀;但find({ "user_id": { "$gt": "a", "$lt": "z" } })会广播到所有分片 - UUID v4 可作分片键,但必须显式指定:
sh.shardCollection("db.col", { "uuid_field": "hashed" });仅客户端生成 UUID 却没把它设为分片键,路由仍按原键走
已经出现热点,怎么紧急止损
手动拆分 + 迁移是唯一有效路径,但必须严格按顺序操作,否则白忙活:
- 先确认均衡器开启:
sh.setBalancerState(true),否则sh.moveChunk()会被拒绝 -
sh.splitAt("db.col", { "timestamp": ISODate("2026-07-20T00:00:00Z") }):只切逻辑块,不移动数据 - 立刻迁移新 chunk:
sh.moveChunk("db.col", { "timestamp": { "$gte": ISODate("2026-07-20T00:00:00Z") } }, "shard03") - 迁移期间该 chunk 不可写,chunk 超过 128MB 就容易卡住;若卡住,需 kill 迁移进程再重试
-
sh.status()显示 chunk 数变多了 ≠ 负载真均衡,还得用db.currentOp()查活跃操作来源,结合mongostat看各分片实时netIn
最容易被忽略的隐性热点来源
不是分片键本身设计错,而是应用层行为或配置放大了倾斜:
- 高频读取固定几个
user_id值,反复打在同一分片上,mongotop显示某分片耗时占比长期超 60% - 写入语句漏传分片键字段,或类型不匹配(比如定义为
{ "user_id": "hashed" },却传字符串而非数字),导致 mongos 广播写入,qr|qw队列堆积 - 对分片集合执行无分片键的
updateMany或deleteMany,全分片广播 + 默认无 writeConcern 限制,瞬间拖垮 mongos - 某分片缺失关键查询索引,导致大量文档扫描,CPU 不高但响应慢,误判为“负载正常”
真实负载从来不在 chunk 分布图里,而在 db.currentOp() 的活跃操作、mongostat 的实时网络流入、以及应用层是否在反复命中同一组值。写热点不是配置问题,是数据分布与业务写入模式共同作用的结果——改键只是起点,后续必须持续用运行时指标验证效果。











