29. MongoDB 副本集故障切换实战

副本集故障切换全流程:选举原理与数据版本规则、优先级与延迟节点、心跳探测机制、故障演练流程、rollback 回滚机制、网络分区场景分析与恢复预案

副本集(Replica Set)的高可用承诺是"主节点挂掉后自动选出一个新主节点",但"平时正常"不等于"故障时正确"。选举的触发时机、谁有资格当选、旧主重新加入时的数据回滚、以及网络分区下的脑裂防护,每一项都依赖精密的内部机制。这些机制平时不可见,只有通过主动的故障演练才能验证。本文从选举原理讲起,覆盖优先级与延迟节点、心跳探测、演练流程、rollback 回滚与网络分区场景,给出可复制的故障切换验证方法。

1. 选举原理与数据版本规则

副本集使用类 Raft 的选举协议:每个节点维护递增的选举任期(term),主节点通过心跳维持权威,当多数成员都无法感知主节点时触发新选举。选举的获胜者必须是"数据最新且符合优先级的节点"。

// 查看副本集当前状态与选举任期
rs.status()

// 输出片段(示意)
// {
//   "set": "rs0",
//   "term": 7,
//   "members": [
//     { "_id": 0, "name": "mongo-a:27017", "state": 1, "stateStr": "PRIMARY" },
//     { "_id": 1, "name": "mongo-b:27017", "state": 2, "stateStr": "SECONDARY" },
//     { "_id": 2, "name": "mongo-c:27017", "state": 7, "stateStr": "ARBITER" }
//   ]
// }

1.1 多数据版本规则

选举不是"谁嗓门大谁赢",而是"数据最新的节点才有资格"。每个候选节点都携带自己的 oplog 信息,落选条件包括:

  • 落后于主节点超过 10 秒(oplog 时间差),视为数据不新鲜,无法赢得选举
  • 配置版本低于其他成员的节点会被拒绝
  • 优先级为 0 的节点永远不能成为主节点
// 检查各成员的同步滞后
rs.status().members.map(m => ({
  name: m.name,
  stateStr: m.stateStr,
  lastHeartbeatRecv: m.lastHeartbeatRecv,
  optimeDate: m.optimeDate,
  electionTime: m.electionTime
}))

1.2 多数据版本 majority 语义

“数据最新"以 oplog 中最后的操作时间为基准。只有 oplog 覆盖了多数成员所确认的最新写入,该节点才符合当选条件。这保证新主节点不会丢失多数派已经确认的数据,是数据安全与高可用之间的核心平衡。

2. 优先级节点与延迟节点

优先级(priority)控制"谁更可能成为主节点”,而延迟(delayed)与隐藏(hidden)节点服务于特定运维需求。

2.1 优先级控制

优先级 0~1000,默认 1。优先级越高,选举时越有优势;优先级 0 的节点永远不会当选主节点,适合作为纯备份或读取节点。

// 把 mongo-b 的优先级调为 2,让它更倾向当选主节点
conf = rs.conf()
conf.members[1].priority = 2
rs.reconfig(conf)

// 禁止 mongo-c 当选主节点(优先级 0)
conf.members[2].priority = 0
rs.reconfig(conf)
优先级能否当选主节点适用场景
0否备份、读取、地域隔离节点
1(默认)是常规节点
2~1000是指定优先的容灾主节点

2.2 隐藏节点与延迟节点

隐藏节点对客户端不可见,不能接收读流量,但可以参与选举(除非 priority 0)。延迟节点从 oplog 延迟 N 秒复制数据,用于"误操作恢复"场景:主节点被误删数据后,延迟节点还能保留 N 秒前的快照。

// 配置隐藏节点
conf.members[1].hidden = true
conf.members[1].priority = 0
rs.reconfig(conf)

// 配置延迟节点:延迟 3600 秒(1 小时)
conf.members[2].priority = 0
conf.members[2].hidden = true
conf.members[2].secondaryDelaySecs = 3600
rs.reconfig(conf)

重要:延迟节点的 secondaryDelaySecs 越大,它当选主节点后丢失的数据窗口越大,因此必须配合 priority 0 使用,保证它永远不会参与选举。

3. 心跳与探测机制

节点之间通过心跳维持对集群健康的认知。心跳频率与超时阈值直接决定故障切换的响应速度。

  • 默认心跳间隔:每 2 秒一次
  • 默认选举超时:electionTimeoutMillis 为 10 秒
  • 连续 10 秒收不到主节点心跳,触发新选举
// 调整心跳间隔与选举超时(毫秒)
conf.settings.heartbeatIntervalMillis = 1000
conf.settings.electionTimeoutMillis = 5000
rs.reconfig(conf)

