Skip to content

MongoDB 复制集

  复制集(Replica Set)是 MongoDB 实现高可用性的核心机制。它通过多节点数据冗余和自动故障转移,确保在节点故障时服务不中断。复制集是生产环境部署 MongoDB 的最低要求,也是分片集群的基础组件。


1. 复制集架构

1.1 基本架构

  一个典型的 MongoDB 复制集由 3 个以上节点组成,包含一个主节点(Primary)和多个从节点(Secondary),可选地包含仲裁节点(Arbiter)。

┌─────────────────────────────────────────────────────────────────┐
│                       MongoDB 复制集                            │
│                                                                 │
│   ┌──────────────┐       ┌──────────────┐       ┌──────────────┐
│   │   Primary    │──────>│  Secondary   │──────>│  Secondary   │
│   │  (主节点)    │ Oplog │  (从节点1)   │ Oplog │  (从节点2)   │
│   │  读写操作    │       │  只读+复制   │       │  只读+复制   │
│   └──────────────┘       └──────────────┘       └──────────────┘
│         │                                                        
│         │ Heartbeat (心跳,每 2 秒)                            
│         ▼                                                        
│   ┌──────────────┐                                               
│   │   Arbiter    │                                               
│   │  (仲裁节点)   │                                               
│   │  仅投票,不存数据                                            
│   └──────────────┘                                               
│                                                                 │
│   ┌─────────────────────────────────────────────────────────────┐│
│   │              客户端驱动程序(自动发现拓扑)                  ││
│   │         mongodb://host1:27017,host2:27017,host3:27017       ││
│   └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘

1.2 节点角色

角色说明数量数据存储
Primary(主节点)接收所有写入操作,记录 oplog有且仅有一个存储完整数据
Secondary(从节点)从 Primary 复制 oplog,提供读服务1 个或多个存储完整数据
Arbiter(仲裁节点)参与选举投票,不存储数据0 个或 1 个(可选)不存储数据
Hidden(隐藏节点)特殊的 Secondary,不提供读服务0 个或多个存储完整数据
Delayed(延迟节点)特殊的 Secondary,数据延迟复制0 个或多个存储完整数据

隐藏节点(Hidden)

  • 优先级为 0,不会成为 Primary
  • 不接收客户端读请求(除非直接连接)
  • 常用于备份、分析、报表等离线任务

延迟节点(Delayed)

  • 故意延迟复制(如延迟 1 小时)
  • 可用于防止人为误操作(如误删数据后从延迟节点恢复)
  • 也是隐藏节点的一种

1.3 节点数量规则

建议说明
最少 3 个节点实现自动故障转移的最低要求
奇数个节点避免选举平票(3、5、7...)
最多 50 个节点MongoDB 复制集成员上限
最多 7 个投票节点参与选举的投票节点上限

为什么推荐奇数个节点

节点数投票节点多数票容错节点数
3321
4431
5532

  4 个节点和 3 个节点容错能力相同(都是 1 个),但 4 个节点多了一个节点成本。因此推荐奇数个节点。


2. Oplog(操作日志)

2.1 Oplog 概述

  Oplog(Operations Log)是复制集的核心组件,记录 Primary 上所有数据变更操作。Secondary 节点通过读取和重放 Primary 的 oplog 来同步数据。

Oplog 特性

特性说明
存储位置local 数据库的 oplog.rs 集合
集合类型上限集合(Capped Collection)
内容格式幂等操作(如 $set、$unset 而非直接赋值)
记录范围仅记录数据变更,不记录查询
按时间排序操作按时间戳自然排序存放

Oplog 文档示例

javascript
{
  "ts": Timestamp(1670000000, 1),    // 操作时间戳
  "t": NumberLong(1),                 // term(选举任期)
  "h": NumberLong("1234567890"),      // 操作哈希值
  "v": 2,                             // oplog 版本
  "op": "i",                          // 操作类型:i=insert, u=update, d=delete, c=command, n=noop
  "ns": "test.orders",                // 命名空间(数据库.集合)
  "ui": UUID("..."),                 // 集合 UUID
  "wall": ISODate("2026-01-15T..."),  // 墙上时钟时间
  "o": { ... },                       // 操作内容
  "o2": { ... }                       // 更新操作的查询条件
}

