自增\_id作分片键必然导致热点分片,因其范围分片机制使新写入集中于最大值区间;改用哈希分片(如{ "\_id": "hashed" })可均匀分布写入,但会牺牲范围查询、排序和聚合性能。

自增 _id 作分片键必然导致热点分片,这不是配置没调好,而是范围分片机制本身决定的——新写入全挤在最大值区间,只打一个分片。
为什么 _id 自增会卡死一个分片
范围分片按 _id 值切 chunk,比如 [1, 1000)、[1000, 2000)、[2000, ∞)。所有新插入的自增 ID 都落在最后一个区间,而这个区间默认驻留在某个分片上。均衡器不认为这是“不均衡”,它默认“持续追加”是合理行为,所以不会主动拆、也不会迁移。
现象很典型:sh.status() 显示某分片 chunk 数极少但数据量占 80%+;mongostat 看到该分片 netIn 和 conn 远高于其他节点;db.currentOp() 查到大量写操作集中在一个分片上。
- 仅用
sh.splitAt()没用:只切逻辑块,不移动数据 - 不配
sh.moveChunk(),等于白干 -
_id字段即使改成ObjectId,若仍用{ _id: 1 }范围分片,因时间前缀部分单调,仍可能轻微热点
改用哈希分片是最直接有效的解法
把高基数字段(如 UUID 或哈希化的 _id)设为分片键,并启用哈希策略,MongoDB 就能靠散列值把写入均匀打到所有分片。
关键操作必须显式执行:
- 对已有集合重新分片:
sh.shardCollection("db.col", { "uuid_field": "hashed" }) - 如果坚持用
_id,推荐:sh.shardCollection("db.col", { "_id": "hashed" })—— MongoDB 会自动建哈希索引,无需预建 - 不能只在客户端生成随机
_id却还用{ _id: 1 }分片,路由仍按后者走
注意:哈希后点查(find({ uuid_field: "xxx" }))变快,但范围查(find({ uuid_field: { $gt: "a", $lt: "z" } }))必须广播到所有分片,聚合和 $sort 在该字段上基本不可用。
别只盯 chunk 分布,真实负载陷阱常被忽略
sh.status() 显示 chunk 均匀 ≠ 实际负载均衡。某分片可能 chunk 数正常,却因以下原因成为隐形热点:
- 该分片缺失关键查询索引,导致大量文档扫描,CPU 拉满
- 应用层高频读取固定几个
uuid_field值(比如登录态用户 ID),反复打同一分片 -
mongos缓存了错误路由信息,或配置服务器元数据延迟未同步
排查必须组合使用:db.currentOp({ secs_running: { $gt: 5 } }) 定位慢操作来源,mongostat -n 1 实时比对各分片 netIn/conn,再结合 db.col.getIndexes() 检查索引覆盖是否完整。
最易被跳过的一步:哈希分片后,所有涉及该字段的范围查询、排序、聚合都得重写逻辑或加冗余字段支撑,否则性能断崖下跌——这不是配置问题,是数据分布方式改变带来的必然代价。











