{ timestamp: 1 } 必然导致单点写入,因其单调递增特性使所有新文档持续路由至最大值所在chunk(固定于最后一分片),而mongodb不拆分“正在追加”的chunk,均衡器也不干预写入热点。

时间戳作为分片键会导致所有新写入持续打在最后一个分片上,这不是配置失误,而是 MongoDB 范围分片机制的必然结果。
为什么 { timestamp: 1 } 必然引发单点写入
范围分片按分片键值大小切分 chunk,新文档总被路由到「当前最大值所在 chunk」。而 timestamp 单调递增,所有新写入都落在最大时间戳所在的 chunk —— 它始终位于最后一个分片,且不会自动拆分(MongoDB 不会拆分「正在追加」的 chunk)。
- 现象:用
sh.status()查看,某个分片 chunk 数极少但数据量占 80%+;mongostat显示该分片netIn和conn持续飙高,其余分片 idle - 均衡器不干预:它只迁移已有 chunk,不阻止新写入继续涌入热点分片
- 手动
sh.splitAt()无效:新 chunk 还是落在同一分片,除非立刻配合sh.moveChunk()
哈希分片能解决吗?{ timestamp: "hashed" } 为什么也不行
哈希分片确实能把值打散,但对 timestamp 效果极差——因为时间戳本身局部高度集中(比如一小时内写入的 timestamp 差值可能只有几秒),哈希后仍大概率映射到相邻 hash 值区间,最终落入少量 chunk。
- 本质问题:哈希函数输入熵太低,不是哈希算法不行,是原始值分布太“平滑”
-
{ timestamp: "hashed" }在实践中和范围分片几乎一样热,只是热点更难排查 - 真正有效的哈希分片键必须具备高基数、无时序规律,例如
user_id、UUID 字符串、或显式生成的shard_key字段
复合分片键能否缓解?{ deviceId: 1, timestamp: 1 } 的真实表现
这个组合在物联网类场景中常用,但它缓解的是「设备维度」的写入倾斜,不是时间维度——只要 deviceId 基数足够高(比如百万级),写入就能分散到多个 chunk;但每个设备自身的写入仍按时间顺序追加,单个 chunk 内部仍可能成为热点。
- 优势:查询
{ deviceId: "abc", timestamp: { $gt: T1, $lt: T2 } }可命中单分片,高效 - 风险:如果某几个
deviceId写入量远超其他(比如故障设备高频上报),它们仍会各自形成局部热点 - 必须确认:
deviceId基数是否真高?是否已建好{ deviceId: 1, timestamp: 1 }复合索引?否则查询会退化为广播
已经用了 timestamp 分片键,现在怎么办
停服重建集合是最干净的解法,但线上系统往往不可行;reshardCollection(5.0+)可在线变更分片键,但仍有明显代价:
- 执行期间读写延迟升高,主从延迟拉长,
db.currentOp()可能卡住大量迁移任务 - 目标分片磁盘空间必须充足,否则迁移中途失败,状态卡在
draining - 应用层需临时关闭
retryWrites: true,否则InterruptedDueToReplStateChange错误会反复重试,拖慢整个过程 - 变更后,原
timestamp上的范围查询全部变为广播查询,性能下降明显,务必提前评估
真正容易被忽略的是:即使 sh.status() 显示 chunk 分布均匀,CPU 或 IO 热点仍可能集中在某一分片——那往往是查询模式或缺失索引导致的读负载不均,和分片键本身无关。得靠 mongostat + db.currentOp({ "secs_running": { "$gt": 5 } }) 实时抓取,而不是只盯 chunk 数量。











