MongoDB 分片集群默认会根据 shard key 将数据分散到多个分片,但业务层面往往会对数据存放位置有硬性要求。比如多租户平台希望不同租户的数据物理隔离,跨境电商需要将欧盟用户的订单存放在欧洲机房,或者日志系统想把近期的热数据固定在 SSD 分片上。单纯依靠范围分片或哈希分片无法直接实现这类细粒度控制,而 shard zone 正是为此设计。它允许管理员给分片打上标签,再将分片键的某一个或几个连续区间绑定到相同标签,让 mongos 在写入和路由时优先把匹配区间内的文档发往带有对应标签的分片。

shard zone 的核心思想是在逻辑数据范围与物理分片之间建立稳定的映射关系。它不改变分片键的选择,也不影响文档在 chunk 中的存放规则,而是通过 chunk 的迁移约束来影响数据最终落在哪些分片上。例如一个订单集合按照 orderId 做范围分片,可以设定 orderId 从 A 到 M 的 chunk 只能存放在打上 SSD 标签的分片上,从 N 到 Z 的 chunk 只能存放在打上 HDD 标签的分片上。这样即使某个 HDD 分片负载很低,也不会接收 SSD 区间的任何 chunk,从而保证读写性能和数据合规。
shard zone 与普通范围分片的本质区别
范围分片会根据分片键的值把数据划分成连续的 chunk,这些 chunk 在初始阶段会均匀分布到各个分片上。不过在没有 zone 的情况下,balancer 的迁移目标是让各分片的 chunk 数量尽可能接近,并不关心数据内容或业务归属。也就是说,同一个租户的订单可能被拆散到多个分片,冷热数据也可能混合存放。这种方式虽然对吞吐量友好,但在需要数据本地化或硬件隔离时就会产生明显短板。
shard zone 在范围分片的基础上增加了一层位置约束。管理员先通过 sh.addShardTag 给分片打标签,再通过 sh.addTagRange 将某个分片键区间与该标签关联起来。MongoDB 会把这一区间对应的 chunk 视为属于该 zone,只有带有相同标签的分片才有资格存储这些 chunk。zone 之间互相独立,各 zone 内部仍然可以有多块分片并继续做负载均衡,但 zone 与 zone 之间的数据不会互相漂移。
下面是一段最简单的 zone 定义示例,它将订单集合中 orderId 小于 100000 的数据定向到带有 zone_a 标签的分片上:
// 给分片 shard0000 打上 zone_a 标签
sh.addShardTag("shard0000", "zone_a");
// 将 orderId 从最小值到 100000 的范围绑定到 zone_a
sh.addTagRange(
"test.orders",
{ orderId: MinKey },
{ orderId: 100000 },
"zone_a"
);
执行上述命令后,如果 test.orders 集合已经分片,那么 orderId 在 MinKey 到 100000 之间的 chunk 只会存储在 shard0000 或其他同样打有 zone_a 标签的分片上。需要注意,这里的分片键必须是支持范围比较的字段,比如数值或字符串。
配置 shard zone 的完整操作流程
在生产环境中引入 shard zone 通常需要按顺序完成几个步骤。第一步是先规划好标签体系,确定哪些分片属于哪个硬件组或地域组,并给这些分片添加标签。标签名称没有强制规范,但建议使用含义清晰的字符串,例如 ssd、hdd、region_eu、tenant_1001。第二步是对目标集合开启分片,选择合适的分片键并执行 sh.shardCollection。第三步才是添加 zone 范围,否则 sh.addTagRange 会因为集合尚未分片而报错。
下面展示一个完整的配置示例。假设集群有四个分片,其中两个使用 SSD,两个使用 HDD,我们要让用户表中用户 ID 前缀为 A 到 M 的数据落在 SSD 分片,剩余数据落在 HDD 分片:
// 1. 给四个分片打上硬件标签
sh.addShardTag("shard0000", "SSD");
sh.addShardTag("shard0001", "SSD");
sh.addShardTag("shard0002", "HDD");
sh.addShardTag("shard0003", "HDD");
// 2. 对 user 集合开启范围分片
sh.shardCollection("test.user", { userId: 1 });
// 3. 为两个区间分别绑定 zone 标签
sh.addTagRange(
"test.user",
{ userId: "A" },
{ userId: "M" },
"SSD"
);
sh.addTagRange(
"test.user",
{ userId: "M" },
{ userId: MaxKey },
"HDD"
);
代码中的范围边界遵循 MongoDB 的区间规则,左边是闭区间,右边是开区间。也就是说第一个 zone 覆盖 userId 大于等于 A 且小于 M 的所有文档,第二个 zone 覆盖 userId 大于等于 M 直到最大值的文档。如果两个 zone 的边界重叠,MongoDB 会直接返回错误,因此需要提前规划好每个区间的上下界。
如果后续业务调整需要修改已有 zone 的范围,可以使用 sh.updateZoneKeyRange 命令。它允许在不删除原有定义的情况下更新某个标签对应的分片键区间。例如想把 SSD 区间的上界从 M 改为 L,可以执行:
sh.updateZoneKeyRange(
"test.user",
{ userId: "A" },
{ userId: "M" },
{ userId: "A" },
{ userId: "L" }
);
需要注意的是,修改范围会改变 chunk 的逻辑归属,可能触发这些 chunk 从原来的分片迁移到新的目标分片。如果涉及的数据量很大,迁移过程会占用带宽和磁盘 IO,因此应尽量在业务低峰期执行,并提前通过 sh.status() 观察 chunk 数量和分布。
zone 对 chunk 迁移和负载均衡的约束
MongoDB 的数据均衡以 chunk 为基本单位,balancer 会周期性检查各个分片的 chunk 数量差异。在未配置 zone 时,balancer 会尽可能让每个分片持有数量相近的 chunk。但一旦集合上存在 zone,balancer 的迁移目标就不再是全局均衡,而是局部均衡。属于某个 zone 的 chunk 只能在带有对应标签的分片之间迁移,绝不能迁移到没有该标签的分片。
这种约束会带来一个直接结果:如果某个 zone 只有一个分片承载,那么该 zone 的所有 chunk 都会堆积在这一个分片上,即使该分片已经出现性能瓶颈,其他空闲分片也无法帮忙分担。因此生产环境中应为一个 zone 至少配置两个分片,这样既能保证高可用,又能在该 zone 内部做负载均衡。
另一个容易忽视的问题是配置 zone 之前已有数据的分布。假设集合在设置 zone 之前已经运行了一段时间,chunk 已经分散到所有分片。此时再添加 zone 范围,MongoDB 不会立刻把所有不符合 zone 约束的 chunk 全部搬走,而是由 balancer 在后续的均衡周期中逐步调整。这个调整过程可能持续数小时甚至数天,取决于 chunk 数量和迁移速度。可以通过 sh.status() 查看当前 balancer 的活动情况以及每个 zone 的分布状态。
下面是用 sh.status() 查看 zone 信息的输出示例,其中 Tag 列会显示每个 chunk 所属的 zone:
sh.status();
// 输出中会包含类似以下信息:
// Test.user
// shard key: { "userId" : 1 }
// unique: false
// balancing: true
// chunks:
// shard0000 12
// shard0001 11
// shard0002 13
// shard0003 12
// tag: SSD { "userId" : "A" } -->> { "userId" : "M" }
// tag: HDD { "userId" : "M" } -->> { "userId" : MaxKey }
从输出中可以看到两个 zone 的范围定义,以及各个分片当前实际持有的 chunk 数量。如果某个分片的 chunk 数量明显偏离预期,可以结合 balancer 日志进一步分析是否出现迁移失败或迁移积压。
生产环境使用 shard zone 的避坑与优化建议
第一点需要注意的是分片键的选择。zone 的核心是范围映射,因此分片键必须能够被明确地划分为连续区间。递增的数值、时间戳或具有字典序的字符串都比较适合。如果使用哈希分片键,虽然也能给整个键范围打上 zone 标签,但哈希后的键值无法按业务含义拆分,通常只能做整体绑定,灵活性大打折扣。因此在需要 zone 的场景下,尽量选择范围分片而不是哈希分片。
第二点是避免 zone 范围重叠。MongoDB 不允许同一个集合的多个 zone 在分片键范围上出现重叠,否则会抛出错误。在添加新的 zone 之前,最好先用 sh.status() 或 config.tags 集合查看已有标签范围。如果发现范围规划不合理,可以用 sh.removeTagRange 先移除旧范围再重新添加。例如:
// 移除原有的 SSD 标签范围
sh.removeTagRange(
"test.user",
{ userId: "A" },
{ userId: "M" },
"SSD"
);
// 添加新的、更精确的范围
sh.addTagRange(
"test.user",
{ userId: "A" },
{ userId: "G" },
"SSD"
);
第三点是谨慎对待 zone 变更带来的迁移成本。每调整一次范围,balancer 都可能要搬迁大量 chunk。建议在变更前先用 sh.disableBalancing("test.user") 暂停自动均衡,然后手动评估需要迁移的 chunk 数量。如果需要精准控制,可以使用 sh.moveChunk 将特定 chunk 迁移到目标分片,等数据分布稳定后再恢复自动均衡。恢复时使用 sh.enableBalancing("test.user")。
第四点是监控与告警。zone 配置完成后,应定期关注 balancer 是否正常运行、chunk 是否持续积压。可以通过 sh.isBalancerRunning() 检查均衡器状态,也可以通过 db.adminCommand({ balancerStatus: 1 }) 获取更详细的运行信息。当新增分片时,需要记得给新分片打上相应标签,否则它不会参与任何 zone 的数据承载,反而会导致原始 zone 的分片压力无法缓解。
总结来说,MongoDB shard zone 标签分片为数据放置策略提供了很强的控制力,特别适合多租户隔离、跨地域合规、冷热数据分层等场景。不过它也会限制 balancer 的全局均衡能力,因此需要结合分片键设计、标签规划、迁移监控和变更流程来综合使用。只有理解 zone 对 chunk 迁移的约束机制,才能在实际项目中避免数据倾斜和迁移风暴。