操作类型(op 字段)

op 值含义说明
"i"insert插入操作
"u"update更新操作
"d"delete删除操作
"c"command数据库命令(如 createIndex、dropDatabase)
"n"noop空操作(用于心跳、新节点选举通知)

2.2 Oplog 大小配置

javascript
// 查看 oplog 信息
rs.printReplicationInfo();

// 输出示例
// configured oplog size:   2048 MB
// log length start to end: 3600 secs (1.00 hrs)
// oplog first event time:  Mon Jan 15 2026 08:00:00 GMT+0800
// oplog last event time:   Mon Jan 15 2026 09:00:00 GMT+0800
// now:                     Mon Jan 15 2026 09:05:00 GMT+0800

Oplog 大小建议

场景推荐大小说明
开发环境1-2 GB小规模数据
一般生产5-10 GB可覆盖数小时的操作
大写入量20-50 GB高写入吞吐量场景
极大规模50+ GB超大集群,频繁维护

Oplog 大小的意义

  • Oplog 窗口(Oplog Window)= Oplog 大小 / 写入速率
  • 如果 Secondary 的复制延迟超过 Oplog 窗口,Secondary 将无法通过增量同步追上,需要全量同步(Initial Sync)
  • 典型 Oplog 窗口建议为 24 小时以上

2.3 Oplog 监控命令

javascript
// 查看 oplog 状态(主节点)
rs.printReplicationInfo();

// 查看从节点复制状态
rs.printSecondaryReplicationInfo();

// 直接查询 oplog
use local;
db.oplog.rs.find().sort({ $natural: -1 }).limit(5);

// 查询特定时间范围的 oplog
db.oplog.rs.find({
  ts: {
    $gte: Timestamp(1670000000, 0),
    $lte: Timestamp(1670086400, 0)
  }
});

// 查看 oplog 当前大小
db.oplog.rs.stats().maxSize;

3. 数据同步

3.1 初始同步(Initial Sync)

  当一个新的 Secondary 节点加入复制集时,需要进行初始同步以获取完整数据集。

初始同步过程

1. 克隆数据库
   ├── 选择一个同步源(通常是 Primary 或较新的 Secondary)
   ├── 遍历源节点的所有数据库和集合
   └── 复制所有文档到本地

2. 应用 oplog
   ├── 记录克隆开始时的 oplog 时间戳
   ├── 克隆完成后,应用从该时间戳到当前的所有 oplog
   └── 确保数据一致性

3. 构建索引
   ├── 为每个集合创建索引
   └── 索引构建期间的写入操作会排队

4. 切换到 Secondary 模式
   ├── 开始实时复制 oplog
   └── 节点状态变为 SECONDARY

初始同步耗时因素

因素影响
数据量大小数据越多,克隆时间越长
网络带宽带宽不足会显著延长同步时间
磁盘 I/O源节点读和目标节点写都会影响速度
索引数量索引越多,构建时间越长
oplog 窗口如果克隆期间 oplog 被覆盖,需要重新开始

初始同步优化

  • 使用文件系统快照(如 LVM snapshot)替代常规初始同步
  • 从一个空闲的 Secondary 进行同步,而非 Primary
  • 确保网络带宽充足

3.2 增量同步(Incremental Sync)

  初始同步完成后,Secondary 节点通过持续读取并重放 Primary 的 oplog 来保持数据同步。

增量同步过程

Primary 写入数据
  └── 写入 oplog
       └── Secondary 持续读取 oplog
            ├── 批量获取 oplog 条目
            ├── 多线程应用(默认 16 个线程)
            └── 更新本地 oplog 时间戳

复制延迟监控

javascript
// 查看复制延迟
rs.printSecondaryReplicationInfo();

// 通过 rs.status() 查看
rs.status().members.forEach(m => {
  print(`Node: ${m.name}, Lag: ${m.optimeDate - m.lastHeartbeat}`);
});

复制延迟原因

