跳到主要内容

分片集群

MongoDB 的水平扩展方案,相当于内置的分库分表。这是 MongoDB 面试最核心的架构题。

三个组件​

应用
↓
┌─ mongos ─┐ 路由进程,无状态,可多个
│ ↓ │
Config Server (副本集) 存元数据:chunk 分布、分片列表
│ ↓ │
┌───┴──┐ ┌─┴────┐
│Shard1│ │Shard2│ ... 每个分片本身是一个副本集
└──────┘ └──────┘
组件职责要点
mongos路由、聚合各分片结果无状态,可水平扩展;通常和应用部署在一起或作为独立层
Config Server存储集群元数据3.4+ 必须是副本集(CSRS),挂了整个集群不可写入
Shard实际存数据生产环境每个分片都必须是副本集,否则单点故障丢数据

一个最小的生产分片集群需要:3 个 config server + 每个分片 3 个节点 × 至少 2 个分片 = 9 个 mongod 进程。这是「上分片的成本」的具体答案 —— 不要为了「以后可能要扩展」提前上分片。

Chunk​

数据按分片键的值域被划分成 chunk,chunk 是迁移的最小单位。

  • 默认大小 128MB(6.0 起;此前是 64MB)
  • chunk 超过阈值时自动分裂(split) —— 只改元数据,不搬数据,很轻
  • balancer 后台进程检测各分片的 chunk 数量差异,超过阈值就触发迁移(migration) —— 这个要实际搬数据,有 IO 开销

:::info 6.0 起的重要变化 MongoDB 6.0 把 balancer 的策略从「均衡 chunk 数量」改为「均衡数据大小」,并引入了自动合并 chunk。此前按 chunk 数量均衡会在数据分布不均时出问题(有的 chunk 满、有的几乎空,数量相同但体积差很远)。 :::

Jumbo chunk:如果某个 chunk 内的所有文档分片键值相同,它就无法再分裂(分裂点必须在不同的键值之间)。这种 chunk 会持续膨胀、超过限制后被标记为 jumbo 并不再参与迁移,导致数据永久倾斜。

Jumbo chunk 是分片键基数不足的直接后果,基本无法补救(只能改分片键,意味着重建集合)。

🔥 分片键选择​

最重要、最容易答错的问题。 分片键一旦选定,4.2 之前不可更改、4.4+ 才支持 refine(只能追加字段)、5.0+ 支持 reshard(但要重写全部数据,代价巨大)。

三个标准:

标准含义反例
高基数(Cardinality)不同取值足够多,否则无法继续分裂 → jumbo chunk用「性别」「状态」「国家」做分片键
分布均匀(Frequency)各个值的文档数量相近用「商家 ID」,头部商家数据撑爆一个分片
非单调递增(Monotonicity)否则所有新写入落到同一个分片用 ObjectId、时间戳、自增 ID 做范围分片

单调递增是最常见的错误。_id(ObjectId)前 4 字节是时间戳,所以范围分片时新数据永远落在 max key 所在的那个分片 —— 写入完全无法水平扩展,和单机没区别,还多了路由开销。

两种分片策略​

范围分片(Ranged)

按分片键值域划分区间。

  • ✅ 范围查询高效 —— 相邻的值在同一分片,$gte/$lt 只需查少数分片
  • ❌ 容易热点(分片键单调递增时)

哈希分片(Hashed)

对分片键算哈希再分片。

  • ✅ 写入均匀分布,没有热点
  • ❌ 范围查询要广播到所有分片(因为相邻的值哈希后分散了)
  • ❌ 不能用于多键(数组)字段

复合策略(4.4+) —— 复合分片键里可以让第一个字段哈希、后续字段范围,兼顾两者。也可以用 {tenantId: 1, timestamp: 1} 这种「业务维度 + 时间」的组合,既避免热点又保留一定的范围查询能力。

查询路由的两种情形​

定向查询(Targeted) —— 查询条件包含分片键,mongos 根据元数据直接定位到特定分片。这是正常情况。

广播查询(Broadcast / Scatter-Gather) —— 查询条件不含分片键,mongos 必须把请求发给所有分片,收集结果再合并返回。

:::warning 广播查询是分片集群最大的性能陷阱 后果:

  • 延迟由最慢的那个分片决定(长尾放大)
  • 每个分片都要执行一次查询,总资源消耗 = 单机 × 分片数
  • 加分片不会让这类查询变快,反而更慢

所以「分片键必须包含在高频查询条件里」不是建议而是硬要求。如果业务有多个查询维度(和 MySQL 分库分表的「订单按用户还是按商家」是同一个问题),解法也一样:冗余一张按另一维度分片的映射集合,或者把数据同步到 ES。 :::

排序和分页在分片集群下的代价:sort 需要各分片各自排序后由 mongos 归并;skip 无法下推 —— skip(10000).limit(10) 会让每个分片都返回前 10010 条给 mongos。深分页必须用游标式分页。

Zone / Tag Aware Sharding​

给分片打标签,把特定范围的数据固定到特定分片。两个实际用途:

  1. 数据合规 / 就近访问 —— 中国用户的数据必须留在国内机房的分片上
  2. 冷热分离 —— 近 3 个月的数据放 SSD 分片,历史数据放 HDD 分片

常见面试追问​

分片集群下的唯一索引有什么限制? 只有包含分片键前缀的索引才能是唯一的。因为唯一性检查需要全局协调,而 MongoDB 只在单个分片内保证。所以「分片集合上给 email 加唯一索引」做不到(除非 email 就是分片键)。变通办法是建一个以 email 为分片键的辅助集合来保证唯一性。

孤儿文档(orphaned documents)是什么? chunk 迁移过程中断导致源分片残留了已经迁走的文档。会让 count() 偏大、readConcern: available 读到重复数据。4.4+ 有 range deleter 自动清理,老版本要手动跑 cleanupOrphaned。

什么时候该上分片? 判断顺序和 MySQL 一样 —— 先用完所有垂直扩展和优化手段:

  1. 索引和查询优化
  2. 加内存(让工作集装进 WiredTiger cache,这往往是最有效的一步)
  3. 读从节点分摊读流量
  4. 垂直扩容(换更大的机器)
  5. 以上都不够才上分片

真正的信号:单机磁盘装不下、工作集远超单机内存、单机写入 IOPS 到顶。只是「数据量大」但工作集很小(比如日志表只查最近一天)不需要分片。

分片后能改分片键吗? 5.0+ 支持 reshardCollection,但它本质是后台重写整个集合,需要额外的磁盘空间和很长时间,期间有性能影响。4.4+ 的 refineCollectionShardKey 只能在现有分片键后面追加字段(用来提升基数),不能改前缀。所以结论仍然是一开始就要选对。

和 MySQL 分库分表的对比​

MongoDB 分片MySQL 分库分表
谁来做数据库内置需要中间件(ShardingSphere / Vitess)或自己写
应用改造几乎透明(连 mongos 即可)需要接入中间件、改 SQL
自动均衡balancer 自动迁移 chunk手动扩容 + 数据迁移
扩容加分片,数据自动重分布双写迁移、停机窗口
分片键变更5.0+ 可 reshard(代价大)几乎等于重做
跨分片事务4.2+ 支持需要 2PC / 本地消息表
跨分片聚合聚合管道原生支持中间件能力有限

这是 MongoDB 相对 MySQL 最硬的优势 —— 水平扩展是内置能力而非外挂方案。被问「为什么选 MongoDB」时,如果业务确实面临数据量持续增长,这个理由比「schema 灵活」更有说服力。