MongoDB副本集这个名字,很多人一开始会误以为是“多复制几份数据”那么简单。我最早也是这样,直到自己在生产环境里把三节点副本集搭起来,然后亲眼看着主库意外宕机、从库自动顶上、业务只断了十几秒,才算真正理解它背后的选举机制和同步逻辑。如果你正在规划一个数据不能丢、服务不能长时间中断的存储层,或者已经用了MongoDB但一直是单节点在跑,这篇手册应该能帮你少走不少弯路。这篇内容围绕三节点副本集展开,覆盖架构选型、部署初始化、高可用原理、日常运维与故障排查的完整链路,适合负责基础设施的工程师,也适合后端开发顺手兼职运维的团队。我会把实际部署时踩过的坑和项目里的处理方式一起写出来,尽量让每个步骤都可以直接照着做。
1. 先从“三节点”说起:为什么是3台而不是2台或5台
1.1 副本集到底是什么,和主从复制差在哪
MongoDB从很早的版本就开始推动副本集取代传统的主从复制。主从复制其实很简单:一个主节点负责写,一个或多个从节点负责复制数据,但主节点挂了之后需要人工切换,而且从节点默认是只读的,整个流程非常依赖人工介入。副本集则是在这个基础上增加了自动故障转移能力,节点之间通过心跳协议互相感知状态,一旦多数派节点认定主节点不可用,就会触发选举,选出一个新的主节点。
用生活化一点的方式理解:主从复制就像只有一个班组长的车间,班组长请假,整个生产线就得停下来等人来调度;副本集则是一个有投票权的小组,班组长缺位时,小组成员按规则投票选出新组长,整条线继续运转。这个“自动”带来的价值,在凌晨三点的故障里最能体现,你能多睡半小时,业务也不至于一直处于写失败的状态。
副本集里的角色主要有三种:
- Primary(主节点):唯一的写入口,所有写操作都走它。
- Secondary(从节点):同步主节点的oplog,可以承担读流量,也可以作为故障切换的备选。
- Arbiter(仲裁节点):只参与选举投票,不存数据。
三节点副本集最常见的形态是“一主两从”,三个节点都有完整数据,任何一个挂掉,剩下两个仍然能形成多数派,选出新主。相比之下,“一主一从一仲裁”虽然也是三节点,节省了一台数据节点的磁盘成本,但数据冗余只有一份,如果从节点也同时出现故障,数据恢复的压力会比较大。我在生产环境里更倾向于一主两从,虽然看起来“浪费”了一点存储,但换来了更稳的容灾冗余。
1.2 三节点的典型布局与选型理由
为什么不是2台?因为副本集的选举遵循多数派原则,一个只有两个数据节点的副本集,如果其中一台宕机,剩下那一台无法构成多数派,它既不能选自己为主,也不能继续对外提供写服务。换句话说,两节点副本集的高可用能力约等于零,主节点一挂,整个集群就只读甚至不可用了。所以三节点是兼顾成本和高可用的最小推荐规模。
为什么不是5台?五节点确实容错能力更强,允许同时挂两台节点仍能选举,但节点越多,选举通信和复制的网络开销越大,运维复杂度也直线上升。很多业务场景的数据量单副本可能就一两TB,复制三份还能接受,五份的磁盘与带宽成本就会显得非常沉重。三节点在大多数内部系统、中小型互联网应用中已经足够,只有对可用性要求极高、资金和运维资源充裕的团队才需要上五节点或跨机房多副本集架构。
节点部署位置也是个值得考虑的细节。三台机器如果放在同一个机柜,那只能防硬件单点;如果要防整个机房级别的故障,就需要把三个节点分散到不同机房或可用区。但这里有一个优先级问题,同一副本集的节点如果跨机房,网络延迟和带宽波动会影响心跳和复制效率,跨城跨地域部署尤其明显。我的建议是:先保证三节点同区域低延迟,再用备份或额外的一套异地副本集来做区域容灾,而不是强行让一个三节点副本集跨千里距离。
2. 部署前必须想清楚的事:规划、版本与配置
2.1 节点规划表和硬件建议
部署之前,把节点规划清楚能省掉后面大部分麻烦。一个常规的三节点副本集至少需要下面这些信息:
| 角色 | 节点IP/主机名 | 端口 | 数据目录 | 日志目录 | 用途说明 |
|---|---|---|---|---|---|
| Primary | mongo-node-01 | 27017 | /data/mongodb | /var/log/mongodb | 初始主节点 |
| Secondary | mongo-node-02 | 27017 | /data/mongodb | /var/log/mongodb | 备节点 |
| Secondary | mongo-node-03 | 27017 | /data/mongodb | /var/log/mongodb | 备节点 |
不要把数据目录和系统盘放在一起,尤其不要放在根分区。MongoDB的写入模式是预分配数据文件,日志和磁盘碎片会让数据文件持续增长,如果根分区被写满,数据库会拒绝写入甚至导致进程崩溃。数据目录尽量挂载到独立的磁盘或云盘上,并确保有足够的预留空间。建议初始规划时就按未来18个月的数据增量倒推磁盘容量,同时留出20%到30%的余量给oplog和临时排序文件。
内存方面,MongoDB的存储引擎默认使用LRU算法将热数据缓存在内存中,官方建议容量规划时尽量让活跃数据集能放进内存,但这在实际项目中很难做到。经验值是一台机器至少16GB内存起步,小业务8GB也能跑,但不要用2GB的机器硬扛生产库。CPU核心数建议4核以上,因为副本集的读写、压缩、选举等操作都需要CPU参与,核心数太低会在高峰期出现CPU饱和,进而拖慢复制进度。
节点之间采用内网或低延迟网络互通,强烈建议配置主机名解析,IP直连也行,但后期如果要迁移机器或调整网络,主机名解析会让配置维护容易得多。
2.2 版本选择与安装方式
版本是第一道坑。MongoDB的大版本迭代非常快,每个大版本的生命周期和特性差异都很大,尤其是从4.x到5.x再到6.x,很多默认行为都变了。我的建议是:
- 先确认内核和驱动是否已适配目标版本。
- 生产环境尽量选择一个“已经发布至少半年”的稳定小版本。
- 不要直接上最新的大版本,除非你有专门的测试环境先跑够足够长的时间。
举例来说,如果你现有系统的驱动是旧版,可能不支持新版本的服务端协议,升级后出现兼容性错误。另外,副本集的特性在不同版本间也有差异,例如5.0后新增的变更流增强、6.0后的部分查询改进,这些都很诱人,但升级前必须验证所有业务SQL和聚合管道的行为没有被改变。
安装方式上,官方提供的压缩包解压安装是最可控的,tar包不依赖系统包管理器的版本锁定,可以精确控制二进制版本。如果你用的是Linux发行版的包管理源,比如直接apt install mongodb-org,建议先安装官方源而非系统自带源,否则装出来的可能是社区打包版,配置文件路径、默认参数都和官方版有差异。
安装完成后,不要急着启动服务。先创建专用的运行用户和目录权限,很多初学的人直接root启动,后面不仅日志权限混乱,还可能被安全扫描工具告警。简单做法是创建一个mongod用户,把数据目录、日志目录的属主指给它,再用systemd或init脚本以这个用户启动。
2.3 配置文件的核心参数解释
MongoDB的配置文件从4.x开始使用YAML格式,用起来非常直观。我这里给一个生产环境常用的最小配置模板,并按段落解释每个参数的含义。
systemLog: destination: file path: /var/log/mongodb/mongod.log logAppend: true logRotate: reopen storage: dbPath: /data/mongodb journal: enabled: true wiredTiger: engineConfig: cacheSizeGB: 8 processManagement: fork: true pidFilePath: /var/run/mongodb/mongod.pid net: bindIp: 0.0.0.0 port: 27017 replication: replSetName: rs0 security: authorization: enabled keyFile: /etc/mongodb-keyfile说明几个关键地方:
- replSetName必须配置,三台机器的副本集名称必须完全一致,否则无法加入同一个副本集。
- bindIp如果用0.0.0.0,意味着所有接口都监听,生产环境一定要靠防火墙或安全组限制来源IP。
- journal.enabled建议保持true,虽然会带来一点性能开销,但能保证意外断电后数据文件的一致性。
- cacheSizeGB需要根据服务器内存比例调,默认值是物理内存的一半,如果机器上还跑着其他服务,建议手动调小到内存总量的25%~40%,否则容易把系统内存吃满。
- keyFile用于节点间的内部认证,三台节点的keyFile内容必须一样,这是所有节点能互相通信和选举的前提。
这里有个容易忽略的点:开启authorization之后,副本集节点之间即使在同一内网,也需要通过keyFile认证。很多人配置了keyFile但没开启authorization,或者反过来,都会导致节点之间认证失败,表现为主节点上看不到对端心跳,状态一直显示UNKNOWN。
2.4 系统和网络层准备
系统层面的坑主要集中在这几块:
- 文件句柄限制。MongoDB需要大量文件描述符,在/etc/security/limits.conf里要适当调大,建议至少65535。如果不调,等到数据文件一多就会报too many open files。
- 关闭透明大页。Linux的THP(Transparent Huge Pages)可能会让MongoDB的内存分配和性能出现明显抖动,很多数据库都建议关闭。临时关闭可以用echo never > /sys/kernel/mm/transparent_hugepage/enabled,持久化配置则要看具体发行版的启动脚本。
- 时间同步。副本集选举和心跳依赖节点间的时钟,虽然MongoDB并不严格要求NTP完全同步,但时钟偏差太大会让日志时间紊乱,排查问题时会非常痛苦,还是建议使用NTP或chrony保持各节点时间一致。
- 防火墙配置。如果三台节点之间有防火墙,放行27017端口和集群内部流量。同时一定要限制外部来源对这个端口的访问,最好的办法是仅允许应用服务器和运维跳板机的来源IP访问。
3. 从零到一的部署流程:三节点副本集初始化实录
3.1 三台节点的安装与启动
我这里用tar包方式举例,因为不同发行版的包管理差异比较大,tar包反而最统一。
首先下载对应版本的压缩包后,解压到/usr/local/mongodb目录,或者按公司规范放到/opt/mongodb。然后创建运行用户:
useradd -r -s /bin/false mongod mkdir -p /data/mongodb mkdir -p /var/log/mongodb chown -R mongod:mongod /data/mongodb chown -R mongod:mongod /var/log/mongodb三台节点都执行同样的操作,然后把上面的配置文件写入到/etc/mongod.conf,不同节点的dbPath和日志路径保持一致即可。
启动方式可以用官方提供的systemd单元,也可以直接执行:
sudo -u mongod /usr/local/mongodb/bin/mongod -f /etc/mongod.conf启动后用mongo shell连上去检查一下:
mongosh --host 127.0.0.1 --port 27017如果能看到test库并且没有报认证错误,说明单节点启动正常。
但要注意:现在这三个节点还只是独立的单节点,它们之间并不知道彼此的存在。复制集信息还没有初始化,我们需要连接到某一个节点,用rs.initiate()把它变成副本集,然后再把其他节点加入进来。
3.2 在主节点上初始化副本集
假设我们选择mongo-node-01作为初始主节点,登录这台机器,在mongosh里执行:
rs.initiate({ _id: "rs0", members: [ { _id: 0, host: "mongo-node-01:27017" }, { _id: 1, host: "mongo-node-02:27017" }, { _id: 2, host: "mongo-node-03:27017" } ] })rs.initiate()执行后会马上触发一次选举。如果三台机器都配置好了主机名解析,并且网络互通,执行后通常几秒内就能看到状态变成PRIMARY。
然后添加用户。副本集开启认证之前,先创建一个管理员用户:
db.getSiblingDB("admin").createUser({ user: "root", pwd: "一个足够复杂的密码", roles: [{ role: "root", db: "admin" }] })创建用户后,再把认证参数启用。启用认证需要重启所有节点。重启顺序建议是:先重启两个从节点,最后重启主节点。因为如果先重启主节点,它会尝试联系其他节点,而其他节点还没起来,短时间内会产生无主窗口。先重启从节点,再切换主节点,最后重启原主节点,可以最大程度减少服务中断时间。
重启之后,每个节点都加载了keyFile和authorization配置,再用admin用户登录验证:
mongosh -u root -p "密码" --authenticationDatabase admin --host mongo-node-01 --port 27017执行rs.status()看看members字段,正常应该是三个节点都是HEALTHY,主节点显示PRIMARY,两个从节点显示SECONDARY。
3.3 高可用与读写验证
副本集初始化结束并不代表真正完成了,建议做一轮验证测试。
先验证写读路径。在Primary上写一条测试数据:
db.test.insertOne({ name: "hello", value: 1 })然后直接到Secondary上查询,你会发现默认是查不到的。这不是数据没同步,而是从节点默认的读策略不允许直接读。需要执行rs.secondaryOk()或者在连接串里设置readPreference=secondary。
接着验证自动故障转移。首先确认当前的Primary是哪个节点,比如是mongo-node-01,然后模拟宕机:
systemctl stop mongod或者更硬核一点,直接shutdown掉进程。等待大约10到15秒,然后用mongosh连到mongo-node-02或者03,执行rs.status(),你会看到新的PRIMARY产生。刚才写入的测试数据也应该在读取时能看到。
这里有一个最常见的误解:故障转移不是瞬时的,主节点挂了之后,其他节点需要经过一个“心跳超时”才会发起选举。默认的选举超时是10秒,加上探测和主从切换,业务侧会有一个短暂的不可写窗口。如果你的业务对写入可用性要求极高,需要在应用层做重试或队列兜底。
4. 高可用的关键原理与日常运维要点
4.1 选举发生的过程
副本集的高可用核心是选举。每个节点会定期向其他节点发送心跳,默认每2秒一次。如果一个节点在10秒内没有收到主节点的心跳,它就会认为自己看到的“主节点”失联,于是向能通信的节点发起选举请求。只有获得副本集中大多数节点的同意,它才能成为新的主节点。
为什么需要“大多数”?因为只有这样才能避免脑裂。假设两台节点之间网络断开,但它们都还活着,如果没有多数派约束,两边都可能认为对方挂了,然后各自把自己选为主,出现两个都能接受写入的节点,这就是脑裂。加入多数派规则后,对于三节点副本集,必须至少得到2票,任何单方面一组都凑不齐票数,所以不可能出现两个合法主节点。
顺便说一个容易被忽视的点:心跳和选举走的是同一套节点间连接,如果网络质量差,经常出现心跳中断,就会引发频繁的选举抖动。所以在部署时,节点间延迟越低越好,最好不要在副本集节点之间加可能产生乱序和延迟的复杂链路。
4.2 主节点降级和从节点同步机制
主节点挂了之后,从节点会被选举为新主。但这里的逻辑是,旧主节点如果突然恢复网络,它发现自己已经不再是大多数派认可的主节点,就会主动降级为从节点,并同步新主的oplog,这个过程叫rollback。如果旧主上还有一些尚未同步给新主的写入,这些操作可能会被回滚,存在丢失风险。
为了降低回滚概率,应用程序在对安全性要求高的写入中应该使用writeConcern: majority。含义是每次写操作必须被大多数节点确认后才返回成功。如果主节点宕机前已经得到多数节点确认,那么即使新主节点接管,旧主的写入也不会轻易被回滚。
从节点通过拉取主节点的oplog来同步数据。oplog是MongoDB的“复制日志”,它记录了对数据的所有操作。连接串中的readPreference=secondary可以让读流量打到从节点上,但要注意,Secondary的数据存在一个极短的延迟窗口,如果业务对强一致读有要求,不建议盲目开启读从节点。
4.3 日常运维:节点更换、滚动升级、配置修改
副本集运维中最常见的操作是换节点。比如某台物理机即将到期,需要把mongo-node-03替换掉。正确流程并不是直接关掉旧机器,而是先在副本集中把它降级并移除,再启动新节点加入。
操作大致是:
// 在新节点启动并配置好同一个 replSetName // 然后连接到主节点 rs.add("mongo-node-new:27017") // 等新节点同步到最新状态,再移除旧节点 rs.remove("mongo-node-03:27017")注意,rs.add()之后,新节点会先执行初始化同步,如果数据量大,磁盘IO会被拉满,建议在业务低峰期操作。同时要保证新节点硬件规格不低于旧节点,避免成为副本集的短板。
滚动升级的思路也类似。先升一个从节点,验证没问题,再逐个升级其他节点,最后切换主节点并升级旧主。整个过程可以做到对业务无感知,前提是业务侧已经配置了合理的连接串。
配置修改中有一个高频操作是调整oplog大小。MongoDB在初始化后不能直接修改oplogSizeMB配置项,这个问题我放到后面的常见故障里详细讲。
4.4 监控指标和告警建议
高可用不代表不需要监控,恰恰相反,只有监控到位,故障转移才能在业务可接受的时间窗口内发生。我最常用的几个监控维度:
- 主节点是否稳定。盯rs.status()里的health和stateStr,任何状态异常都应该立刻告警。
- oplog时间窗口。通过rs.printReplicationInfo()查看oplog的time字段,如果窗口只有几个小时,说明从节点同步压力很大,或者oplog太小。
- 复制延迟。通过db.adminCommand({replSetGetStatus: 1})里的optimeDate字段对比,主从延迟超过几秒就要关注。
- 磁盘和内存。磁盘使用率超过80%就要扩容或清理;swap使用率过高说明内存不足。
- 连接数。连接数突然暴涨通常是连接池配置不当或应用侧严重异常。
告警阈值可以结合业务容忍度调整,但有一个底线建议:副本集任何一个节点进入RECOVERING或UNKNOWN状态,都要第一时间处理,因为某些状态是拖垮整体可用性的前兆。
5. 避坑手册:常见故障与排查实录
5.1 新从节点一直处于RECOVERING状态
新加一个从节点后,执行rs.status()看到它的状态是RECOVERING,而且迟迟不同步,多半是初始化同步没完成,或者同步过程中断。先看日志,通常会有大量网络或连接超时错误。
一种常见原因是新节点正在做initial sync,但这个节点磁盘或网络资源太弱,导致同步速度很慢。另一种原因是oplog窗口太小,在初始化同步期间,新节点需要从主节点拉取某个时点之后的全部操作,如果oplog已经覆盖不到那个时点,它会一直尝试重试。
处理方式一般是清掉新节点的数据目录重新初始化同步,并在初始化前适当调大oplog,让它有足够的时间窗口完成首次同步。对于数据量特别大的集合,可以先在新节点上用mongodump从已有的从节点做一个备份,再恢复到新节点,最后再把它加入副本集,这样可以避免全量拉取时把主节点IO打满。
5.2 主从节点延迟突然增大
延迟增大的原因主要有三类:
- 主节点上有耗时非常长的写操作,比如大规模批量更新或聚合管道,导致oplog产生速度过快。
- 从节点的硬件性能不足,复制线程跟不上主节点的写入压力。
- 从节点网络带宽受限,或者连接数过多导致复制连接被挤占。
排查顺序是先看从节点日志,然后看rs.status()里最新的optimeDate差异,再用mongostat观察oplog和应用线程的指标。如果能快速定位是某个大操作引起,可以暂停或限流该操作;如果是从节点能力不足,最直接的办法是升级从节点规格或把部分读流量切走。
5.3 磁盘写满导致节点进入RECOVERING
数据目录所在的磁盘被写满后,MongoDB进程会不断尝试写入日志,但由于空间不足,写入失败,节点会慢慢变得不可写。此时如果它是主节点,会直接影响写业务;如果是从节点,会停止同步并可能进入RECOVERING状态。
处理办法是:先清理无用日志或临时文件,释放部分空间,然后确认数据目录能恢复写入。如果释放空间后节点仍然异常,再考虑物理扩容。更稳妥的做法是提前用监控盯住磁盘使用率,在达到90%前就触发扩容流程。
5.4 网络分区导致频繁选举
三节点分布在两个机房或多个可用区时,如果链路不稳定,节点之间心跳频繁超时,就会触发反复选举。这种现象最坑的地方是,业务没有开始写,副本集却在后台不断切换主节点,应用端的连接因此频繁失效。
处理思路有几个方向:避免跨地域组一个副本集;如果必须跨可用区,要保证链路可靠并有足够带宽;同时可以适当调大心跳超时参数,让节点对短时抖动更宽容。不过调大超时参数的副作用是故障转移变慢,需要综合权衡。
5.5 oplog大小怎么调整
前面提到,oplog大小在配置文件中可以设置,默认大小是磁盘剩余空间的5%,对于复制压力大的场景可能不够。如果发现主从延迟一直偏高,或者写入高峰期明显出现复制追赶的情况,就需要调大oplog。
直接修改配置文件然后重启是不生效的,因为副本集启用后oplog集合已经创建好了。正确方式是登录主节点,在admin库下执行:
db.adminCommand({replSetResizeOplog: 1, size: 20480})size单位是MB,这里示例表示把oplog调整为20GB。执行后不需要重启节点,但最好在业务低峰操作。调整后通过rs.printReplicationInfo()确认time窗口变大到合理水平。
5.6 备份恢复的实用经验
副本集本身不等于备份。如果所有节点都部署在同一个机房,而且发生了机房级故障,三份数据一起丢也不是不可能。所以必须有独立的备份机制。
常用的备份方案有两种:
- 从某个Secondary节点做mongodump,不直接连Primary,避免对主节点增加额外压力。
- 使用文件系统快照或者Percona Backup for MongoDB,能做到更接近一致性的备份。
恢复时也要在临时机器上先搭一个单节点实例,恢复数据并验证逻辑完整性,再把它作为新节点加入生产副本集。千万不要在主节点上直接执行restore,先不说风险,光是需要停写这一点,绝大多数业务都承受不住。
6. 一些补充:安全基线与应用侧接入建议
6.1 连接串设计与读写偏好
应用连接MongoDB副本集时,最标准的连接串格式是:
mongodb://user:password@host1:27017,host2:27017,host3:27017/?replicaSet=rs0&readPreference=secondaryPreferred&w=majority这里面readPreference可以选primary、primaryPreferred、secondary、secondaryPreferred、nearest。如果业务允许读延迟,用secondaryPreferred可以把压力分摊到从节点;如果必须是刚写入的数据立即可读,那只能选primary或者使用writeConcern为majority配合读主节点。
w=majority是生产建议,它意味着写入时至少要得到多数节点确认。相比默认的w=1,它的优点是故障切换时基本不会丢数据,缺点是每次写要等额外一次确认,延迟略高。实测下来,内网环境下增加的延迟通常在几毫秒内,绝大多数业务都能接受。
6.2 身份认证和最小权限
很多初学部署的人为了省事,创建了root角色用户后就直接给应用程序用,这是非常不规范的做法。生产环境最好为每个应用单独创建数据库用户,只授予该应用所需数据库的读写权限,不要给任何全局管理权限。
例如,业务应用只需要操作appdb:
db.getSiblingDB("appdb").createUser({ user: "appuser", pwd: "独立强密码", roles: [{ role: "readWrite", db: "appdb" }] })运维和管理使用单独的admin账号,审计日志和操作日志要留存。这样即使应用侧连接串泄露,攻击者也不能直接删库或修改副本集配置。
6.3 备份策略实践建议
我习惯对副本集采用“全量+增量”的组合,而不是每天全量备份。每天凌晨从某一个Secondary节点执行mongodump全量备份,同时开启MongoDB的变更流或使用oplog时间戳做增量备份脚本。这样恢复时先用最近的全量备份恢复到临时实例,再重放增量操作,能把RPO缩短到分钟级甚至秒级。
这里的难点是增量重放时,必须确保数据没有重复写入,操作需要做幂等处理。更省事的方案是使用专门的备份工具,但无论如何,备份的可恢复性要定期演练,不能只备份不复原。我就见过某团队备份脚本连续跑了几个月,真到恢复时才发现备份文件不完整,那才是最头疼的。
写在最后的几点体会
把三节点副本集跑起来,其实只是高可用存储的起点。真正难的是后续的日常运维、故障预案和容量规划。我自己在这几年里经历过主节点半夜宕机自动切换、也从节点同步延迟连续报警、磁盘被日志塞满差点把数据库冲垮,每一次排查都把操作手册翻了一遍又一遍。要说最大的体会,莫过于不要盲目依赖“高可用”这三个字,副本集解决的是自动故障转移的问题,但它不等于数据零丢失,也不等于所有故障都能无缝恢复。三节点这个规模,在很多场景下已经足够稳定,但前提是你对它背后的心跳、选举、oplog和多数派机制都有清晰认识。如果你正准备从单节点迁移到副本集,可以先拿一个测试项目练手,按照这篇手册把部署、切换、换节点、备份恢复全部走一遍,再到生产环境动手,你会发现大部分坑其实都已经有人替你踩过了。