原因说明解决方案
网络延迟跨地域复制就近部署,使用网络优化
写入量过大Secondary 追赶不上增加 oplog 大小,优化写入
磁盘 I/O 慢Secondary 磁盘性能差升级磁盘(SSD),独立存储
索引构建构建大索引阻塞复制滚动构建索引
计算资源不足CPU/内存不够扩容或优化资源分配

4. 选举机制

4.1 Raft 协议

  MongoDB 的选举机制基于 Raft 共识算法,确保在 Primary 故障时能快速选出新的 Primary。

选举触发条件

触发场景说明
Primary 宕机心跳超时,Secondary 发起选举
Primary 网络隔离网络分区导致 Primary 无法与多数节点通信
手动降级执行 rs.stepDown() 命令
新节点加入新节点加入触发选举测试
配置变更修改复制集配置后

4.2 心跳(Heartbeat)

  复制集节点之间每 2 秒发送一次心跳(Heartbeat),用于检测节点存活状态。

心跳参数

参数默认值说明
heartbeatIntervalMillis2000心跳间隔(毫秒)
electionTimeoutMillis10000选举超时(毫秒)

心跳超时判断

Secondary 连续 10 秒未收到 Primary 心跳
  ├── 认为自己与 Primary 失去连接
  ├── 检查自己是否有资格成为 Primary(优先级 > 0、数据足够新)
  └── 发起选举

4.3 优先级(Priority)

  每个节点都有一个优先级(Priority),影响选举结果。优先级越高,越容易成为 Primary。

javascript
// 设置节点优先级
cfg = rs.conf();
cfg.members[0].priority = 2;  // 主节点优先级更高
cfg.members[1].priority = 1;  // 次要节点
cfg.members[2].priority = 0;  // 永远不会成为 Primary
rs.reconfig(cfg);

优先级规则

priority 值含义使用场景
0永远不会成为 Primary隐藏节点、延迟节点、备份节点
0.5低优先级备用 Primary(性能较差的节点)
1默认优先级普通节点
> 1高优先级期望成为 Primary 的节点(如硬件更好)
最大值1000配置上限

4.4 Term(任期)

  Term 是 Raft 协议中的概念,每次选举会增加 Term 值。Term 用于防止脑裂和确保数据一致性。

Term 的作用

  • 每次成功的选举,Term 加 1
  • 所有写入操作都携带当前 Term
  • 具有更高 Term 的节点不会被低 Term 节点覆盖
  • 写入冲突时,高 Term 的写入优先

查看 Term

javascript
// 查看当前 Term
rs.status().members.forEach(m => {
  print(`Node: ${m.name}, Term: ${m.optime && m.optime.t}`);
});

4.5 选举过程

1. 心跳超时(10 秒未收到 Primary 心跳)
   └── 检查是否有资格成为 Primary
       ├── priority > 0
       ├── 数据足够新(oplog 时间戳不能落后太多)
       └── 能与多数节点通信

2. 发起选举(Request Vote)
   ├── 向所有有投票权的节点发送投票请求
   └── 等待投票结果

3. 投票
   ├── 每个节点最多投一票
   ├── 检查候选节点的 oplog 是否比自己新
   ├── 检查当前 Term 是否匹配
   └── 投票给符合条件的候选节点

4. 选举结果
   ├── 获得多数票 → 成为 Primary
   ├── 未获得多数票 → 等待下一轮选举
   └── 平票 → 重新选举(随机延迟后)

选举时间估算

阶段时间说明
心跳超时~10 秒检测到 Primary 失效
选举过程~1-5 秒投票和协商
总计~12-15 秒从故障到新 Primary 就绪

5. 写关注(Write Concern)

5.1 写关注级别

  写关注定义了写入操作需要多少个节点确认才算成功。

javascript
{ w: <value>, j: <boolean>, wtimeout: <number> }
w 值含义容错延迟
0不等待确认最低
1Primary 确认故障时可能丢失
22 个节点确认1 个节点故障
"majority"大多数节点确认大多数-1 个节点故障
<tag>指定标签的节点确认取决于标签-

写关注与复制集大小

