☰
MongoDB三节点副本集高可用架构与运维实践
2026/10/15 6:49:31 网站建设 项目流程

做后端开发的,迟早会和数据库的高可用打交道。早期单机 MongoDB 扛业务,数据量上来、并发一高,磁盘 I/O 和连接数先扛不住,更别提机器宕机或者机房断电这种物理级事故——真碰上,恢复数据全靠运气和备份时效。后来我把核心业务切到了三节点副本集,累计稳定运行两年多,中间经历过主节点被强制 kill、机房网络抖动、磁盘写满等状况,都没丢失数据,也没出现长时间不可用。这篇就把我在这套架构上的设计、部署、运维实践和踩坑记录完整写出来。

所谓副本集,本质就是一组 mongod 进程构成的主从复制集群,通过内置的复制协议自动完成数据同步和故障切换。三节点是最小的高可用配置,也是我在实际项目里认为性价比最高的形态。它能容忍一个节点完全宕机而不影响业务写入,配合合理的读偏好设置,还能横向分担一部分查询压力。

1. 架构设计与副本集核心机制

1.1 为什么选三节点而不是两节点或单机

单机部署的问题很直接:没有冗余,mongod 进程崩溃等同于数据库不可用。两节点虽然多了一台机器,但会引入一个非常尴尬的投票死局——两个节点各有一票,一旦发生网络分区,双方都认为自己是主节点,各写各的,脑裂风险极高;如果加上仲裁节点,实际上又变成了三节点的拓扑。所以生产环境我基本不会考虑两节点。

三节点最标准的拓扑是一个主节点加两个副节点。三个节点各自持有一票,任意节点想要成为主节点,必须获得超过半数的投票,也就是至少两票。这样在任何时刻,整个集群有且只有一个主节点能够被多数派认可,从机制上就消除了脑裂的可能性。

三节点的优势还不止投票这个层面。两个副节点意味着我们有双份完整的数据副本,当其中一个副节点因为硬件故障掉线时,另一个仍然能够提供完整数据,并且可以正常参与选举。如果只有一主一副,副节点一挂,主节点虽然还能工作,但整个集群已经退化成单点,既没有故障转移的能力,也丧失了数据冗余,这种情况下基本应该立刻介入修复。

注意:副本集的“高可用”是有损降级的高可用。三节点集群允许损失一个节点,但集群健康度会从 GREEN 降到 YELLOW,此时如果再出现第二个节点故障,整个副本集将进入只读或完全不可用状态。这个风险阈值必须让所有人都清楚。

1.2 副本集的核心写入与同步协议

MongoDB 副本集的数据一致性依赖操作日志,也就是 oplog。每次写入操作在主节点执行后,都会被记录为一条 oplog 条目。副节点通过持续拉取 oplog 并重放这些操作,异步地将自身数据推进到与主节点一致的状态。

这里需要说明的是:所有的写操作,包括插入、更新、删除以及带事务的多文档操作,都只会经过主节点。副节点直接收到的写请求会被主节点驳回,除非设置了合适的 read preference 允许从副节点读——但即使从副节点读,写还是必须走主节点。

我习惯用一个不太精确但很好理解的类比:主节点是编辑部,内容在这里完成终审签发;oplog 是排版好的版样;副节点是印刷厂,拿到版样后照单印刷。只要版样完整,任何一个印刷厂都能印出同样的报纸。

实际部署中,我强烈建议用 SSD 承载 oplog 所在的存储池,尤其是高写入负载的业务。oplog 的写入和读取都是顺序 I/O,SSD 能显著降低副节点的复制延迟。机械硬盘在长时间高强度写入下,很容易出现复制 lag 涨到几千秒的情况。

1.3 选举机制与故障转移原理

当主节点发生故障,比如进程宕掉、网络不可达、心跳超时,副节点会发起一次选举。整个选举过程遵循一个非常简单的原则:能联络到多数派、拥有最新 oplog 数据的节点胜出。

每个节点维护一个 term 计数器,类似选举代次。每次选举都会使 term 加一。节点之间通过心跳消息交换各自的状态和 term。当一个节点发现无法联系到主节点,它会将自身状态切换到 candidate,并请求其他节点投票。其他节点会检查候选者的 oplog 是否足够新(比较 lastOpTime),以及自己当前有没有更优先的候选,然后决定是否投出赞成票。

