做过大规模数据导入的工程师,几乎都遇到过这样的场景:集群明明配置了 8 个 shard,结果一写入数据,流量全部集中在第一个节点上,其他节点全程闲置。问题不在于硬件,也不在于配置——仔细检查后发现,sh.status() 里 chunks 只有一行,全部落在 shard0000 上。这意味着预分片没有到位,最佳窗口期已经错过。

如果不提前做预分片,千万级的数据导入基本会卡死在第一个 shard 上——这并非配置问题,而是 MongoDB 的 chunk 初始化机制决定的。
为什么必须在写入前执行 sh.splitAt()
一旦集合里有了任何文档,sh.splitAt() 就可能失败或被 balancer 中断。MongoDB 不允许对已有数据的 chunk 边界进行“插队式”拆分;它只接受在空集合上预先规划好 chunk 范围。如果你发现 sh.status() 中 chunks 行只显示 1 个、并且全部集中在 shard0000,说明已经错过了最佳窗口期。
- 检查方法:连接 mongos 后运行
sh.status(),观察chunks行是否为 1,以及balancer状态是否为OK(不能是Running或Stopping) - 补救成本很高:已有数据时强行执行 split + moveChunk 容易触发迁移冲突,而且无法保证后续写入能立即分散
- 哈希分片键(例如
{_id: "hashed"})比范围分片更适合预分片,因为边界值可以精确计算,不会产生热点偏移风险
如何用 sh.splitAt() 计算并切出 N 个初始 chunk
以 8 个 shard 为例,哈希空间是 64 位有符号整数(-2^63 到 2^63-1),总跨度为 2^64。每个 chunk 的宽度 = 2^64 / 8 = 0x2000000000000000,切点从 0x2000000000000000 开始,每次加上这个值,直到 0xE000000000000000(共 7 个切点)。
- 用 Python 快速生成切点(注意使用
NumberLong格式):for i in range(1, 8): print(hex(i * 0x2000000000000000))
- 逐条执行:
sh.splitAt("db.coll", {_id: NumberLong("0x2000000000000000")}) - 执行完毕后立即用
sh.status()验证:确认chunks总数为 8,且每个 shard 下至少有 1 个 chunk(不能全部堆积在同一个 shard)
只有 bulkWrite + unordered 才能让预分片真正生效
预分片只是铺好了道路,如果客户端仍然串行发送写请求,mongos 仍有可能把所有请求都打到同一个 shard 上。必须配合无序批量写入,才能触发 mongos 并行路由。
- 驱动层设置:
ordered: false(Node.js、PyMongo、Java Driver 均支持) - 命令行工具不适合:
mongoimport默认采用有序写入,并且无法控制ordered参数;它底层调用的是单文档插入,不是bulkWrite - 性能差异非常明显:实测同样 500 万文档,
bulkWrite+ordered: false比mongoimport快 3–5 倍,CPU 利用率在所有 shard 上均匀分布 - 错误处理需要自行兜底:无序模式下某条失败不会影响其余,但你需要检查返回结果中的
writeErrors字段
导入后是否需要 touch 数据预热
预热对于首次查询延迟敏感的场景有帮助,但对纯写入优化无效。如果你刚导入完就立刻运行大量聚合查询或索引构建,db.runCommand({touch: "coll", data: true, index: true}) 能减少磁盘 IO 颠簸。
- 只对高频访问的集合做,不要全库 touch;内存不足时老数据会被挤出,反而增加 page fault
- 确认效果:执行后查看返回的
millis字段,data和indexes都应当有毫秒级耗时,否则说明文件没有成功加载 - touch 不解决写入瓶颈,只是缓解后续读压力;写入卡顿的根源始终在 chunk 分布和写入并发策略
最容易被忽略的一点是:预分片的切点必须严格按照哈希空间等分,少一个切点,就少一个初始 chunk,剩余数据仍然会堆积在最后一个未 split 的 chunk 上——哪怕你算错了 1 个 bit,整个预分片方案就失效了。
