ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MongoDB副本集实战:从单机到自动故障切换的高可用数据库

MongoDB副本集实战:从单机到自动故障切换的高可用数据库 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/db1Linux 下直接 /data/db1 这样的路径就行。为了便于理解我下面的操作示例统一用 Linux 风格路径你在自己机器上按系统替换成相应写法。3.2 三个节点的配置、端口、目录规划我习惯把实验环境规划得清清楚楚宁可多写几个目录也不要图省事全塞一个地方。这里建议你按照下面这张表来准备节点信息节点角色主机名/IP端口数据目录日志目录主节点初始localhost27017/data/mongodb-27017/data/mongodb-27017/log从节点 Alocalhost27018/data/mongodb-27018/data/mongodb-27018/log从节点 Blocalhost27019/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.add4.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 或 SECONDARYhealth表示节点心跳是否正常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 secondaryOkfalse。所以我在代码里先调用了rs.slaveOk()或者db.getMongo().setReadPref(secondary)目的明确地告诉驱动或 Shell我知道你在连从节点我就是要做读操作。你要是看到这个报错不用慌不是数据坏了而是 MongoDB 的自我保护机制在起作用避免客户端在不知不觉中读到可能滞后的数据。5.2 模拟故障关掉主节点会发生什么数据验证通过后就是副本集最激动人心的实验环节把主节点杀掉看集群怎么反应。这里我用最粗暴的方式模拟宕机直接杀掉 27017 端口的进程sudo kill -9 27017进程PID此刻你如果登录 27018 节点用 rs.status() 查看会发现短时间内 27017 节点的 health 变成 0stateStr 显示为(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 被修改过或者有多个网卡绑定了多个 IPbindIp 也必须显式写成客户端能访问到的地址否则就会出现一个进程起来了、日志却疯狂报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 的复制能力。
返回列表