“有个关系还不错的开发者前阵子找我帮忙看一套 MongoDB 高可用方案,他说自己把生产环境从单节点迁到了 Replica Set,用的是两台数据节点加一台 Arbiter。我听完第一反应不是‘稳不稳’,而是‘你先告诉我这台 Arbiter 放在哪儿了’。很多人一看 MongoDB 使用 Arbiter 搭建 Replica Set,就自动带入‘省钱版三节点高可用’这个定位,但 Arbiter 本质上是投票补位,不是数据副本,用对了能少买一台全量存储机器,用错了会在你最需要它的时候让故障切换直接失效。这篇文章我会从复制集原理讲起,把 Arbiter 的角色、适用场景、完整搭建步骤、故障演练以及各种坑一次性说清楚,适合正在设计 MongoDB 高可用方案的 DBA、后端开发和架构师阅读。”
1. 先搞懂 Replica Set 是怎么高可用的
1.1 单点故障的痛,和一份省钱版的解法
MongoDB 早期部署最常见的形态是单节点跑一个 mongod,应用连接它就完事了。这种模式在开发环境毫无问题,但一旦上了生产就会遇到几个绕不开的坎:机器重启的时候写入被打断,磁盘损坏直接丢数据,半夜出现故障时没有自动恢复机制,需要人肉切流量甚至从备份重建。Replica Set 解决的就是这些问题,它的思路本质上是把同一份数据在多个节点上各保留一份,节点之间通过心跳互相感知状态,一旦主节点不可用,其余节点会自动选出一个新的主节点顶上,客户端不需要手工改配置。
但这里有一个容易被忽略的细节:高可用是分等级的。三个全量数据节点组成的 Replica Set,数据在同一时刻至少存在三个完整副本,任何一个节点宕机,剩余两个节点依然有完整的读写能力;而两台数据节点加一台 Arbiter 的副本集,数据实际只冗余了两份。Arbiter 不存数据,它只参与投票,所以这种架构被很多人叫作“省钱版三节点方案”——花两台全量机器的钱,拿到三节点的投票资格。这不是坏事,前提是你清楚这个“资格”能换来什么、换不来什么。
大多数团队选择 Arbiter 的动机很实际:存储资源很贵,尤其是使用云盘或高配 SSD 的时候,第三个全量节点的成本可能比一台普通虚机高出数倍,而 Arbiter 只需要很低的内存和磁盘就能运行。这个动机本身没问题,但很多人忽略了一个前提——Replica Set 的高可用能力来自“大多数投票成员可达”,而数据安全性来自“大多数数据节点收到写入”。Arbiter 能帮你凑够投票大多数,却不会帮你多存一份数据,这两个概念从设计开始就要分开。
1.2 节点成员类型:不是所有节点都在存数据
Replica Set 里并不只有主节点和从节点两种角色。在配置层面,每个成员可以被赋予不同的优先级、投票权和隐藏属性,从而适配不同的部署意图。为了讲清楚 Arbiter 的位置,我先列一下常见的成员类型。
| 成员类型 | 是否存储数据 | 是否参与投票 | priority 默认值 | 典型用途 |
|---|---|---|---|---|
| Primary | 是 | 是 | 1 | 处理全部写入,默认读取 |
| Secondary | 是 | 是 | 1 | 数据冗余,可承担读流量 |
| Arbiter | 否 | 是 | 0 | 只参与选举投票,不存数据 |
| Hidden Secondary | 是 | 可选 | 0 | 备份、报表分析,客户端不可见 |
| Delayed Secondary | 是 | 可选 | 0 | 延迟同步,用于误操作恢复 |
从这张表就能看出来,Arbiter 的定位非常特殊:它不落数据,不提供读服务,不参与同步,唯一存在价值就是在选举的时候投出自己那一票。它的 priority 必须是 0,意味着它永远不会被选举为 primary;它的投票权默认为 1,意味着当集群需要对新的主节点做出裁决时,它的声音和全量数据节点一样有效。
这一点常常是新手的认知分界线。有人会问:既然它不存数据,那两台数据节点加一台 Arbiter,和两台数据节点有什么区别?区别就在选举。两台数据节点发生故障时,只剩一台机器在线,它只有一票,在默认的多数派规则下永远凑不齐两张票,无法被提升为新的 primary;而有一个 Arbiter 在线时,剩余这台数据节点加上 Arbiter 正好两张票,可以合法完成选举。Arbiter 的价值是数学意义上的票数补位,而不是物理意义上的数据副本。
1.3 选举、心跳与多数派:高可用的底层协议
Replica Set 的自动故障切换不是靠某个外部调度器,而是靠节点之间的心跳和选举协议。默认情况下,副本集成员每 2 秒互相发送一次心跳,如果一个节点超过选举超时时间(默认大约 10 秒)没有收到 primary 的心跳,它就会认为自己所在的副本集可能失去了主节点,并发起一轮选举。选举采用类 Raft 的多数派规则,候选节点需要获得副本集中所有投票成员的大多数选票,才能成为新的 primary。
多数派规则可以通过一个日常例子解释:三个人开投票会,任何决议至少需要两票通过,两个人私下商量好就能拍板,但一个人无论如何无法代表整体。数据库里的选举也是这个逻辑,只不过它要防的不是“代表整体”,而是“同一时间出现两个主节点”。两个节点各自认为对方已死,同时把自己变成 primary,客户端写入就会分成两路,数据会逐渐分叉,之后想合并都找不到依据。多数派规则保证在任何网络分区下,最多只有一个分区能凑齐大多数票,从而只有那一个分区能合法产生 primary。
除了节点之间的内部协议,客户端写入也有对应的关注机制。默认的写入关注是 w:1,即只要 primary 确认写入就算成功;如果你想在副本集层面确认数据已经复制到大多数投票成员,可以显式配置 w:majority。这个参数在带 Arbiter 的副本集里有一个容易被忽略的坑,我后面会在故障演练部分专门展开。
2. Arbiter 到底解决了什么问题
2.1 投票平局与奇数节点的数学道理
为什么 MongoDB 官方经常建议生产环境至少有 3 个投票成员?核心原因是多数派规则要求“过半”,所以投票成员总数最好是奇数。两个投票成员的副本集有一个天然缺陷:如果其中一个节点宕机,剩余节点无论怎么投票都只有一票,而多数派阈值是两票,永远选不出主节点,整个集群实际上失去了写入能力。
两台数据节点加一台 Arbiter 之后,投票成员总数变成 3,多数派阈值变成 2。这意味着两个数据节点中任意一个宕机时,只要 Arbiter 还活着,另一个数据节点就能拿到两票完成选举。这个算术可以从另一个角度解读:三节点副本集的容错能力是“同时允许一台投票成员不可用”,两数据节点加一个 Arbiter 也拥有同样的容错能力,区别只是把第三份数据副本替换成了无数据的仲裁票。
这种替换的代价需要被明确认知。三个全量数据节点允许任意一台机器完全损坏而不丢数据,因为另外两台还有完整副本;而两数据节点加 Arbiter 的架构不允许两台数据节点同时损坏,因为此时即使 Arbiter 还活着,也只有一个数据副本残存,数据完整性完全取决于那台幸存机器的磁盘状态。Arbiter 提供的是选举层面的可用性,不是数据层面的冗余,这个边界必须刻在脑子里。
2.2 什么时候该用 Arbiter,什么时候应该果断放弃
根据我的实际经验,下面几种场景是 Arbiter 的正確使用方向:
- 预算有限,但业务对写入连续性有要求,希望至少能扛住单台数据节点故障。例如主从两节点配合一个轻量仲裁节点,可以在主节点宕机时自动切换到从节点,避免人工干预。
- 开发或测试环境需要模拟三节点副本集的故障转移行为。用两台开发机加一个容器跑 Arbiter,成本和复杂度都远低于再申请一台全量数据节点。
- 现有副本集已经是偶数个数据投票节点,例如 4 个数据节点,想用 Arbiter 把投票节点凑成奇数,降低选举时出现平票的概率。
反过来,以下几种情况我会明确建议不要用 Arbiter:
- 业务无法容忍任何数据节点故障期间出现“只有一份数据副本”的状态。如果两个数据节点之一宕机后,剩下的唯一数据节点再出问题就会导致数据丢失,那么应该使用三个全量数据节点。
- 系统写入量很大,且你希望每个数据节点都承担冗余。Arbiter 不承接同步负载,对数据复制能力没有任何帮助。
- 节点数量已经是奇数且全量数据,例如已经是三个数据节点时,再加 Arbiter 并不会带来额外的高可用收益,反而增加维护点。
Arbiter 适用的核心条件是“你缺的不是数据副本,而是选票”。
2.3 双机房与三地点部署的讲究
部署位置是 Arbiter 架构里最容易翻车的地方。很多人图省事,把 Arbiter 和 primary 放在同一个服务器或同一个机柜,这等于在最关键的故障时刻把 Arbiter 和 primary 一起拖下水。如果主节点所在机房发生整段网络故障,Arbiter 也会同时失联,它那票就废了,剩余数据节点只有一票,无法完成选举,高可用效果归零。
更值得注意的场景是双机房部署。假设机房 A 放 primary 节点,机房 B 放 secondary 节点,Arbiter 放在机房 A。如果两个机房间的网络发生分区,机房 A 内部 primary 和 Arbiter 仍然互通,它们一共有两票,超过多数派阈值,primary 会继续保持主节点身份;而机房 B 的 secondary 由于只能拿到自己的一票,永远不可能被提升。这种情况下业务表面上没有中断,但写入只发生在机房 A,机房 B 的数据副本在分区期间停更,一旦 A 机房整体损坏,B 机房的数据滞后会造成数据丢失。所以如果只能在两个机房之间选一个放 Arbiter,应该优先把它放在故障域更独立、网络更稳定的位置,或者干脆把 Arbiter 放在第三个低延迟地点,让它在两个数据机房发生分区时保持中立和可达。
3. 搭建实操:从两台虚机加一台小机器开始
3.1 环境规划与版本选择
我下面的搭建步骤以 MongoDB 6.x/7.x 为基础,配置语法在 4.x 之后基本兼容,所以如果你用的是 5.0 或 8.0,核心流程也可以照搬。规划时准备三台 Linux 虚拟机或物理机,假设主机名分别是 node1、node2、arbiter。node1 和 node2 配备正常的数据盘,arbiter 只需要很小的磁盘空间,因为它的数据目录基本是空的。
| 角色 | 主机名 | 端口 | 资源配置建议 |
|---|---|---|---|
| Primary 候选 | node1 | 27017 | 2C4G 或以上,数据盘按业务量估算 |
| Secondary 候选 | node2 | 27017 | 与 node1 对齐 |
| Arbiter | arbiter | 27017 | 1C1G 足够,磁盘 5G 以上即可 |
三台机器之间要确保 27017 端口可以互通,防火墙和云安全组需要放行。生产环境必须启用认证,但为了让你先把架构跑通,下面的步骤先以开放认证的方式展开,我会在文末的安全章节单独说加认证的要点。
3.2 一份可复制的三节点配置
MongoDB 的节点角色不是从配置文件里强行指定的,而是通过复制集初始化命令决定的。每个节点的 mongod.conf 核心配置几乎一样,只是 replSetName 必须保持一致。
以 node1 为例,配置文件如下:
storage: dbPath: /data/mongodb systemLog: destination: file path: /var/log/mongodb/mongod.log logAppend: true processManagement: fork: true net: bindIp: 0.0.0.0 port: 27017 replication: replSetName: rs0关键是replication.replSetName这个字段。三个节点的配置里必须写同样的 rs0,否则启动后它们不会向同一个复制集注册。如果你用的是 systemd 托管 mongod,可以把processManagement.fork去掉,让服务直接在前台模式运行。
启动命令很简单,每台机器执行:
mongod -f /etc/mongod.conf启动后检查日志确认没有报错,并用下面的命令测试端口是否监听:
ss -lntp | grep 270173.3 初始化副本集并加入仲裁节点
三台 mongod 都启动完成后,登录其中一台,比如 node1,执行初始化命令。最简单的做法是一次性把三个成员全部写进初始化配置,Arbiter 节点通过arbiterOnly: true标记。
进入 mongo shell:
rs.initiate({ _id: "rs0", members: [ { _id: 0, host: "node1:27017" }, { _id: 1, host: "node2:27017" }, { _id: 2, host: "arbiter:27017", arbiterOnly: true } ] })如果你的环境里三个节点的 IP 固定但域名没配,host 字段写 IP 也可以,例如"192.0.2.11:27017"。初始化成功后,node1 会成为 primary。此时可以用rs.addArb()这种分步方式添加仲裁节点,也可以像上面这样一步到位,我更推荐一步到位,因为可以避免先初始化再修改配置时出现网络或角色状态不一致的概率。
执行完初始化后,在任意节点查看复制集状态:
rs.status()重点关注每个成员的 stateStr,期望看到的结果是:node1 为 PRIMARY,node2 为 SECONDARY,arbiter 为 ARBITER。如果 arbiter 长时间显示 STARTUP 或 RECOVERING,需要检查三台机器之间的网络连通性以及节点日志。
3.4 应用连接串与读写关注配置
副本集搭建完成后,应用的连接串不能只写单个节点地址,否则故障切换后客户端不会自动发现新主节点。正确写法是把多个数据节点作为种子列表,并指定 replicaSet 参数:
mongodb://node1:27017,node2:27017/?replicaSet=rs0&w=majority&readPreference=secondaryPreferred这里有几个参数值得展开说明。
replicaSet=rs0让驱动知道这是一个副本集,而不是多个独立 mongod。驱动会通过种子节点获取整个副本集的拓扑图,并持续跟踪主节点变化。w=majority表示写入需要得到大多数投票成员确认才向业务返回成功,在这个副本集里也就是两票。readPreference=secondaryPreferred表示优先从 secondary 读取,primary 只处理写入,这个配置适合读多写少的场景,可以在不动业务代码的前提下减轻主节点压力。
在代码里,不同语言驱动设置这些参数的方式略有差异,但连接的 URI 部分是通用的。需要注意的是,如果客户端只填了 primary 的地址而没带 replicaSet 参数,那它只是一个普通的单点连接,完全无法感知副本集的故障转移,这是很多线上事故低配版出现的原因。
4. 故障演练:把主节点干掉会发生什么
4.1 优雅降级与强制宕机两种玩法
搭建完成不等于真的高可用,我强烈建议你在上线前做一次故障演练。演练有两种方式,一种是优雅降级,一种是强制宕机。
优雅降级使用 Mongo 提供的主动让位命令:
rs.stepDown(60)这条命令会让 primary 主动放弃主节点身份,并且在 60 秒内不会重新参与选举。执行后观察 node2 和 arbiter 的投票过程,node2 大概率会在几秒后被选举为新的 primary。这种方式适合验证日常运维中的计划性切换,比如机器巡检、内核升级。
强制宕机用于模拟真实故障,直接杀掉 primary 进程:
kill -9 <mongod_pid>此时副本集会等待选举超时,默认大约 10 秒左右,然后 node2 发起选举,arbiter 投出支持票,node2 成为新的 primary。在故障发生时正在写入的数据,如果设置了w=majority,客户端会收到写入失败或超时错误;如果没有设置,部分写入可能只发生在旧主节点上,恢复后通过回滚机制处理。
演练完成后,观察rs.status()会看到 node1 回来后自动变成 SECONDARY,并开始从 node2 同步 oplog,把宕机期间丢失的数据补回来。这个过程不需要人工干预,但你需要关注同步延迟,可以用下面的命令观察:
rs.printSecondaryReplicationInfo()4.2 仲裁节点一起挂掉还能玩吗
Arbiter 挂掉的场景很多人没想过,但生产环境一定会遇到。我设计了几种故障组合,它们的表现差异很大。
场景一:secondary 宕机,Arbiter 仍然存活。此时 primary 和 arbiter 在线,投票数两票,满足多数派,primary 保持正常读写,只是少了一个数据副本。随后如果你的 primary 也宕机,二级节点的位置已经被第一次宕机占据,剩余二级 1 票 + arbitration 1 票 = 2 票,可以选主。但如果剩余数据节点挂掉,就只剩 arbiter 一票,永远选不出主。
场景二:Arbiter 宕机,primary 和 secondary 都在线。投票成员在线数为 2,仍构成多数派,因此 primary 不会自动退位,读写服务正常。但这个状态其实非常脆弱:如果此时 primary 再宕机,剩下 secondary 只有 1 票,达不到多数派阈值,整个集群将没有主节点,写入彻底中断。也就是说,Arbiter 单独挂机时,集群表面症状不明显,实则已经把容错能力从“允许一个投票节点离线”降到了“不允许任何额外故障”。
场景三:primary 和 Arbiter 同时挂机。此时只剩一个 secondary 在线,1 票 < 2,无法完成选举,集群会进入只读或不可用状态。这说明 Arbiter 在保存数据方面毫无帮助,如果希望两数据节点加一个仲裁的架构在“primary 和数据节点同时故障”时还能恢复写入,那你需要一个全量数据节点来替代 Arbiter 的角色,这也是我建议高风险场景用三数据节点的原因。
4.3 从演练结果反推架构设计思路
演练最常见的结论是“原来 Arbiter 挂了之后影响这么大”,其实 Arbiter 挂掉本身只是失去了一票,真正的问题是你把容错空间建在了一票之上。三节点副本集能容忍一台订阅服务器离线,但两数据节点加一仲裁的结构,如果仲裁离线,则容错能力退化为“不允许任何投票节点持续离线”。因此,Arbiter 不仅需要监控,它还需要更严格的监控告警。
另外,客户端同时配置w=majority时,写入返回成功只代表大多数投票成员确认了事务,但这个“大多数”可能包含了 Arbiter 的票。Arbiter 不存储数据,所以返回成功的数据仍然只存在于一个或两个数据节点上。这听起来有些反直觉,但确实是这样:majority 确认不是多数数据副本确认,而是投票成员的多数确认。所以当业务层要求严格的数据多副本确认时,不要依靠 Arbiter 凑数。
5. 常见坑与排查速查表
5.1 初始化阶段就翻车的几个状况
初始化副本集的时候,下面几个问题出现的概率非常高,我把现象、原因和解决办法整理成一张表:
| 现象 | 常见原因 | 解决方式 |
|---|---|---|
| rs.initiate 报错 no replset config has been received | 三台机器 replSetName 不一致,或未正确加载配置 | 检查 mongod.conf 中 replication.replSetName |
| 节点状态长期 STARTUP | 节点之间 27017 端口不通,bindIp 设置太严格 | 检查防火墙和安全组,bindIp 改为 0.0.0.0 或内网地址 |
| 添加 arbiter 后显示 RECOVERING | 网络有延迟,或 MongoDB 大版本不一致 | 等待 1-2 个心跳周期,仍异常检查日志 |
| 驱动连接时报 unknown replica set | 连接串没带 replicaSet 参数 | 在 URI 中追加 ?replicaSet=rs0 |
| rs.status() 能看到节点但客户端无法读写 | 客户端没有把 primary 地址包含进种子列表 | 连接串至少包含两台数据节点,再让驱动自动发现 |
这些错误最常见的根源其实只有一个:节点之间网络不通。MongoDB 复制集对网络质量非常敏感,初始化之前先用telnet或nc验证两两端口的连通性,可以省掉很多定位时间。
5.2 故障切换后的连接与数据同步异常
故障切换之后,第一次遇到的往往是客户端连接问题。如果应用只连接了旧 primary 的 IP,或者连接串没有副本集拓扑信息,新主节点上线后客户端并不会自动重连,体现为连接超时、写失败。使用官方驱动并配置了正确 URI 的情况下,驱动会通过拓扑感知自动切换,但连接池里的旧连接要等到请求报错后才会被丢弃重建,所以短暂的写入失败或超时在切换瞬间是正常的,业务层要做好重试。
另一个常见的故障是 secondary 恢复后一直追不上 primary,同步延迟不断增大。这种情况通常与 oplog 大小有关。如果备用节点离线时间太长,而 primary 的 oplog 已经覆盖了离线期间的日志,备用节点就找不到同步起点,只能通过全量同步重新建立数据副本。重新同步期间节点一直处于 RECOVERING 状态,需要占用大量网络和磁盘 IO。生产环境建议把 oplog 大小根据业务写入量设置为足够覆盖一天以上的量,并给全量同步预留窗口。
5.3 监控、备份和安全配置建议
Arbiter 不存数据,但它仍然是整个高可用架构的关键投票节点,因此不能因为资源占用低就忽视监控。至少要对 Arbiter 做端口存活、进程存活、心跳延迟的监控,有条件的话把它与数据节点放到同一套监控体系里,任何一台投票成员离线都要触发告警。数据节点的监控重点则包括主备切换事件、同步延迟、oplog 时间窗口、磁盘容量。
备份方面,Arbiter 不需要备份,但两台数据节点必须定期备份。推荐的做法是使用文件系统快照或mongodump定时导出,备份文件和 oplog 之间的时间点要通过恢复演练验证,否则备份等于白做。某些团队喜欢对隐藏节点做备份,这是一个更优的选择,因为它不会占用业务节点性能。
安全部分需要强调几点:生产环境必须开启访问控制和传输加密;如果已经启用了认证,Arbiter 也需要配置对应用户,并且在集群初始化时把 keyfile 放到所有节点上;数据库端口不要直接暴露到公网,只允许应用服务器网段访问。这些基础操作虽然不复杂,但很多高可用演练翻车就是栽在认证配置不一致上。
最后分享一个我个人的使用习惯:在资源允许的情况下,我更倾向于把 Arbiter 视为“过渡期方案”,而不是长期固定架构。当业务量上涨、数据重要性提升时,尽早把 Arbiter 替换成第三个全量数据节点;Arbiter 可以在数据节点同步完成后直接从配置中移除,整个过程中业务不会有明显感知。如果你只是想在低成本下获得自动故障切换能力,那两数据节点加 Arbiter 已经足以覆盖大多数中小业务的可用性要求,关键是别把它当成三数据节点的平替,想清楚自己的可用性目标,再决定怎么使用这一票。