调小 electionTimeoutMillis 能加快故障切换,但也会让瞬时网络抖动更容易触发无谓的选举。生产环境通常保持默认或适度调小,避免"切换比故障更频繁"。

// 强制某个节点立即选举(模拟 stepdown 后的再选举)
rs.stepDown(30)        // 当前主节点主动让位 30 秒
rs.replSetStepDown(30) // 复制集管理命令,效果相同

主节点通过 replSetStepDown 主动让位是运维常见操作(如计划内升级)。它与故障死亡的差别在于:主动让位前会先同步 oplog 到多数成员,确保让位后数据不丢;而异常宕机可能产生 divergent oplog,为后续 rollback 埋下伏笔。

4. 故障演练流程

故障切换必须被主动演练,而不是等到事故发生时第一次体验。一次标准的演练包含以下步骤:

4.1 演练准备

// 1) 记录当前主节点与读写端点
rs.status()   // 确认 PRIMARY 所在节点

// 2) 开启慢查询与日志留痕,便于复盘
db.adminCommand({ setParameter: 1, profile: 2, slowms: 100 })

演练前要明确:演练窗口、观察指标(切换耗时、客户端报错数、数据一致性校验)、回滚预案(演练失败如何恢复)。绝不在无人值守时段演练。

4.2 触发切换

// 方式一:优雅让位(推荐,先同步 oplog)
rs.stepDown(60)

// 方式二:模拟异常宕机(直接杀进程,验证 rollback 路径)
// 在系统层面 kill -9 主节点进程
// kill -9 $(pgrep -f "mongod.*replSet")

优先演练优雅让位,再演练异常宕机。两种路径的行为完全不同:优雅让位不产生 rollback,异常宕机可能触发回滚,这正是需要被验证的差异。

4.3 观察切换过程

// 持续观察状态变化
while (true) {
  const st = rs.status()
  const primary = st.members.find(m => m.stateStr === "PRIMARY")
  print(new Date().toISOString(), "PRIMARY =", primary ? primary.name : "选举中")
  sleep(1000)
}
观察点期望结果异常信号
选举完成时间数秒内完成超过 electionTimeout 数倍
客户端连接自动重连新主节点大量报错堆积
写入可用性恢复后正常写入长时间不可写
数据一致性无丢失、无错序出现 rollback 文件

4.4 恢复旧节点

演练结束后,把被停掉的主节点重新启动,观察它作为副节点追平 oplog 并重新同步。

// 恢复后确认节点重新进入 SECONDARY 状态
rs.status().members.map(m => ({ name: m.name, stateStr: m.stateStr }))

4.5 客户端感知与重连验证

故障切换的另一个观察面是应用侧。驱动应开启自动重连与可重试写入,否则主节点切换后应用会持续报连接错误。演练时在应用侧记录错误日志,验证客户端在切换窗口内的表现。

// Node.js 驱动连接串:开启重试写入与多数派读偏好
// mongodb://app:pass@mongo-a:27017,mongo-b:27017,mongo-c:27017/?replicaSet=rs0&retryWrites=true&w=majority

// 演练期间应用侧观察脚本(示意)
let ok = 0, fail = 0
for (let i = 0; i < 200; i++) {
  try {
    await db.orders.insertOne({ seq: i })
    ok++
  } catch (e) {
    fail++   // 切换窗口内的写入失败
  }
}
console.log({ ok, fail })   // 期望 fail 数量远小于 200,且集中在切换瞬间
客户端配置作用注意事项
retryWrites主节点切换瞬间的写入自动重试需要复制集且写入 w: majority
retryReads切换瞬间读操作自动重试默认开启
serverSelectionTimeoutMS探测新主节点的超时调大避免误报
readPreference读流量路由secondaryPreferred 降低主节点压力

决策铁律:演练的目的不是证明"切换很快",而是验证"切换过程数据不丢、客户端感知正确、恢复路径清晰"。每次演练都应有书面报告,包含切换耗时、数据校验结果与发现的问题清单。

5. rollback 回滚机制

异常宕机场景下,旧主节点可能在宕机前已经写入了若干"未被多数成员同步"的操作。当它重新加入副本集时,发现自己的 oplog 与当前主节点产生分歧(divergent),MongoDB 会把这些分歧操作回滚。

5.1 回滚发生的条件

  • 旧主在宕机前写入了未同步到多数成员的数据
  • 旧主重新加入后,这些操作不在新主的 oplog 中
  • 新主已接受的其他写入与旧主的数据冲突

5.2 回滚的处置