有件事值得特别提醒:在投票逻辑里,数据新鲜度优先于节点优先级。也就是说,哪怕一个节点配置了更高的 priority,但只要它比另一个节点的 oplog 落后,选举时高优先级也不会压过数据更新的低优先级节点。这个设计很合理,因为高可用系统的第一目标是数据不丢,而不是性能或角色偏好。

等网络恢复、原主节点重新加入集群后,它会发现自己当前 term 小于集群内的最新 term,于是主动降级为从节点,并同步追赶新主节点的数据。这个自我纠错的过程是自动完成的,不需要人工干预。

2. 环境规划与部署准备

2.1 节点配置与磁盘规划建议

三节点副本集的每一台机器承担的角色其实相同——既可以当主节点也可以当副节点,所以三台机器配置最好保持一致,不要出现一台 8 核 64G 带两台 2 核 4G 这种奇葩组合。副本集的高可用依赖于所有节点都足够健壮,短板机器不仅容易成为故障点,还会拖慢整个集群的同步效率。

我当前这台生产环境的副本集是这样的配置:

节点角色云主机规格磁盘系统
主节点4核 8GSSD 200G主流 Linux 发行版
副节点 A4核 8GSSD 200G主流 Linux 发行版
副节点 B4核 8GSSD 200G主流 Linux 发行版

内存规划上,MongoDB 默认的 WiredTiger 存储引擎会使用大多数可用内存作为缓存。这里要区分实际情况,如果机器上只跑 MongoDB,可以按默认的 50% 或更高设置 cache size;如果机器上还要跑别的应用,建议显式配置 storage.wiredTiger.engineConfig.cacheSizeGB,防止 MongoDB 把内存吃光导致其他服务被 OOM。

磁盘这块我需要多说一句。副本集所有节点上的数据是完全相同的,所以三台机器需要同等大小的磁盘空间。很多人图省事给副节点分很小的磁盘,结果主节点数据涨到一定规模后,副节点复制直接失败,集群变成一主一从,这个坑我踩过一次。

2.2 安装 MongoDB 并配置基础参数

安装过程不复杂,重点在于几个关键的配置参数。我直接贴一份生产可用的配置模板,然后逐个解释每项参数的含义。

systemLog: destination: file path: /var/log/mongodb/mongod.log logAppend: true logRotate: reopen verbosity: 0 storage: dbPath: /var/lib/mongo journal: enabled: true wiredTiger: engineConfig: cacheSizeGB: 4 processManagement: fork: true pidFilePath: /var/run/mongodb/mongod.pid timeZoneInfo: /usr/share/zoneinfo net: port: 27017 bindIp: 0.0.0.0 replication: replSetName: rs0 security: authorization: enabled keyFile: /etc/mongodb/rs-keyfile

先看 replication.replSetName:这个名称在同一个副本集里的所有节点上必须一致,否则节点之间无法识别彼此属于同一个集群。我见过有人只改了一台机器的 replSetName 就直接 rs.add(),结果报错 appeared to be a different replica set,排查半天才发现是把名称写错了。

再看 security.keyFile:启用认证后,副本集各节点之间需要用同一个 keyfile 进行内部通信认证。这个文件必须提前生成并分发到所有节点上,权限要设为 600,属主必须是运行 mongod 的用户。如果 keyfile 权限不对,mongod 启动时会直接拒绝启动,并报错 permissions on keyfile are too open 之类的话。

cacheSizeGB 的配置要和硬件匹配。比如机器是 8G 内存,MongoDB 之外还要跑监控 agent 和数据同步进程,我就把缓存设成了 4G。这个值并不是越大越好,要是把系统内存全占了,会导致操作系统频繁 swap,性能反而断崖式下跌。

2.3 安全加固与访问控制

生产环境绝对不能裸奔,没有鉴权的 MongoDB 等于把数据裸奔在公网。我的做法是创建独立的业务账号和运维账号,业务账号只授予目标库的读写权限,运维账号才拥有 dbOwner 或更高的管理权限。账号职责分离,避免一个账号被泄露后整个集群沦陷。

