跳到主要内容

分片与副本集

问题

MongoDB 的副本集(Replica Set)和分片(Sharding)是什么?它们分别解决什么问题?如何选择分片键?

答案

副本集(Replica Set)

副本集是 MongoDB 的 高可用 方案。一组 mongod 进程维护相同数据集的多个副本,提供数据冗余和自动故障转移。

副本集架构

角色职责数量
Primary接收所有写操作,维护 oplog只有 1 个
Secondary从 Primary 同步 oplog,可以处理读请求1 个或多个
Arbiter投票节点,不存储数据,参与选主投票可选(节省资源)

Oplog(操作日志)

Oplog 是 Primary 上的一个 固定大小的 capped collectionlocal.oplog.rs),记录所有数据变更操作。Secondary 通过持续拉取(tailing)Oplog 来同步数据。

// 查看 oplog 状态
db.getReplicationInfo();
// {
// logSizeMB: 1024, // oplog 大小
// usedMB: 256, // 已使用
// timeDiff: 86400, // 时间窗口(秒)
// tFirst: "...", // 最早记录时间
// tLast: "..." // 最新记录时间
// }
Oplog 窗口

如果 Secondary 离线时间超过 oplog 的时间窗口,就需要执行 全量同步(Initial Sync),代价很高。生产环境应确保 oplog 足够大。

选主机制

当 Primary 不可用时(心跳超时,默认 10 秒),副本集自动发起 选举

选举条件

  • 候选节点的 oplog 时间戳最新
  • Priority 值最高(默认 1,设为 0 的节点不能当选)
  • 获得副本集中 多数成员 的投票

为什么推荐奇数个成员:保证选举时能获得多数票。3 个成员容忍 1 个故障,5 个成员容忍 2 个故障。

读偏好(Read Preference)

模式描述适用场景
primary只从 Primary 读(默认)强一致性要求
primaryPreferred优先 Primary,不可用时读 Secondary较强一致性
secondary只从 Secondary 读读写分离,可接受延迟
secondaryPreferred优先 Secondary读写分离
nearest最低延迟节点地理分布式部署
从 Secondary 读取的一致性

从 Secondary 读取的数据可能有 复制延迟(通常毫秒到秒级),不保证读到最新写入。如果需要读自己的写(Read-Your-Writes),应使用 primary 或配合 readConcern: "majority"writeConcern: "majority"

分片(Sharding)

分片是 MongoDB 的 水平扩展 方案。当单个副本集无法承载数据量或吞吐量时,将数据分布到多个分片上。

分片集群架构

组件职责
Mongos查询路由器,将请求路由到正确的分片,合并多分片结果
Config Server存储集群元数据(分片信息、Chunk 分布、路由表)
Shard存储实际数据,每个 Shard 是一个副本集

分片键(Shard Key)

分片键决定了数据如何分布到各个分片上,是分片设计中 最关键的决策。分片键一旦选定,无法更改(5.0 之前)。

分片策略
策略说明优势劣势
Range 分片按分片键的值范围分布范围查询高效写入热点(递增键)
Hashed 分片对分片键值取哈希写入均匀分布范围查询需广播所有分片
Zone 分片将数据绑定到特定区域地理分布、多租户配置复杂
// Range 分片
sh.shardCollection("mydb.orders", { createdAt: 1 });

// Hashed 分片
sh.shardCollection("mydb.users", { userId: "hashed" });

// Zone 分片(地理分区)
sh.addShardToZone("shard-us", "US");
sh.addShardToZone("shard-eu", "EU");
sh.updateZoneKeyRange("mydb.users", { region: "US" }, { region: "US\xff" }, "US");
分片键选择原则

好的分片键应满足以下 三个特性

分片键示例评价
{ _id: "hashed" }✅ 写入均匀,但范围查询需广播
{ userId: 1, createdAt: 1 }✅ 复合键兼顾查询和分布
{ createdAt: 1 }❌ 单调递增,最新数据全写入最后一个 Chunk
{ status: 1 }❌ 基数太低(活跃/非活跃),分布极不均
{ region: 1, userId: 1 }✅ 可配合 Zone 分片做地理分区
分片键选错的代价
  • MongoId/时间戳等单调递增的字段做 Range 分片 → 所有写入都路由到同一个 Chunk(即一个分片),形成写入热点
  • 低基数字段→ 导致 Jumbo Chunk(超大块无法拆分和迁移)
  • 选错后无法更改(5.0 前),需要重建集合

