预分片是秒杀场景下规避写入热点的唯一务实手段,需配合哈希分片键、显式sh.splitAt()预先切分chunk并均匀分布,且活动前停均衡器、验证chunk分布与数量。

秒杀场景下,MongoDB分片集群不能靠“等数据写入后再自动拆分”来扛住流量——那会直接压垮单个 shard,引发 hotspot 和迁移延迟雪崩。预分片(pre-splitting)是唯一能提前把数据打散、规避写入热点的务实手段。
为什么自动分片在秒杀中完全不可靠
默认块大小 64MB、触发拆分阈值为 51.2MB(0.8 × chunkSize),而秒杀请求常在几秒内涌入数十万文档。即使分片键合理,首个 chunk 仍会持续接收所有新写入,直到撑爆阈值才分裂——此时 mongos 路由压力、单 shard 的锁竞争、WiredTiger page latch 冲突已导致超时或拒绝服务。
- 自动分裂是被动响应,无法应对毫秒级突发写入
- 均衡器(balancer)迁移 chunk 是后台异步操作,迁移期间 chunk 不可写,加剧排队
- 秒杀 ID 若含时间前缀(如
"20260407_001"),范围分片下所有请求路由到同一 chunk,哈希分片虽缓解但无法消除首写集中
预分片必须配合哈希分片键 + 显式 sh.splitAt()
预分片不是“多建几个 shard”,而是预先将目标集合按预期写入规模切出足够多的初始 chunk,并均匀分布到各 shard。关键前提是:分片键必须支持哈希,且基数足够高(如用户 ID、订单号哈希值)。
- 先启用库和集合分片:
sh.enableSharding("seckill"),再用哈希方式指定键:sh.shardCollection("seckill.orders", { "user_id": "hashed" }) - 计算预估总写入量(如 100 万订单),结合平均文档大小(如 512B),得出需覆盖的哈希空间范围(
Long.MIN_VALUE到Long.MAX_VALUE) - 用
sh.splitAt()手动切出 chunk,例如:sh.splitAt("seckill.orders", { "user_id": NumberLong("-4611686018427387904") }),重复执行直至覆盖全哈希空间 - 用
sh.status()验证 chunk 数量与分布,确保各 shard 的 chunk 数差异 ≤ 2
预分片后仍要禁用均衡器并锁定 chunk 分布
秒杀活动期间,任何 chunk 迁移都是灾难——它会触发跨网络复制、元数据更新、短暂不可写。必须在活动开始前停掉均衡器,并防止自动平衡干扰。
- 停用均衡器:
sh.stopBalancer()(注意:该命令需在mongos上执行,非 config server) - 确认已停:
sh.getBalancerState()返回false,且sh.isBalancerRunning()返回false - 避免误启:检查
config.settings中balancer文档的stopped字段是否为true - 活动结束后再手动调用
sh.startBalancer(),并观察config.changelog确认迁移完成
真实部署中容易被忽略的三个硬约束
预分片不是配置完就一劳永逸。实际压测时常见失败点都卡在底层限制上:
- 每个 shard 最多支持约 32,000 个 chunk(受 WiredTiger cache 和元数据内存占用限制),预分片总数不能超过
shard_count × 32000 -
sh.splitAt()只接受精确的分片键值,不能传范围;若用复合哈希键(如{ "user_id": "hashed", "item_id": 1 }),则必须按哈希后值 + 范围组合构造 split 点,极易出错 - 预分片操作必须在集合为空时执行,一旦有数据写入,
sh.splitAt()会报错ChunkTooBig或静默失败