// 在初始化副本集后执行 // 创建应用读写账号 db.getSiblingDB("appdb").createUser({ user: "appUser", pwd: "强密码", roles: [ { role: "readWrite", db: "appdb" }, { role: "read", db: "appdb_log" } ] }); // 创建运维管理账号 db.getSiblingDB("admin").createUser({ user: "opsAdmin", pwd: "另一个强密码", roles: [ { role: "root", db: "admin" }, { role: "clusterAdmin", db: "admin" } ] });

业务侧统一走专有账号连接,URI 格式采用副本集连接串:

mongodb://appUser:password@192.168.1.11:27017,192.168.1.12:27017,192.168.1.13:27017/appdb?replicaSet=rs0&readPreference=secondaryPreferred

要注意 readPreference 这个参数。副本集默认所有读请求都打到主节点,相当于副本集只提供了高可用,没有扩展读能力。设置了 secondaryPreferred 后,读流量会被优先分配到副节点,只有当副节点全部不可用时才会走主节点,这对读多写少的业务帮助很大。

3. 副本集初始化与功能验证

3.1 初始化副本集

三个节点的 mongod 启动完成后,任意挑一个节点连接上去执行 rs.initiate()。通常以第一个启动的节点,或者我们希望它成为初始主节点的节点为准来执行初始化。

// 连接第一台节点 mongosh --host 192.168.1.11 --port 27017 rs.initiate({ _id: "rs0", members: [ { _id: 0, host: "192.168.1.11:27017", priority: 2 }, { _id: 1, host: "192.168.1.12:27017", priority: 1 }, { _id: 2, host: "192.168.1.13:27017", priority: 1 } ] });

关于 priority 这里我解释一下。节点的 priority 决定它在选举中的权重,默认都是 1。我给第一个节点设了 priority: 2,这样只要它在线,它被选为主节点的概率就会更高。这个配置在跨机房的部署里很有用,可以把主节点固定在有业务写入倾向的那个机房或延迟更低的那台机器上。

如果不想固定主节点,三个节点都用默认 priority: 1 也完全可以,选举机制会自动选出数据最新的节点来当主。

执行 rs.initiate() 后,副本集会在几秒钟内完成选举,可以通过 rs.status() 查看状态:

rs.status().members.forEach(m => { print(`name: ${m.name}, stateStr: ${m.stateStr}, health: ${m.health}`); });

正常情况会看到三个节点中一个 PRIMARY,两个 SECONDARY,所有节点的 health 都是 1。如果出现某个节点一直显示 STARTUP 或 STARTUP2,说明它还在同步初始数据,等 oplog 同步完成后会自动变成 SECONDARY。

3.2 验证主从复制与事务支持

副本集初始化完成的头两件事,我一向是验证写入复制和验证事务能力。

先测复制:

// 在主节点写入一条测试数据 db.testcoll.insertOne({ _id: 1, ts: new Date() }); // 连接副节点查询 db.getMongo().setReadPref("secondary"); db.testcoll.findOne({ _id: 1 });

只要在副节点能查到 _id 为 1 的文档,复制链路就没问题。注意查询前必须切换读偏好,不能在副节点上做默认主读,因为主节点上的数据不会自动反向广播到副节点——对,副节点确实持有副本,但默认读并不保证能命中副本数据。

再测事务。MongoDB 副本集支持多文档事务,但前提是存储引擎必须是 WiredTiger,并且副本集成员全部启用副本集模式。我一般用一个两文档更新的小事务来验证:

const session = db.getMongo().startSession(); session.startTransaction(); try { session.getDatabase("appdb").collA.updateOne( { _id: 1 }, { $set: { val: "tx-test" } } ); session.getDatabase("appdb").collB.updateOne( { _id: 1 }, { $set: { val: "tx-test" } } ); session.commitTransaction(); } catch (e) { session.abortTransaction(); throw e; }

如果之前用的还是 MMAPv1 的旧版本,一定要现在做计划升级到 WiredTiger。我印象非常深的一点是:MMAPv1 不支持事务,连单文档原子性都很受限,这在现代业务场景下几乎没法用。

3.3 延迟与数据一致性验证

部署完副本集,我不会马上把业务流量切过去。我做一轮完整的延迟测试,核心指标就是写操作完成时间以及副节点复制延迟。