复制集节点数"majority" 含义所需节点数
33/2 + 1 = 22 个节点
55/2 + 1 = 33 个节点
77/2 + 1 = 44 个节点

5.2 Journal(j: true)

javascript
// 写入操作需要写入 journal 日志后才确认
db.orders.insertOne(
  { orderNo: "ORD001" },
  { writeConcern: { w: "majority", j: true } }
);

Journal 的作用

  • Journal 是 WiredTiger 的预写日志(WAL),每 50ms 刷盘一次
  • 开启 j: true 后,写入确认会等待 journal 刷盘
  • 即使发生断电,写入的数据也不会丢失
  • 但会增加写入延迟(约 50ms)

5.3 自定义写关注(Write Concern Tags)

javascript
// 配置节点标签
cfg = rs.conf();
cfg.members[0].tags = { dc: "sz", rack: "rack1" };
cfg.members[1].tags = { dc: "sz", rack: "rack2" };
cfg.members[2].tags = { dc: "gz", rack: "rack1" };
rs.reconfig(cfg);

// 定义自定义写关注
cfg.settings = {
  getLastErrorModes: {
    "twoDC": { dc: 2 },           // 至少两个不同数据中心确认
    "twoRack": { rack: 2 }        // 至少两个不同机架确认
  }
};
rs.reconfig(cfg);

// 使用自定义写关注
db.orders.insertOne(
  { orderNo: "ORD001" },
  { writeConcern: { w: "twoDC", j: true, wtimeout: 5000 } }
);

6. 读偏好(Read Preference)

6.1 读偏好模式

模式说明使用场景
primary只从 Primary 读取需要强一致性的读取
primaryPreferred优先 Primary,不可用时读 Secondary大多数读取场景
secondary只从 Secondary 读取报表、分析、备份
secondaryPreferred优先 Secondary,不可用时读 Primary降低 Primary 压力
nearest读取网络延迟最低的节点地理分布式部署

6.2 读偏好使用

javascript
// 连接字符串中设置
// mongodb://host1:27017,host2:27017,host3:27017/?readPreference=secondaryPreferred

// 单个查询设置
db.orders.find({ status: "pending" }).readPref("secondary");

// 指定标签
db.orders.find({ status: "pending" }).readPref(
  "secondary",
  [{ dc: "sz" }]  // 只从深圳数据中心的 Secondary 读取
);

6.3 读偏好与一致性

读偏好读关注一致性
primary"local"强一致性
primary"majority"强一致性
secondary"local"可能读到旧数据
secondary"majority"多数节点已确认的数据
secondary"available"任意可用数据

7. 读关注(Read Concern)

7.1 读关注级别

级别含义适用场景
"local"返回节点本地最新数据,不保证已复制默认级别,低延迟
"available"返回节点可用数据,可能不一致分片集群低延迟
"majority"返回已复制到大多数节点的数据需要读一致性的场景
"linearizable"线性一致读,保证读取最新数据金融、余额等强一致性场景
"snapshot"快照隔离,事务内使用多文档事务

7.2 读关注使用示例

javascript
// 多数读关注
db.orders.find({ status: "pending" }).readConcern("majority");

// 线性一致读(需要 w: "majority" 的写入配合)
db.accounts.findOne({ _id: "ACC001" }).readConcern("linearizable");

// 快照读(事务内)
const session = client.startSession();
session.startTransaction({
  readConcern: { level: "snapshot" },
  writeConcern: { w: "majority" }
});

7.3 读关注与写关注的组合

写入(w)读取(readConcern)保证
1"local"可能读到未提交的数据
"majority""local"可能读到稍旧的数据
"majority""majority"读到的数据不会回滚
"majority""linearizable"读到的数据一定是最新的

8. 自动故障转移

8.1 故障转移过程

1. Primary 宕机
   └── Secondary 心跳超时(10 秒)
       └── 检测到 Primary 不可用

2. 选举新 Primary
   ├── 有资格的 Secondary 发起选举
   ├── 获得多数票的节点成为新 Primary
   └── 新 Primary 通知所有节点

3. 客户端重连
   ├── 驱动程序检测到连接断开
   ├── 自动发现新的 Primary
   └── 重新发送未完成的写入操作(如果使用可重试写入)

