refinecollectionshardkey在生产中基本不可用,因其要求新字段必须在所有文档中存在且非空、不能为数组、集合不能有预拆分chunk,且执行后不迁移历史数据,仅更新refinedkey子字段,导致路由和查询仍按旧键执行。

MongoDB 分片键不能直接修改,所谓“变更”本质是重建集合 + 迁移数据 —— 5.0+ 的 reshardCollection 是唯一原生支持的在线方案,但限制极多,多数业务场景仍得绕道操作。
为什么 refineCollectionShardKey 在生产中基本不可用
这个命令在 5.0 中标记为实验性,不是功能缺失,而是设计上就拒绝常见业务需求:
- 新字段必须在所有文档中存在、非
null、非缺失(即不能是后期加的可选字段) - 新字段不能是数组,否则报错
Cannot refine shard key with array field - 集合不能有预拆分 chunk,否则触发
Refining shard key requires all chunks to be owned by the same shard - 执行后不迁移历史数据,
sh.status()输出里主shard key字段仍显示旧值,仅refinedKey子字段体现新键 —— 监控脚本若没读这个字段,会误判键已生效 - 后续对新字段的
$lookup或范围查询可能被路由跳过,因为 mongos 仍按旧键分发
reshardCollection 的真实可用边界
这是 5.0 引入的真正“重新分片”能力,但它不是万能补丁,而是重型手术:
- 必须满足
writeConcernMajorityJournalDefault: true,4.4 升级上来的集群常默认关着,需提前开启 - 若集合有唯一索引,新分片键必须保证全局唯一 —— 比如原键是
{tenant_id: 1},想改{tenant_id: 1, user_id: 1},那tenant_id重复的文档就会卡住整个操作 - 操作期间禁止
createIndex、drop、renameCollection等 12 类命令,任何误触都会导致重分片失败并残留中间状态 - 要求每个分片有至少
(collection_size + index_size) * 2 / shard_count的空闲空间 —— 一个 2TB 集合配 4 个分片,每个分片就得腾出 1.2TB,不是“够用”而是“硬性门槛” - Atlas Search 索引会在重分片后失效,必须手动重建,且重建期间搜索不可用
绕过原生命令的实操迁移路径
当 reshardCollection 因资源或约束卡住时,团队普遍采用“三步迁移法”,它可控、可验证、无停机:
- 先用
sh.splitAt()和sh.moveChunk()将现有 chunk 拆到最小粒度(例如按当前分片键值精确切分),确保 chunk 边界与未来新键前缀对齐 - 创建新集合,用目标分片键调用
sh.shardCollection(),并开启preSplit: true;注意复合索引必须把分片键字段放最前,否则查询无法下推 - 用
mongodump --query+mongorestore --maintainInsertionOrder --drop迁移;加--maintainInsertionOrder是关键,避免并发写入打乱 chunk 分布,导致后续均衡器疯狂搬块 - 切换应用写入前,用 change stream 捕获增量,双写比对最后 5 分钟数据,确认一致性
最容易被忽略的隐性成本
所有方案都绕不开这几个静默开销:
-
mongorestore会重写_id索引 —— 若你用自定义ObjectId或字符串_id,需确认新集合的索引类型和排序是否兼容 - 局部辅助索引必须逐个登录每个 shard 的 primary 执行
db.col.createIndex(),靠 mongos 下发会静默失败,且explain("executionStats")看不出某 shard 没走索引 - 分片键含哈希字段(如
{_id: "hashed"})时,几乎无法补救非_id查询 —— 因为哈希值不可控,建再多辅助索引也覆盖不了路由逻辑 - 迁移期间磁盘占用翻倍是常态,但更危险的是 I/O 压力:如果 chunk 迁移 + restore 同时发生,I/O 利用率冲到 90% 以上,moveChunk 会超时回滚,留下不一致 chunk