被回滚的文档不会被静默丢弃,而是写入回滚文件(rollback/*.bson),供运维人工审查与恢复。回滚期间,该节点短暂不可读。

// 查看回滚目录(mongod 数据目录下)
// ls /data/db/rollback/
// rollback-2026-09-30T11:30:00.bson

// 用 mongorestore 恢复回滚数据到隔离集合
// mongorestore --db=rollback_audit /data/db/rollback/
维度无回滚(优雅让位)有回滚(异常宕机)
数据丢失无未同步的少数写入被回滚
运维动作无人工审查回滚文件
发生条件让位前已同步多数宕机前未同步多数

5.3 用 writeConcern 抑制回滚

回滚本质上源于"写入未被多数成员确认"。若关键写入使用 w: "majority",则主节点在返回成功前已同步到多数成员,旧主宕机后重新加入时不会产生可回滚的分歧数据。

db.orders.insertOne({ orderNo: "X-1" }, { writeConcern: { w: "majority", j: true } })

重要:w: "majority" 不是零成本。它增加了每次写入的同步延迟,换来的是一致性与"无回滚"保证。核心业务写入用 majority,非关键写入用默认 w: 1,是常见的分级策略。

6. 网络分区场景

网络分区(split brain)是副本集最危险的故障形态:节点之间的网络被切断,但节点本身仍在运行。此时多数派与少数派可能各自形成独立的子集群。

6.1 多数派原则

选举要求"多数成员"参与并确认。少数派分区中的节点因为无法凑齐多数,会主动降级为只读副节点,从而避免"两个主节点同时写入"的脑裂。

// 分区场景:少数派中的节点状态
rs.status().members.map(m => ({
  name: m.name,
  stateStr: m.stateStr,
  // 少数派节点会进入 SECONDARY(只读),即使它原本是主
}))
分区形态多数派行为少数派行为
3 节点:1 与 2 断开1、2 选主并继续写入3 只读,等待恢复
2+1 仲裁:主与仲裁断开副+仲裁选新主旧主降级只读
5 节点:2 对 33 节点分区正常服务2 节点分区只读

6.2 分区恢复

网络恢复后,各节点重新交换心跳,少数派节点会从当前主节点追平 oplog,重新回到同步状态。此时应检查是否有 rollback 文件产生,并验证数据一致性。

// 恢复后统一检查
db.adminCommand({ replSetGetStatus: 1 }).members.map(m => ({
  name: m.name,
  stateStr: m.stateStr,
  lagSeconds: m.optimeDate ? Date.now() / 1000 - m.optimeDate.getTime() / 1000 : null
}))

6.3 分区期间的读写行为

分区期间,客户端写入少数派分区会立即失败(只读降级),读取则取决于 readPreference 与 readConcern。多数派分区照常服务读写,这正是高可用设计的预期行为。

分区侧写入读取行为
多数派(含新主)正常正常完整读写能力
少数派(旧主)失败降级只读只读请求仍可返回旧数据
恢复中的节点失败受限追平 oplog 中

6.4 脑裂防护的工程含义

副本集通过"多数派写入 + 少数派只读"天然防脑裂,但它依赖一个前提:每个节点都能正确感知自己属于多数派还是少数派。若网络分区与节点故障叠加,且监控缺失,就可能出现"主节点切换了但应用还连着旧主"的窗口期。应用层应配置合理的连接探测与重连策略,与副本集自动切换协同。

// 应用层探测当前可写节点(写前探测,示意)
function probeWritable(db) {
  const result = db.adminCommand({ isMaster: 1 })
  if (!result.isWritablePrimary) {
    throw new Error("当前连接不可写,等待重连")
  }
  return result
}

网络恢复后,优先验证多数派分区中是否产生了 rollback 文件,再恢复少数派节点。切忌在分区未恢复时人工强制某节点为主节点,那会破坏多数派保证并引入数据不一致。

7. 演练清单与运维总结

故障切换能力不是配置出来的,而是演练出来的。一份完整的副本集故障演练清单应覆盖:优雅让位、异常宕机、网络分区、节点恢复、rollback 审查、客户端重连,共六个场景。

  • 提前记录基线:演练前保存 rs.status 与各节点 oplog 时间
  • 优雅路径优先:计划内维护用 replSetStepDown,避免无谓回滚
  • 异常路径必演:kill 主进程验证 rollback 与数据审查流程
  • 分区场景单列:网络层故障与进程故障行为不同,分开演练
  • 每次留下报告:切换耗时、数据校验、发现的问题与整改项

决策铁律:副本集故障切换的黄金指标是"恢复时间 + 数据丢失量"两个数字。演练时同时记录二者:切换耗时多长,是否有数据被回滚。只要这两个数字在业务可接受范围内,故障切换机制就是合格的。

延伸阅读

继续阅读

探索更多技术文章

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

全部文章 返回首页

「mongodb」更多文章

  1. 33. MongoDB 地理空间与全文检索
  2. 32. MongoDB 批量写入与吞吐优化
  3. 31. MongoDB 读写关注与一致性