复制延迟可以通过下面这种方式观察:

const primaryOpTime = rs.printReplicationInfo(); const secondaryLag = rs.status().members .filter(m => m.stateStr === "SECONDARY") .map(m => ({ name: m.name, lag: m.optimes ? m.optimes.appliedOpTime.ts - rs.status().members[0].optimes.lastCommittedOpTime.ts : null }));

更常用的做法是看第二行的时间差。rs.printReplicationInfo() 会显示 oplog 的最大时间和当前时间,如果两者差距不大,说明复制基本是实时的。在有同机房副节点的情况下,我看到的最长复制延迟也就是毫秒级别。

但要注意:写主节点的响应时间只能证明主节点写入成功,并不代表副节点已经追上。所以真正生产环境中的一致性校验不能只看主节点,必须要从副节点读出来比对。我们内部有一条不成文的规定:重要数据写完以后 5 秒,会用副节点读一条来交叉验证,长期执行下来没发现过大分叉,这才敢把线上读流量切到 secondaryPreferred。

4. 日常运维与高可用演练

4.1 必须盯住的监控指标

副本集部署完成只是开始,日常运维才是重头戏。我当前监控面板上的核心指标大致分四类:

指标分类具体指标告警阈值
节点状态节点是否可达、主从角色30 秒无心跳即告警
复制状态复制延迟、oplog 时间差延迟超过 60 秒告警
系统资源CPU、内存、磁盘使用率、I/O 等待磁盘使用率超过 85% 告警
连接状态当前连接数、连接池等待时间连接数达到上限 80% 告警

系统资源里最容易被忽略的就是磁盘。副本集节点上的数据相同,磁盘消耗完全同步增长,一旦主节点磁盘告警,副节点也会在同一时间附近告警。如果只剩 5% 空间,oplog 可能立刻被截断,比节点宕机还危险,因为这个故障没有自动恢复方案,必须手工扩容或清理数据。

复制延迟指标的采集方式,建议直接用驱动自带的监控命令或者平台监控脚本,定死在每 15 秒一采。opslag 异常攀升往往预示着网络抖动或者副节点 I/O 饱和,这个时候去看主节点压测得到的负载数据,基本就能对上有问题的是哪台。

4.2 备份策略与恢复演练

副本集不等于备份。很多人以为副本集有多个副本就不需要备份了,这是非常危险的误解。副本集解决的是高可用问题,如果业务上有人误执行了 dropDatabase(),这个删除操作会通过 oplog 同步到所有副节点,所有副本上的数据都会消失。这种情况下唯一能救命的只有真正的备份。

我当前采用的备份策略如下:

  • 每天凌晨 2 点对副节点执行 mongodump,备份整个业务库
  • 备份保留周期为 7 天,同时每天导出一份到对象存储做异地留存
  • 每月做一次全量恢复演练,验证备份数据可恢复性

具体的备份命令如下,一定注意要连副节点执行,避免对主节点造成额外压力:

mongodump -h 192.168.1.12:27017 -u opsAdmin -p '密码' \ --authenticationDatabase=admin \ -d appdb \ -o /backup/mongodb/appdb/$(date +%Y%m%d%H%M)

恢复命令则是对应上传:

mongorestore -h 192.168.1.11:27017 -u opsAdmin -p '密码' \ --authenticationDatabase=admin \ -d appdb \ /backup/mongodb/appdb/202501020300

需要指出的是:mongodump 属于逻辑备份,对于超大库会明显比物理文件拷贝慢。如果数据库超过 500G,我建议改用文件系统快照方式备份副节点数据目录。用云厂商的快照能力做一致性快照,速度快很多,恢复时也能直接挂载读取。

还有一点,任何备份策略都要经过真实的恢复演练才能算数。我自己就遇到过备份文件损坏、mongorestore 到一半报错的情况,都是靠定期演练提前发现的。不演练的备份,真到事故面前就是摆设。

4.3 高可用切换演练的具体步骤

高可用系统的价值在故障时才能体现,所以我们平时就要主动制造可控故障,验证切换逻辑。

