1. 第 10 期,该从"能跑"跨到"挂了还能跑"了
如果你一路跟着这个 MongoDB 系列学过来,到这一期应该已经具备了几项基础能力:装好 MongoDB、用它自带的 Shell 或者 Compass 连接数据库、会建库建集合、能熟练做增删改查,也知道索引怎么写能让查询慢不了。这些都是单机环境下的"基本功"。但你心里应该一直有个不舒服的问题:我这样辛辛苦苦维护一个数据库,如果这台服务器突然宕机、进程崩溃、磁盘损坏,数据怎么办?服务是不是就彻底断了?
第 10 期的内容,就是解决这件事。副本集(Replica Set)是 MongoDB 从"单机玩具"走向"生产可用"的核心组件,也是后续学习分片、备份恢复、读写分离等一切高可用机制的地基。我会把创建副本集的知识点按实操逻辑重新梳理一遍:角色怎么分、为什么至少三个节点、怎么在同一台机器上模拟试验、初始化命令敲完会看到什么、故障发生时选举机制怎么工作、以及我在这个过程中踩到过的坑。
这一期对已经刷完前 9 期的朋友来说是一个阶段性的"转大人"时刻。对从中间跳进来、没看过前文的朋友也友好,因为副本集本身是一个相对独立的知识模块,你不需要回看太多历史文章,只要机器上装好了 MongoDB,并且知道启动和连接的基本操作,就可以照着下面的步骤搭出一套自己的副本集环境来。
2. 副本集运行前,必须搞懂的三个角色与选举机制
2.1 主节点、从节点、仲裁者,谁在干什么
副本集的成员角色一共三种:主节点(Primary)、从节点(Secondary)、仲裁者(Arbiter)。我先把它们之间的关系说透,不然你看后面的命令会觉得很蒙。
主节点是当前唯一能处理写请求的节点。客户端往里插的数据,先落主节点,再由主节点把操作记录写到自己的 oplog 里,从节点会持续拉取 oplog 并重放这些操作,从而保持与主节点一致。从节点不是摆设,它可以承担读请求,前提是客户端把读取偏好(read preference)设为 secondary 或 secondaryPreferred。仲裁者是最特殊的一个角色——它不存数据,也不参与主节点选举之外的任何业务,纯粹在某些投票场景中凑一票。
很多人刚接触仲裁者会问:既然它不存数据,那我加它进来有什么意义?答案是,它能在保证"选举可达成多数派"的前提下,降低你维护数据副本的成本。举个例子,如果你只有两个数据节点,主节点挂了,剩下那个从节点只有一票,无法形成多数派,那它永远无法被选举为主。这时加一个仲裁者进来,投票数变成三,挂了一个还有两票,多数派可以达成,集群就能恢复写入能力。仲裁者是个轻量进程,占用资源极小,适合放在另一台配置不高的机器上,目的就是省资源。
2.2 心跳与选举,主节点是怎么换人的
副本集里每个成员都会每隔两秒向其他成员发送一次心跳请求。如果主节点在超过 electionTimeoutMillis(默认是 10 秒)的时间里没有被其他成员感知到,那么从节点就会发起选举。这个机制有点像你们团队 Leader 突然失联,大家等了他十分钟还联系不上,于是有人提议"我们按规则选一个新 Leader 出来主持工作吧"。
选举不是随便投个票就完事了。能参与选举的节点需要有比较新的 oplog 数据,落后太多的节点是没有资格当主节点的,因为 MongoDB 要尽量避免主节点切换后数据回退。这是副本集设计中一个很重要的取舍:它优先保证数据不丢,其次才追求快速恢复。你平时看到的"mongod 故障后 10 秒左右自动恢复写入",就是指这整个探测加选举的过程。
还有一个很多人理解有偏差的地方:从节点在主节点存活时,会不会主动竞争?不会。副本集不存在负载均衡式的主节点轮换,只要主节点健康,其他节点会一直保持从节点状态。除非你手动执行 rs.stepDown() 让主节点让位,或者触发优先级机制强制重新选举。
2.3 为什么最少三个节点,而不是两个
官方推荐副本集至少三个成员,我的建议是,如果不是纯粹做实验,不要在生产环境搭两个节点的副本集。因为副本集的高可用依赖于"多数派"这个概念。副本集的总票数越多,能容忍故障的节点数也越多:3 个节点最多挂 1 个,5 个节点最多挂 2 个。但两个节点的情况下,只要其中一个挂了,剩下的那个最多也就是拿到自己的一票,凑不够两票的多数,这时候副本集只能进入只读模式,甚至某些操作会直接拒绝服务,等于高可用失效。
反过来说,节点数也不建议盲目增加,因为每多一个数据节点就多一份全量数据和 oplog 的同步开销。常见的生产组合是三个数据节点,或者两个数据节点加一个仲裁者。前者适合对数据冗余要求更高的业务,后者适合成本敏感、已经有备份机制的场景。第 10 期实验我用三个数据节点,因为仲裁者的操作在理解了角色之后其实是追加一条命令的事,初学阶段先把三个数据节点的完整形态跑通更有价值。
3. 单机环境做 3 节点副本集实操,前置准备与配置要点
3.1 没有三台服务器怎么做实验
看到这里你可能会说,我没有三台云服务器,副本集实验是不是就做不了了?不是。副本集的所有节点只需要满足一个条件:host 和 port 组合能被其他节点访问到。这三个节点完全可以放在同一台机器的不同端口上,MongoDB 的安装包也只装一份,然后用三个不同的数据目录、三个不同的配置文件分别启动三个进程即可。
你如果正在用 Windows 做开发,或者在 Linux 服务器上做测试,这个方法都成立。唯一的差别是数据目录的路径写法,Windows 下要注意写反斜杠时要转义,或者干脆用正斜杠如 D:/mongodb/data/db1;Linux 下直接 /data/db1 这样的路径就行。为了便于理解,我下面的操作示例统一用 Linux 风格路径,你在自己机器上按系统替换成相应写法。
3.2 三个节点的配置、端口、目录规划
我习惯把实验环境规划得清清楚楚,宁可多写几个目录也不要图省事全塞一个地方。这里建议你按照下面这张表来准备节点信息:
| 节点角色 | 主机名/IP | 端口 | 数据目录 | 日志目录 |
|---|---|---|---|---|
| 主节点(初始) | localhost | 27017 | /data/mongodb-27017 | /data/mongodb-27017/log |
| 从节点 A | localhost | 27018 | /data/mongodb-27018 | /data/mongodb-27018/log |
| 从节点 B | localhost | 27019 | /data/mongodb-27019 | /data/mongodb-27019/log |
目录先创建好,再用 mongod 命令分别初始化。每个节点需要有一个独立的配置文件,我通常命名为 mongod-27017.conf、mongod-27018.conf、mongod-27019.conf。其中一个最小可用的配置内容长这样:
storage: dbPath: /data/mongodb-27017/data systemLog: destination: file path: /data/mongodb-27017/log/mongod.log logAppend: true net: port: 27017 bindIp: 127.0.0.1 replication: replSetName: rs0 processManagement: fork: true其中 27018 和 27019 的配置文件只需要把 port、dbPath、log path 改成对应值,其它内容都不用动。关键点是 replication.replSetName 必须设成同一个名字,名字可以随便起,但不能三个节点各叫各的,否则它们永远不认为彼此属于同一个副本集。我这次统一用的 rs0,你可以起名 dev0、test_cluster,都行,只是后面所有 mongosh 命令和配置里要保持一致。
3.3 启动前的两个小检查,避免 20 分钟后才发现方向错了
启动节点前有两个检查非常重要。第一,确认机器的 27017-27019 这几个端口没有被其他进程占用,尤其如果你之前用过旧版本 MongoDB,可能有一个单机版 mongod 还跑在 27017 端口上,那后面初始化副本集时会很不顺。在 Linux 下用ss -lntp | grep 2701看看,一秒钟就知道有没有进程占着。第二,确认你已经在配置文件里写好了 replSetName,因为如果你先启动成单机模式,数据目录里已经写入了默认配置,后面就算加了复制配置重启,报错也会很奇怪。我见过不少初学者在这儿卡住,其实改配置前多想一步就能避开。
检查完以后,用三个终端或后台方式分别执行:
mongod -f /etc/mongod-27017.conf mongod -f /etc/mongod-27018.conf mongod -f /etc/mongod-27019.conf启动完成后,可以先随便连其中一个看看日志。如果你用--fork开启了后台守护,进程会直接返回,启动信息里会有一个waiting for connections的提示,那说明 mongod 已经准备好了。没加 fork 的话终端会一直挂着跑,那就多开几个 SSH 会话或者用 systemd 方式管理,看你自己习惯。
4. 创建副本集的完整命令链路,从 rs.initiate 到 rs.add
4.1 先在一个节点上完成初始化
三个进程都起来之后,用 MongoDB Shell 登录第一个节点,也就是规划表里的 27017 端口。这一步不需要三台都操作,只在其中一个节点上执行初始化,它会成为副本集的起点。
mongosh --port 27017进入 Shell 后,直接执行:
rs.initiate({ _id: "rs0", members: [ { _id: 0, host: "localhost:27017" }, { _id: 1, host: "localhost:27018" }, { _id: 2, host: "localhost:27019" } ] })你可能会在网上的旧教程里看到 rs.initiate() 不带参数、后面再用 rs.add() 逐个加成员的写法。没毛病,那也是一种操作路径。但我更推荐初始化时就把三个成员一次性定义好,因为这样更接近"一份既定的集群规划",不容易漏加节点,而且 rs.initiate() 返回的配置确认也更符合预期。
如果一切顺利,Shell 会返回一个包含 ok: 1 的文档,同时你注意到命令行提示符可能变成了类似rs0 [direct: primary]的样子。这里有个细节:如果你看到的是[direct: primary],说明你已经在一个可用的主节点上了,但当前 Shell 的连接方式是 direct,也就是直连到某一个节点而不是通过副本集连接串连接。在实验阶段这没问题,生产环境则建议用包含多个节点地址的连接串,让客户端自动发现谁是主节点。
4.2 rs.status() 怎么看,重点看哪些字段
初始化完成后,我习惯立刻执行一次rs.status(),确认副本集处于健康状态。这个命令会输出一大段 JSON,初次看到容易眼花,这里我给你圈出最关键的几项:
rs.status()输出中的members数组很重要,每一项代表一个节点。其中你该最关心三个字段:stateStr表示节点当前的角色,正常应该是 PRIMARY 或 SECONDARY;health表示节点心跳是否正常,1 代表正常;lastHeartbeat表示最近一次心跳时间,如果这个时间非常陈旧,说明节点间通信可能有问题。
如果你执行 rs.status() 时发现三个节点全处在 SECONDARY 或者 STARTUP 状态,主节点迟迟不出现,通常不是你没配好,而是在初始化后很短时间内的正常现象。因为成员之间正在同步配置、建立心跳,给它们 10 到 20 秒再查一次,状态就会稳定下来。要是等了很久还是只有一个主节点,另一个节点卡在 STARTUP,可以登录那个节点看日志,最常见的原因是 bindIp 配置只允许 127.0.0.1 但 host 里写了局域网 IP,或者数据目录之前已经存在非副本集方式启动的数据,这个踩坑率高得离谱。
4.3 加仲裁者或不加仲裁者,两种场景都演示一下
前面说了三个数据节点是更完整的实验形态,但生产上也大量存在"两个数据节点 + 一个仲裁者"的部署,所以我把仲裁者怎么加也一并讲了。假如你现在的副本集只有两个数据节点,且想补一个仲裁者,登录当前 PRIMARY 执行:
rs.addArb("localhost:28017")仲裁者同样需要一个 mongod 进程和一个配置文件。它的配置几乎与数据节点相同,唯一要额外强调的是,仲裁者不应该设置太大的 oplog,也不用分配太多磁盘,因为它不复制业务数据。如果你在部署仲裁者的机器上发现磁盘空间不够,那是正常的,不用担心。
反过来,如果你初始化的节点数量已经满足需求,那就什么都不用加。实验环境下,我更推荐保持三个数据节点,之后测试故障转移的体验更直接。仲裁者适合在理解了数据节点的同步机制之后再折腾,没必要为了"集齐所有角色"而刻意加一个。
5. 副本集真正香的地方:主节点宕机后的自动切换实测
5.1 先验证数据同步是否正常工作
副本集不是建完就完事了,你得确认数据真的会在节点间自动同步。建好副本集后,我建议在主节点上插入一批测试数据,然后故意去从节点查一下,确认能查到,这样能直观地验证同步链路是通的。
在主节点上执行:
use testdb db.users.insertMany([ { name: "zhang san", age: 25 }, { name: "li si", age: 30 }, { name: "wang wu", age: 28 } ]);然后直接换个端口登录从节点查数据:
mongosh --port 27018rs.slaveOk() db.getMongo().setReadPref("secondary") use testdb db.users.find()这里有一个新手绕不开的坎:MongoDB 默认不允许在从节点执行读操作,会报错not primary and secondaryOk=false。所以我在代码里先调用了rs.slaveOk()或者db.getMongo().setReadPref("secondary"),目的明确地告诉驱动或 Shell:我知道你在连从节点,我就是要做读操作。你要是看到这个报错,不用慌,不是数据坏了,而是 MongoDB 的自我保护机制在起作用,避免客户端在不知不觉中读到可能滞后的数据。
5.2 模拟故障,关掉主节点会发生什么
数据验证通过后,就是副本集最激动人心的实验环节:把主节点杀掉,看集群怎么反应。这里我用最粗暴的方式模拟宕机,直接杀掉 27017 端口的进程:
sudo kill -9 <27017进程PID>此刻你如果登录 27018 节点用 rs.status() 查看,会发现短时间内 27017 节点的 health 变成 0,stateStr 显示为(not reachable/healthy)。最多十几秒后,27018 或 27019 其中一个节点的 stateStr 会从 SECONDARY 变成 PRIMARY。这时整个集群恢复写入能力。
值得注意的是,新主节点出现意味着你之前插入的那三条数据依然能查到,因为它们在主节点挂掉之前已经通过 oplog 同步到了从节点。但这有一个前提:你写入数据时使用的是默认写关注(writeConcern)。如果业务代码把写关注设成了{w: 1},意味着主节点只要写入本地就算成功,不等从节点确认,那么主节点在同步完成前崩溃,这部分数据就可能丢。这一块在真正上生产时要仔细权衡,不是副本集存在就等于数据百分百不丢,它保证的是数据库服务在节点故障后可自动恢复,至于精确到毫秒级的数据零丢失,需要更高的写关注级别配合。
为了看清故障转移的前后行为,我通常会在主节点挂掉之前拿一个终端持续执行 db.hello() 或者 rs.isMaster(),观察 primary 字段的地址变化。从 primary 变成新节点 IP 的那一瞬间,代表选举已完成。整个过程在我的普通配置电脑上大约是几秒到十几秒,取决于选举超时配置和节点心跳间隔。
5.3 旧主节点复活后,数据会怎么补齐
故障实验不能只做一半,当你重新启动之前被杀掉的 27017 节点,它会发现副本集中已经有一个更新的主节点,于是自动以从节点身份加入集群,并把落后的 oplog 补上。你不需要手动干预什么,它会追平与主节点之间的数据差距,最终回到 SECONDARY 状态。
这就是副本集天然具备的"自愈"特性。我建议你也做一次完整验证:重启旧节点后,在它还不是主节点的情况下,再往当前主节点插入几条数据,等一小会儿再从旧节点读,确认新数据也同步过去了。通过这个步骤,你能彻底明白副本集和"双机热备"那种需要手动切换的方案之间的本质不同。
6. 副本集实验中最容易翻车的几个细节与经验补强
6.1 最大翻车点:配置差异与主机名解析
同一个副本集里,_id、replSetName、成员 host 写法不一致是排障时最先要检查的地方。成员 host 如果写的是localhost,那么所有节点最好都用localhost,不要一部分写 localhost 一部分写 127.0.0.1,也不要一部分写公网 IP 一部分写内网 IP,因为这些写法可能被解析成不同的地址,导致节点间互相不认识。此外,如果 MongoDB 所在机器的 /etc/hosts 被修改过,或者有多个网卡绑定了多个 IP,bindIp 也必须显式写成客户端能访问到的地址,否则就会出现一个进程起来了、日志却疯狂报Failed to connect to的问题。
我之前做过一个线上案例,就是两个 MongoDB 节点在同一台物理机,配置文件里 host 一个写机器名、一个写 IP,从节点一直无法同步。排查到最后,根本不是数据或权限问题,就是名字解析不一致。这个坑在跨网段部署时尤其常见,所以我现在每次创建副本集前都会先敲一遍hostname和hostname -I,确保配置文件和系统解析一致。
6.2 端口占用、操作系统防火墙、数据目录残留
如果创建副本集后某个节点一直STARTUP,另一个直接报地址已被占用,优先怀疑端口冲突或防火墙拦截。本机实验时防火墙一般不是大问题,但如果你在云服务器上跨机器组副本集,一定要把 27017-27019 的安全组规则和操作系统防火墙一起放通,只放某一个端口有时候不够,因为节点间通信不光走数据端口。这种情况我会先用telnet或nc从另一端测端口连通性,确认网络层没问题再看 MongoDB 日志,避免一上来就挖日志浪费时间。
还有一个极其隐蔽的问题:dbPath目录里已经有了旧数据。如果你在这台机器上之前用普通单机模式启动过 mongod,数据目录会污染新创建的副本集。遇到这种情况,要么清空数据目录重新初始化,要么把副本集的 dbPath 指向一个全新的目录。实验环境我建议直接清空重来,反正没什么重要数据,但生产环境千万别手滑执行 rm 删除数据目录。
6.3 后面的优化方向:从实验到生产的三个补强
实验跑通后,你离真正的生产部署还差几步,这里给你列一个扩展清单,后续可以按顺序补:
| 扩展点 | 作用 | 建议时机 |
|---|---|---|
| 开启访问控制与内部鉴权 | 防止未授权节点加入副本集、防止数据裸奔 | 上线前必须做 |
| 调整 electionTimeoutMillis | 让故障切换时间更符合业务容忍度 | 监控数据积累后调优 |
| 增加备份策略与延迟节点 | 防止误删数据、提供时间点恢复能力 | 有真实写入流量前 |
| 配置连接串与驱动 readPreference | 让读写压力分散到从节点 | 写并发开始增长时 |
前 9 期你在 MongoDB 上做的都是一对一的命令操作,从这一期开始,你接触的东西开始有了"分布式系统"的味道。后面不管你是要学分片集群,还是要给应用做高可用架构,副本集的经验都是垫脚的石头。第 10 期我就梳理到这里,但我自己更愿意把它当作一个起点:当你亲手杀掉一个主节点,看着数据在几秒后自动恢复,那一刻你才算真正开始信任 MongoDB 的复制能力。