分片与副本集
问题
MongoDB 的副本集(Replica Set)和分片(Sharding)是什么?它们分别解决什么问题?如何选择分片键?
答案
副本集(Replica Set)
副本集是 MongoDB 的 高可用 方案。一组 mongod 进程维护相同数据集的多个副本,提供数据冗余和自动故障转移。
副本集架构
| 角色 | 职责 | 数量 |
|---|---|---|
| Primary | 接收所有写操作,维护 oplog | 只有 1 个 |
| Secondary | 从 Primary 同步 oplog,可以处理读请求 | 1 个或多个 |
| Arbiter | 投票节点,不存储数据,参与选主投票 | 可选(节省资源) |
Oplog(操作日志)
Oplog 是 Primary 上的一个 固定大小的 capped collection(local.oplog.rs),记录所有数据变更操作。Secondary 通过持续拉取(tailing)Oplog 来同步数据。
// 查看 oplog 状态
db.getReplicationInfo();
// {
// logSizeMB: 1024, // oplog 大小
// usedMB: 256, // 已使用
// timeDiff: 86400, // 时间窗口(秒)
// tFirst: "...", // 最早记录时间
// tLast: "..." // 最新记录时间
// }
如果 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 读取的数据可能有 复制延迟(通常毫秒到秒级),不保证读到最新写入。如果需要读自己的写(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 如何保证高可用?
答案:
通过副本集实现:
- 数据冗余:同一数据在 Primary 和 Secondary 上各有一份
- 自动选主:Primary 故障后,在 10-12 秒内自动选出新 Primary
- 客户端自动重定向:驱动程序自动发现新 Primary 并重新连接
- 写关注(Write Concern
majority):确保数据写入多数节点后才确认 - 读关注(Read Concern
majority):确保读取多数节点确认的数据
Q2: 什么是分片键?如何选择?
答案:
分片键是决定文档分布到哪个分片的字段。选择原则:
- 高基数:值种类多(如 userId),避免低基数导致 Jumbo Chunk
- 写入分散:避免单调递增字段(如
_id、createdAt)做 Range 分片,否则写入集中在最后一个 Chunk - 查询友好:最常用的查询条件应包含分片键,避免广播查询
- 复合分片键:组合字段(如
{ tenantId: 1, createdAt: 1 })兼顾分布性和查询性
Q3: 副本集的选举需要多少节点投票?
答案:
需要 副本集成员总数的多数(majority)。具体为 Math.floor(N/2) + 1:
| 成员数 | 需要票数 | 容忍故障数 |
|---|---|---|
| 3 | 2 | 1 |
| 5 | 3 | 2 |
| 7 | 4 | 3 |
这就是为什么推荐 奇数个成员。偶数个成员(如 4 个)需要 3 票才能选主,容忍故障数和 3 个成员一样(都是 1),多了一个节点却没增加容错能力。
Q4: Hashed 分片和 Range 分片怎么选?
答案:
| 场景 | 推荐 |
|---|---|
| 写入密集,需要均匀分布 | Hashed |
| 按时间范围查询(如日志、订单) | Range(配合复合键避免热点) |
| 需要高效范围扫描 | Range |
| 分片键是单调递增(如 ObjectId) | Hashed(避免热点) |
Q5: 什么是 Jumbo Chunk?如何处理?
答案:
Jumbo Chunk 是指 无法被分裂 的超大 Chunk。原因是 Chunk 中所有文档的分片键值相同,无法找到分裂点。
这通常由 低基数分片键 导致(如 status 只有 "active"/"inactive" 两个值)。
处理方法:
- 避免低基数分片键(从根本上避免)
- 手动分裂或迁移(MongoDB 工具支持)
- 重新设计分片键(5.0+ 支持重新分片
reshardCollection)
Q6: 分片集群至少需要多少节点?
答案:
生产环境最小部署:
| 组件 | 数量 | 说明 |
|---|---|---|
| Config Server | 3 | 副本集,存储元数据 |
| Shard | 至少 2 × 3 = 6 | 每个 Shard 是 3 节点副本集 |
| Mongos | 至少 2 | 路由器,通常与应用部署在一起 |
| 最少总计 | 11 个节点 | — |
这就是为什么说「不要过早分片」—— 运维成本和硬件成本都很高。