我每次演练过程是这样组织的:

  1. 通知业务方和研发,确认当前没有批量写任务在执行
  2. 找到当前主节点,记录其 host 和优先级
  3. 执行 db.adminCommand({ shutdown: 1 }),模拟主节点被动宕机
  4. 观察剩余两个节点的选举过程
  5. 验证新主节点是否接管业务写入
  6. 恢复原主节点进程,观察它重新加入集群并同步数据
  7. 再次执行 rs.status() 确认所有节点健康

整个切换过程通常只需要几秒到十几秒。这个过程中业务端需要做的就是正确配置驱动里的 replicaSet 参数,以及处理好 readPreference。驱动会自己感知主节点切换并重连,应用层最需要跨过的是网络重连和自动重试的异常处理。如果业务代码里没用连接池,而是每次手动 new MongoClient,则故障切换瞬间会有少量请求失败,这个问题必须提前让研发同学知道。

切换演练对副本集的意义不用多说。真正让团队所有人建立起信心的,就是经历过几次这样的演练,大家都清楚故障来临时数据库会怎样响应。

5. 常见问题与排查实录

5.1 节点加入副本集一直显示 STARTUP

这个问题非常常见。新节点加入副本集后,很长时间一直处于 STARTUP 或 STARTUP2 状态,无法变为 SECONDARY。

排查思路分三步走:

第一步,检查 mongod 日志。最常见的原因是初始同步需要全量拷贝数据,拷贝时间与数据量成正比。几 G 数据可能要几分钟,几百 G 就要几十分钟甚至更久,这属于正常现象,耐心等待即可。

第二步,如果长时间毫无进展,就要检查新节点的磁盘空间。初始同步期间,MongoDB 会先拷贝数据文件,再回放 oplog 追赶主节点。如果磁盘空间不足,同步过程会反复失败,节点会陷入反复重试。

第三步,检查网络连通性。副本集节点之间需要能够直连彼此的 27017 端口,如果有防火墙挡着,同步过程完全无法启动。

5.2 主节点频繁切换

如果集群里主节点频繁自动切换,也就是 flapping,业务会间歇性感知到写超时。这个问题的根源通常是网络质量。

最常见的场景:主节点和其他节点之间的网络有丢包或抖动,导致心跳超时,其他节点发起选举,原主节点恢复后被迫降级,短时间内又可能触发再次选举。

另一个常见原因是 CPU 饱和。如果主节点的 CPU 使用率长期打满,mongod 处理心跳消息就会延迟,其他节点误以为主节点失联,然后发起不必要的选举。

针对这类问题,我的建议是按顺序排查:

  • 用业务自带的网络监控工具或 ping、mtr 检查节点间丢包率是否大于 0
  • 用 top 和 mongostat 确认各节点的 CPU 使用率
  • 如果网络质量没有问题,那就考虑调整心跳间隔,但这是兜底方案,而不应该是首选方案

5.3 复制延迟持续增大且无法追平

复制延迟表现为副节点和主节点的数据差异持续拉大,副节点永远追不上主节点。这种情况多半不是瞬时抖动,而是某个副节点存在长期性能瓶颈。

我在生产环境里遇到过的一种典型情况:副节点同时承担了业务侧大量的读请求,又承担了备份任务的负载,导致 I/O 带宽耗尽,oplog 拉取和重放都被拖慢。

解决方向有两条:

第一,将读流量和备份任务迁移到另一个副节点,或者直接买性能更高的机器来跑副节点。第二种,如果确认这个副节点永远追不上主节点,不如想办法让它偶尔停掉同步,在空闲时段再重新开启追赶,比如在凌晨低峰期执行 rs.syncFrom(),让它从另一台更健康的副节点同步。

不过这里有个更关键的常识需要强调:复制延迟的隐患并不是副节点读取的数据不够新,而是如果主节点在这种状态下宕机,那么拥有最新 oplog 数据的节点才有资格当选主节点。如果副节点落后太多,它不仅追不上数据,还可能在选举中被剥夺候选资格,集群将退化为不可用状态。所以延迟监控必须尽快处理,不能等着自我恢复。

5.4 oplog 大小与容量规划

oplog 是副本集复制的核心,它的大小直接决定了复制链路的容错能力。默认的 oplog 大小是磁盘容量的 5%,对于长期高写入的业务来说往往不够。如果某个副节点离线超过 oplog 覆盖的时间窗口,它重新上线后无法从主节点增量同步,只能做全量重同步,耗时和风险都大。