4. 旧 Primary 恢复
   ├── 重新加入复制集
   ├── 作为 Secondary 追赶 oplog
   └── 复制落后时回滚冲突操作

8.2 回滚(Rollback)

  当旧 Primary 恢复后,如果它有一些操作未被复制到大多数节点,这些操作会被回滚。

回滚发生条件

  1. Primary 接收了写入操作
  2. 写入操作未复制到大多数节点(w: 1 或 w: 2 但不足 majority)
  3. Primary 宕机,新 Primary 选举
  4. 旧 Primary 恢复,发现自己的 oplog 与新 Primary 的 oplog 有分歧

回滚的文件

bash
# 回滚的数据保存在 rollback 目录
ls /data/db/rollback/
# rollback-&lt;timestamp&gt;-&lt;uuid&gt;.bson

回滚数据恢复

bash
# 使用 mongorestore 恢复回滚数据
mongorestore --db test --collection orders rollback/rollback-xxx.bson

避免回滚

  • 使用 w: "majority" 写关注:写入操作必须被大多数节点确认,不会回滚
  • 使用 { j: true } 写关注:即使 Primary 宕机,journal 中的数据不会丢失

8.3 可重试写入(Retryable Writes,3.6+)

javascript
// 连接字符串中启用可重试写入
// mongodb://host:27017/?retryWrites=true

// 驱动程序自动重试
// 故障转移期间,驱动程序会自动重试写入操作
// 确保写入操作不会因为临时故障而丢失

可重试写入的限制

  • 仅支持单文档写入操作(insertOne、updateOne、deleteOne、findOneAndXxx)
  • 不支持 bulkWrite 和 insertMany
  • 需要 session 支持
  • 重试有超时限制

9. 复制集部署

9.1 部署配置

yaml
# mongod.conf(复制集配置)
replication:
  replSetName: "rs0"           # 复制集名称
  oplogSizeMB: 5120            # oplog 大小(MB)

net:
  port: 27017
  bindIp: 0.0.0.0

storage:
  dbPath: /data/db
  journal:
    enabled: true
  wiredTiger:
    engineConfig:
      cacheSizeGB: 8

9.2 初始化复制集

javascript
// 连接到第一个节点
// mongo --host host1:27017

// 初始化复制集
rs.initiate({
  _id: "rs0",
  members: [
    { _id: 0, host: "host1:27017", priority: 2 },
    { _id: 1, host: "host2:27017", priority: 1 },
    { _id: 2, host: "host3:27017", priority: 1 }
  ]
});

// 如果是已有的单节点,需要先初始化
rs.initiate();

9.3 添加和移除节点

javascript
// 添加新节点
rs.add("host4:27017");

// 添加仲裁节点
rs.addArb("host5:27017");

// 移除节点
rs.remove("host4:27017");

// 重新配置
cfg = rs.conf();
cfg.members[3].priority = 0;  // 修改现有节点配置
rs.reconfig(cfg);

9.4 生产环境部署建议

建议说明
至少 3 个数据节点实现高可用最低要求
奇数个节点避免选举平票
节点分布在不同的物理机避免单点故障
不同机架/可用区提高容灾能力
使用专用网络减少网络延迟和丢包
配置监控和告警及时发现节点异常
定期备份即使有复制集也需要备份
配置 oplog 足够大避免 Secondary 需要全量同步
使用 DNS 或主机名而非 IP 地址,方便迁移

10. 复制集监控

10.1 rs.status()

javascript
rs.status();