Chunk 与 Balancer

数据被分成若干 Chunk(默认 128MB)。每个 Chunk 包含一段连续的分片键范围。

  • Chunk 分裂:当 Chunk 超过阈值,自动一分为二
  • Balancer:后台进程,自动在分片间迁移 Chunk,保持各分片数据均衡

查询路由

Mongos 根据查询条件和分片键决定路由策略:

查询类型路由方式性能
包含完整分片键定向查询(Target Query)✅ 最快
包含分片键前缀定向到部分分片✅ 较快
不包含分片键广播查询(Scatter-Gather)⚠️ 较慢

副本集 vs 分片:选择时机

维度副本集分片集群
目的高可用、数据冗余水平扩展、容量扩展
数据量单机可承载单机已无法承载
吞吐量单机能满足写入需求写入/读取需要分散
运维复杂度高(多组件)
成本高(至少 9 个节点)
何时使用绝大多数场景的首选数据量/QPS 超过单副本集极限
重要原则

先用副本集,真正遇到瓶颈再考虑分片。 多数 MongoDB 项目用一个副本集就能满足需求。分片引入了大量运维复杂度,不要过早分片。


常见面试问题

Q1: MongoDB 如何保证高可用?

答案

通过副本集实现:

  1. 数据冗余:同一数据在 Primary 和 Secondary 上各有一份
  2. 自动选主:Primary 故障后,在 10-12 秒内自动选出新 Primary
  3. 客户端自动重定向:驱动程序自动发现新 Primary 并重新连接
  4. 写关注(Write Concern majority):确保数据写入多数节点后才确认
  5. 读关注(Read Concern majority):确保读取多数节点确认的数据

Q2: 什么是分片键?如何选择?

答案

分片键是决定文档分布到哪个分片的字段。选择原则:

  1. 高基数:值种类多(如 userId),避免低基数导致 Jumbo Chunk
  2. 写入分散:避免单调递增字段(如 _idcreatedAt)做 Range 分片,否则写入集中在最后一个 Chunk
  3. 查询友好:最常用的查询条件应包含分片键,避免广播查询
  4. 复合分片键:组合字段(如 { tenantId: 1, createdAt: 1 })兼顾分布性和查询性

Q3: 副本集的选举需要多少节点投票?

答案

需要 副本集成员总数的多数(majority)。具体为 Math.floor(N/2) + 1

成员数需要票数容忍故障数
321
532
743

这就是为什么推荐 奇数个成员。偶数个成员(如 4 个)需要 3 票才能选主,容忍故障数和 3 个成员一样(都是 1),多了一个节点却没增加容错能力。

Q4: Hashed 分片和 Range 分片怎么选?

答案

场景推荐
写入密集,需要均匀分布Hashed
按时间范围查询(如日志、订单)Range(配合复合键避免热点)
需要高效范围扫描Range
分片键是单调递增(如 ObjectId)Hashed(避免热点)

Q5: 什么是 Jumbo Chunk?如何处理?

答案

Jumbo Chunk 是指 无法被分裂 的超大 Chunk。原因是 Chunk 中所有文档的分片键值相同,无法找到分裂点。

这通常由 低基数分片键 导致(如 status 只有 "active"/"inactive" 两个值)。

处理方法:

  1. 避免低基数分片键(从根本上避免)
  2. 手动分裂或迁移(MongoDB 工具支持)
  3. 重新设计分片键(5.0+ 支持重新分片 reshardCollection

Q6: 分片集群至少需要多少节点?

答案

生产环境最小部署:

组件数量说明
Config Server3副本集,存储元数据
Shard至少 2 × 3 = 6每个 Shard 是 3 节点副本集
Mongos至少 2路由器,通常与应用部署在一起
最少总计11 个节点

这就是为什么说「不要过早分片」—— 运维成本和硬件成本都很高。

相关链接