先看当前 oplog 情况:

rs.printReplicationInfo();

输出里的 oplog first event time 和 oplog last event time 之间的时间跨度,代表当前 oplog 能覆盖多长的时间窗口。如果这个窗口只有几个小时,而你的日常维护或网络中断可能要持续大半天,那就要考虑调整 oplog 大小了。

调整 oplog 大小的方式在 4.0 以上版本可以直接在运行中执行:

db.adminCommand({ replSetResizeOplog: 1, size: 16384 // 单位 MB,即 16G });

执行之后,新的大小会立即生效,不需要重启。我一般建议将 oplog 设置为可以覆盖至少 12 小时到 24 小时的写入量,极端写入密集的库甚至要留两天的余量。

5.5 认证开启后节点间通信失败

如果之前以无认证方式运行过副本集,后来才启用 keyFile 认证,最容易踩的坑是集群内部节点之间的认证没生效。

现象是:节点启动后一直处在 UNKNOWN 或者 REMOTE 状态,日志报错 not authorized for admin command。排查办法是先确认所有节点使用了同一个 keyFile 内容,然后确认所有节点的 keyFile 权限为 600。

需要特别说明的是,修改 keyFile 后必须重启 mongod 才能生效。副本集内部通信的认证是在进程启动时加载的,只改文件不重启是不行的。我见过有人改了 keyFile,然后把文件拷贝到所有节点,却不重启进程,导致节点间互相不信任,整个集群差点瘫痪。

6. 监控告警的落地配置样例

前面讲了监控看什么指标,这里给一份可落地的告警规则样例,方便直接借鉴。我目前用的是开源监控方案 Prometheus,配合社区维护的 MongoDB Exporter,整体稳定性和灵活性都比较满意。

关键的告警规则可以这样定义:

groups: - name: mongodb-replicaset rules: - alert: MongoDBNodeDown expr: mongodb_health_status == 0 for: 1m labels: severity: critical annotations: summary: "MongoDB 节点不可达" - alert: MongoDBReplicationLagHigh expr: mongodb_mongod_replset_member_optimes_seconds - on(instance, setname) mongodb_mongod_replset_member_optimes_seconds{state="PRIMARY"} > 60 for: 2m labels: severity: warning annotations: summary: "副本集复制延迟超过 60 秒" - alert: MongoDBPrimaryFlapping expr: changes(mongodb_mongod_replset_member_state{state="PRIMARY"}[15m]) > 3 labels: severity: warning annotations: summary: "主节点在 15 分钟内发生多次切换" - alert: MongoDBDiskUsageHigh expr: mongodb_disk_usage_percent > 85 for: 5m labels: severity: warning annotations: summary: "MongoDB 数据目录磁盘使用率超过 85%"

这些配置都是我日常在维护和迭代的。告警阈值这个东西,别照搬网上的,一定要结合自己集群的实际情况来定。比如复制延迟 60 秒对某些业务可能已经是严重的,但对于一个夜间离线跑批的系统来说可能完全无所谓。因为告警的最终目的不是被发现,而是帮助人快速定位和介入。

另外我对告警完整性还有一点补充:一定要做分钟级的数据保留和历史趋势留存。光看当前值很难判断是瞬时抖动还是持续恶化,有了趋势图,很多问题一眼就能看出来,能省下大量排查时间。

写在最后的一点经验

从最开始单机 MongoDB 被业务写垮,到现在这套三节点高可用副本集稳定扛下所有核心读写流量,最大的感受是:高可用不是说出来的,是设计、验证和演练反复打磨出来的。

最后分享两个我在运维过程中沉淀下来的习惯。第一个是每次故障恢复后,一定要做一次复盘,把时间线、根因、解决办法、预防措施四件事写清楚,沉淀成团队的运维手册。第二个是每月至少做一次主节点手动切换演练,没人知道真实的故障什么时候来,但等到故障来了再手忙脚乱翻文档,损失一定比演练成本大得多。

副本集本身是一个非常成熟的技术方案,文档也写得很全。但真正让它发挥价值,靠的还是部署细节的严谨和日常运维的用心。希望这篇手册能帮你少踩几个坑,把 MongoDB 副本集真正跑稳。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询