05. MongoDB 复制集与高可用架构

MongoDB ReplicaSet: Primary/Secondary 选举机制、读写分离、仲裁节点与自动故障转移

MongoDB 复制集(Replica Set)由一组 mongod 实例组成,通过自动故障转移和数据冗余机制,为业务系统提供高可用性和读写扩展能力。在实际生产环境中,单节点部署不仅存在单点故障风险,还无法满足持续增长的数据读取压力。本文将从原理到实践,系统性地拆解复制集的架构设计、运行机制与运维要点,并给出可直接落地的配置示例,帮助开发者和 DBA 快速掌握 MongoDB 高可用方案的核心技术。

1. 复制集架构

1.1 基本组成与角色划分

MongoDB 复制集的核心由三类节点构成:Primary 主节点、Secondary 从节点和 Arbiter 仲裁节点。只有 Primary 节点能接收写入请求,所有写操作先写入 Primary 的 oplog,再通过异步复制传播到各个 Secondary 节点。Secondary 节点持续拉取 oplog 并重放操作,以保持自身数据与 Primary 的最终一致性。

        ┌─────────────────┐
        │    Primary      │ ← 唯一可写节点,处理所有写入与默认读取
        │   (priority:1)  │     维护 oplog,驱动数据复制流程
        └────────┬────────┘
                 │ 写入 + Oplog 同步
        ┌────────┴────────┐
        │                 │
┌───────▼───────┐   ┌────▼──────────┐
│  Secondary 1  │   │  Secondary 2  │ ← 只读节点,异步复制 oplog
│(priority:0.5) │   │(priority:0.5) │     可承担读请求或备份任务
└───────────────┘   └───────────────┘

可选: Arbiter(仲裁节点,无数据,仅参与投票打破平局)

复制集核心规则:

  • 当 Primary 因硬件故障、网络中断或主动维护而下线时,存活的 Secondary 节点会自动发起选举,投票产生新的 Primary,整个过程无需人工干预。
  • 选举成功的必要条件是存活节点数超过总节点数的半数,即多数派(majority)存活。对于三节点复制集,至少需要两个节点在线才能选出新主节点。
  • 默认的写行为(writeConcern)仅要求 Primary 写入成功即返回客户端,这种方式延迟最低,但在极端场景下可能丢失最近写入的数据。业务对数据一致性要求较高时,应显式配置 w: “majority”。

1.2 数据复制流程

MongoDB 的复制不是基于传统的主从 binlog 轮询,而是基于本地 oplog(操作日志)的追加重放机制。具体流程如下:客户端向 Primary 发起写操作,Primary 将数据变更先写入 WiredTiger 存储引擎,同时将同一操作记录到 local.oplog.rs 这个定容集合中。Secondary 节点通过一个名为同步源(sync source)的长连接,持续从上游拉取 oplog 条目,并在本地按顺序重放。这种 Pull 模型比传统的 Push 模型更具弹性,Secondary 可以根据自身负载自主调节同步速率。

// 连接到 Primary,查看当前复制集拓扑和同步源分配
rs.status().members.forEach(m => {
    print(`${m.name}: ${m.stateStr}, syncSource: ${m.syncSource || 'N/A'}`);
});

2. 部署配置

2.1 初始化复制集

在生产环境首次部署时,需要逐一启动 mongod 实例,然后登入任意节点的 mongo shell 执行初始化命令。初始化时指定的 _id 即为复制集名称,后续所有节点加入时都必须使用相同的名称。

// 假设三个节点分别部署在 mongo1、mongo2、mongo3
// 每个节点的 mongod 启动参数应包含 --replSet rs0

rs.initiate({
    _id: "rs0",
    members: [
        { _id: 0, host: "mongo1:27017", priority: 2 },
        { _id: 1, host: "mongo2:27017", priority: 1 },
        { _id: 2, host: "mongo3:27017", priority: 1, arbiterOnly: false }
    ]
});

// 初始化完成后查看各节点状态
rs.status();

// 查看并验证当前配置
rs.conf();

priority 参数决定了节点在选举中的竞争力。数值越高,越容易当选为主节点。Arbiter 节点应显式设置 arbiterOnly: true,且不能承载实际数据。

// 添加 Arbiter 仲裁节点的示例
rs.addArb("mongo4:27017");

// 验证 arbiter 是否加入成功
rs.status().members.filter(m => m.stateStr === "ARBITER");

2.2 读写分离