// 关键字段解读
{
  set: "rs0",
  members: [
    {
      _id: 0,
      name: "host1:27017",
      health: 1,               // 0=宕机, 1=健康
      state: 1,                // 1=PRIMARY, 2=SECONDARY, 7=ARBITER
      stateStr: "PRIMARY",
      uptime: 86400,
      optime: { ts: Timestamp(...), t: NumberLong(1) },
      optimeDate: ISODate("..."),
      syncSourceHost: "",      // 同步源(Primary 为空)
      syncSourceId: -1,
      infoMessage: "",
      configVersion: 1,
      self: true               // 当前连接的节点
    },
    {
      _id: 1,
      name: "host2:27017",
      health: 1,
      state: 2,
      stateStr: "SECONDARY",
      uptime: 86400,
      optime: { ts: Timestamp(...), t: NumberLong(1) },
      optimeDate: ISODate("..."),
      lastHeartbeat: ISODate("..."),       // 上次心跳时间
      lastHeartbeatRecv: ISODate("..."),   // 上次收到心跳时间
      pingMs: NumberLong(0),               // 网络延迟(毫秒)
      syncSourceHost: "host1:27017",       // 同步源
      syncSourceId: 0,
      infoMessage: "",
      configVersion: 1
    }
  ]
}

节点状态(state 字段)

statestateStr说明
0STARTUP启动中
1PRIMARY主节点
2SECONDARY从节点
3RECOVERING恢复中
5STARTUP2初始同步完成,加载索引
6UNKNOWN未知状态
7ARBITER仲裁节点
8DOWN不可达
9ROLLBACK回滚中
10REMOVED已移除

10.2 rs.printReplicationInfo()

javascript
rs.printReplicationInfo();

// 输出示例
// configured oplog size:   5120 MB
// log length start to end: 86400 secs (24.00 hrs)
// oplog first event time:  Mon Jan 14 2026 10:00:00 GMT+0800
// oplog last event time:   Mon Jan 15 2026 10:00:00 GMT+0800
// now:                     Mon Jan 15 2026 10:05:00 GMT+0800

10.3 rs.printSecondaryReplicationInfo()

javascript
rs.printSecondaryReplicationInfo();

// 输出示例
// source: host2:27017
//   syncedTo: Mon Jan 15 2026 10:05:00 GMT+0800
//   0 secs (0 hrs) behind the primary

10.4 关键监控指标

指标获取方式告警阈值
复制延迟rs.printSecondaryReplicationInfo()> 10 秒
Oplog 窗口rs.printReplicationInfo()< 1 小时
节点健康状态rs.status().members[].healthhealth = 0
节点状态rs.status().members[].statestate != 1/2/7
网络延迟rs.status().members[].pingMs> 10ms(同机房)
复制集写关注监控写入确认时间超时

11. 常见问题与解决方案

11.1 脑裂(Split Brain)

问题:网络分区导致两个节点都认为自己是 Primary。

解决方案

  • MongoDB 使用 Raft 协议,只有获得多数票的节点才能成为 Primary
  • 网络分区后,被隔离的少数节点不会成为 Primary(因为无法获得多数票)
  • 确保节点数为奇数,避免平票

11.2 复制延迟过大

问题:Secondary 延迟严重,数据不一致。

解决方案

  • 检查网络延迟和带宽
  • 增加 oplog 大小
  • 优化 Secondary 的磁盘 I/O(使用 SSD)
  • 减少 Secondary 的读负载
  • 调整 Secondary 复制线程数

11.3 Oplog 被覆盖

问题:Secondary 离线时间过长,oplog 已被覆盖。

解决方案

  • 执行全量初始同步(Initial Sync)
  • 或使用文件系统快照恢复
  • 预防:设置足够大的 oplog

12. 总结

  复制集是 MongoDB 生产部署的基石,提供数据冗余、高可用和读扩展能力。

关键要点回顾

  1. 复制集架构:1 Primary + N Secondary + 可选 Arbiter,最小 3 节点
  2. Oplog:上限集合,记录所有数据变更,Secondary 通过重放 oplog 同步数据
  3. 选举:基于 Raft,心跳超时 10 秒,获得多数票成为 Primary
  4. 写关注:w: "majority" 保证数据不丢失,j: true 保证断电不丢数据
  5. 读偏好:按需选择 Primary 或 Secondary 读取,平衡一致性和性能
  6. 读关注:majority 保证读取的数据不会回滚,linearizable 保证最新
  7. 故障转移:自动检测、自动选举、自动恢复,约 12-15 秒完成
  8. 监控:使用 rs.status()、rs.printReplicationInfo() 等命令监控复制集状态