MongoDB 副本集天然支持读写分离。业务层通过 readPreference 参数指定读请求的路由策略,从而将报表查询、统计分析等读密集型任务分流到 Secondary 节点,降低 Primary 的负载压力。

// 通过连接串指定 readPreference
const uri = "mongodb://user:pass@mongo1:27017,mongo2:27017,mongo3:27017/mydb?replicaSet=rs0&readPreference=secondaryPreferred";

// Node.js 驱动示例:使用 secondaryPreferred
const { MongoClient } = require('mongodb');
const client = new MongoClient(uri);

async function readFromSecondary() {
    const db = client.db("mydb");
    const coll = db.collection("logs");
    const docs = await coll.find({}).toArray();
    console.log(`从 Secondary 读取 ${docs.length} 条文档`);
    await client.close();
}

readFromSecondary();

readPreference 的五种模式对比:

模式路由目标适用场景
primary仅 Primary强一致性读取,如订单查询、账户余额
primaryPreferred优先 Primary,不可用时降级到 Secondary兼顾一致性和可用性
secondary仅 Secondary容忍延迟的报表、大数据分析
secondaryPreferred优先 Secondary,无可用时读 Primary读多写少的最常用方案
nearest网络延迟最低的任意节点多地部署时降低跨机房延迟

2.3 Write Concern

writeConcern 控制写操作在返回客户端之前需要满足的确认级别,是数据持久性与写入延迟之间的权衡杠杆。

// 多数派确认写入:保证数据被复制到大多数节点后才返回成功
db.orders.insertOne(
    { userId: "U001", amount: 199.0, status: "paid", createdAt: new Date() },
    {
        writeConcern: {
            w: "majority",
            j: true,
            wtimeout: 5000
        }
    }
);

// 写入确认级别对照表:
// w: 0   → 发后即忘(fire-and-forget),不等待任何确认,吞吐量最高但可能丢数据
// w: 1   → 仅 Primary 确认,默认行为,折中方案
// w: "majority" → 大多数节点持久化确认,数据最安全但延迟最大
// w: 3   → 至少 3 个节点确认,适用于 5 节点以上的复制集

// Read Concern 配合示例:读取已经被多数派确认的数据
const client = new MongoClient(uri, {
    readConcernLevel: "majority"
});

3. 选举机制与故障转移

3.1 Raft-like 选举流程

MongoDB 的选举算法借鉴了 Raft 共识协议的部分思想,但针对自身特点做了调整。当 Secondary 节点通过心跳检测发现 Primary 超过 electionTimeoutMillis(默认 10 秒)未响应时,该节点会递增自身任期号(term),并向其他节点发送选举请求(vote request)。收到请求的节点会检查以下条件:请求节点的数据是否最新(比较 optime 时间戳)、自身是否已在当前任期投过票、请求节点的 priority 是否不低于自己。只有所有条件满足时,才会投赞成票。当某节点获得超过半数(strict majority)的投票时,即当选为新的 Primary 并立即开始接受写入。

// 查看选举超时配置(单位毫秒)
rs.conf().settings.electionTimeoutMillis;

// 调整心跳超时(生产环境不建议频繁修改)
rs.reconfig({
    ...rs.conf(),
    settings: {
        ...rs.conf().settings,
        electionTimeoutMillis: 5000,
        heartbeatIntervalMillis: 2000
    }
});

3.2 脑裂防护

网络分区(Network Partition)是分布式系统中最棘手的场景之一。假设旧 Primary 与多数 Secondary 之间出现网络中断,旧的 Primary 仍然认为自己是主节点并继续处理写入;与此同时,Majority 侧的 Secondary 会选举出新 Primary。当网络恢复后,旧 Primary 尝试与心跳通信,会发现自己的任期号落后于新 Primary,从而自动降级为 Secondary,并将分区期间产生的未同步数据回滚(rollback)。这个机制从根本上杜绝了双主(脑裂)导致的数据冲突。

// 模拟场景验证:观察旧 Primary 降级过程
// 在旧 Primary 节点执行(网络恢复后)
rs.status();

// 若看到 stateStr 从 PRIMARY 变为 SECONDARY,即说明降级成功
// 检查是否有需要手动处理的回滚数据
const rollbackDir = "/var/lib/mongodb/rollback";
db.adminCommand({ getParameter: 1, rollbackDir: 1 });

4. Oplog 机制与管理

4.1 Oplog 工作原理

Oplog(Operations Log)是 MongoDB 复制的核心基础设施,本质上是 local 数据库中的一个定容集合(capped collection)。所有导致数据变更的写操作(insert、update、delete)都会被 Primary 序列化后追加到 oplog 中。每条 oplog 条目都包含 ts(时间戳)、op(操作类型)、ns(命名空间)和 o/o2(操作对象)等字段。Secondary 节点通过 tailing oplog 的方式持续消费这些变更记录,保持数据同步。

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

// 输出示例:
// configured oplog size:   5120MB
// log length start to end: 720secs (0.2hrs)
// oplog first event time:  Thu Sep 10 2026 09:31:05 GMT+0800
// oplog last event time:   Thu Sep 10 2026 09:43:05 GMT+0800
// now:                     Thu Sep 10 2026 09:43:06 GMT+0800

4.2 Oplog 大小计算与配置

Oplog 的容量直接决定了复制集的容错时间窗口。如果 Secondary 宕机或网络中断的时长超过了 oplog 的覆盖范围(oplog window),该节点就必须执行全量同步(initial sync),这对大容量数据库而言是灾难性的。因此,合理规划 oplog 大小是复制集运维的首要任务。

计算公式:Oplog 大小应足以容纳业务高峰期间完成一次完整同步所需的数据量。一个经验法则是:oplog 窗口应至少覆盖备用节点恢复所需的最大时间(含网络恢复、服务重启、磁盘重建)。

// 查看各 Secondary 的同步延迟
rs.printSlaveReplicationInfo();

// 查看 oplog 当前实际大小(单位字节)
db.oplog.rs.stats().maxSize;

// 在线调整 oplog 大小(仅 Secondary 可安全操作,Primary 需先 stepDown)
// 1. 停止 mongod 实例
// 2. 以单节点模式启动
mongod --dbpath /var/lib/mongodb --replSet rs0 --port 27018

// 3. 修改 oplog 大小为 20GB
use local
db.adminCommand({
    replSetResizeOplog: 1,
    size: 20000  // 单位 MB
});

// 4. 验证新大小
db.oplog.rs.stats().maxSize;

// 5. 重启回复制集模式

4.3 Oplog 监控与告警

生产环境中应将以下指标接入监控告警体系:

// 获取 Secondary 同步延迟(秒)
rs.status().members.forEach(m => {
    if (m.state === 2) {
        const lagSec = Math.floor((new Date() - m.optimeDate) / 1000);
        print(`节点 ${m.name} 延迟: ${lagSec} 秒`);
    }
});

// 计算 oplog 窗口剩余时间
db.getReplicationInfo();

建议的告警阈值:Secondary 延迟超过 10 秒触发提醒,超过 oplog 窗口的百分之五十则触发紧急告警。

5. 延迟节点与隐藏节点

5.1 Hidden 隐藏节点

Hidden 节点是 Secondary 的一种特殊角色,它参与数据复制和投票选举,但对外不可见,Driver 不会将任何读请求路由给它。Hidden 节点常用于托管数据备份、离线统计任务,或者作为 delayed secondary 的载体,避免这些重负载任务影响在线业务的响应时间。

// 将现有节点配置为 Hidden
cfg = rs.conf();
cfg.members[2].hidden = true;
cfg.members[2].priority = 0;      // Hidden 节点 priority 必须为 0
cfg.members[2].votes = 1;         // 保留投票权
rs.reconfig(cfg);

// 验证隐藏节点不接收读请求
rs.status().members[2].stateStr;  // 应为 SECONDARY,但 Driver 不会路由到它

5.2 Delayed Secondary 延迟节点

延迟节点故意滞后于 Primary 一个固定时间窗口,用于提供 “时间机器” 式的数据恢复能力。当误操作(如误删集合、错误更新)发生后,可以从延迟节点中导出受影响时间段之前的数据快照,然后恢复到主库,避免全量从备份还原的漫长等待。

// 配置延迟为 1 小时(3600 秒)
cfg = rs.conf();
cfg.members[2].slaveDelay = 3600;  // 单位秒
cfg.members[2].priority = 0;       // 延迟节点不能竞选 Primary
cfg.members[2].hidden = true;      // 通常设为 hidden 避免误读旧数据
rs.reconfig(cfg);

// 从延迟节点恢复误删数据示例
// 假设 primary 上的 orders 集合在 14:00 被误删
// 延迟节点 (slaveDelay=1h) 在 15:00 仍保留 14:00 之前的数据快照
// 连接到延迟节点(直接连接,不通过 replicaSet)
mongo --host delayed-node:27017

use mydb;
db.orders.find({ createdAt: { $lt: new Date("2026-09-10T14:00:00Z") } });

// 导出为 bson 后导入到 Primary
// mongodump --host delayed-node:27017 --db mydb --collection orders --query '{createdAt: {$lt: ...}}'
// mongorestore --host mongo1:27017 --db mydb orders.bson

6. 副本集运维实战

6.1 成员的增删改

生产环境中经常需要扩容复制集、替换故障节点或下线旧实例。MongoDB 提供了原生的在线操作接口,可以避免停机。

// 新增成员(前提是新节点已正确启动 mongod 并配置 --replSet rs0)
rs.add({ host: "mongo4:27017", priority: 1, votes: 1 });

// 验证新节点是否同步完成
rs.status().members.find(m => m.name === "mongo4:27017");

// 移除成员(先确认该节点不是 Primary,且移除后仍满足 majority)
rs.remove("mongo3:27017");

// 修改已有节点配置
cfg = rs.conf();
cfg.members[1].priority = 3;       // 提升选举优先级
cfg.members[1].tags = { region: "us-east", dc: "dc1" };  // 打标签用于读偏好
rs.reconfig(cfg);

6.2 主动 Step Down

在进行计划内的硬件维护或配置变更时,应先将 Primary 主动降级,让选举平滑发生,而不是粗暴地关闭主节点服务。

// 强制 Primary 主动降级为 Secondary
// secondaryCatchUpPeriodSecs: 等待从节点追上的最大时间
// force: 是否强制在超时后也降级
rs.stepDown(60, { secondaryCatchUpPeriodSecs: 15 });

// 降级后立即查看新的 Primary
rs.status().members.find(m => m.state === 1);

6.3 关键配置项调优

// 完整复制集配置示例(含调优参数)
rs.reconfig({
    _id: "rs0",
    members: [
        { _id: 0, host: "mongo1:27017", priority: 5, tags: { role: "primary_dc" } },
        { _id: 1, host: "mongo2:27017", priority: 3, tags: { role: "primary_dc" } },
        { _id: 2, host: "mongo3:27017", priority: 1, tags: { role: "dr_dc" } }
    ],
    settings: {
        electionTimeoutMillis: 10000,
        heartbeatIntervalMillis: 2000,
        chainingAllowed: true,              // 允许 Secondary 链式复制
        catchUpTimeoutMillis: 60000,        // 新 Primary 追平数据的最大时间
        catchUpTakeoverDelayMillis: 30000
    }
});

// 基于标签的读偏好(Tag Set)示例
const client = new MongoClient(uri, {
    readPreference: "secondaryPreferred",
    readPreferenceTags: [{ role: "dr_dc" }]
});

6.4 日常监控检查清单

// 每日应执行的检查脚本
function dailyHealthCheck() {
    const status = rs.status();
    const now = new Date();

    status.members.forEach(m => {
        const lag = m.state === 2 && m.optimeDate
            ? Math.floor((now - m.optimeDate) / 1000)
            : null;

        printjson({
            node: m.name,
            state: m.stateStr,
            health: m.health,
            lagSeconds: lag,
            lastHeartbeat: m.lastHeartbeat
        });
    });

    // oplog 窗口检查
    const opInfo = db.getReplicationInfo();
    print(`Oplog 窗口: ${opInfo.logLengthSeconds} 秒 (${(opInfo.logLengthSeconds/3600).toFixed(2)} 小时)`);

    // 检查是否有成员处于 RECOVERING 或其他异常状态
    const abnormal = status.members.filter(m => m.state !== 1 && m.state !== 2 && m.state !== 7);
    if (abnormal.length > 0) {
        print("WARNING: 发现异常状态节点:");
        abnormal.forEach(m => print(`  ${m.name}: ${m.stateStr}`));
    }
}

dailyHealthCheck();

7. 生产部署建议

建议说明优先级
最少部署 3 节点单节点故障时复制集仍可读写和选举必选
节点总数为奇数避免两节点平局导致选举僵局必选
跨可用区部署机房或交换机故障时多数派存活强烈推荐
优先使用 Hidden/Secondary报表、备份与分析任务不干扰主库负载推荐
监控 oplog 延迟和窗口Secondary 延迟超过阈值立即告警,防止进入全量同步必选
启用认证与 TLS生产环境必须启用 SCRAM-SHA-256 和 TLS 传输加密必选
奇数个有数据节点 + Arbiter若预算受限,可用 Arbiter 替代第三个数据节点降低成本可选

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「mongodb」更多文章

  1. 数据生命周期、TTL 与冷热归档
  2. $graphLookup 与层次结构建模
  3. GridFS 与大文件存储实践