☰
MongoDB 手动主从切换全流程实战:rs.stepDown 演练指南
2026/10/19 5:41:12 网站建设 项目流程

1. 什么场景逼着你手动切换:不只是机器要挂那么简单

去年某天凌晨两点,值班手机把我在睡梦中吵醒。某核心业务库的 primary 所在宿主机磁盘剩余空间不到 2%,再过半小时可能就写不进去了。业务方在群里说绝对不能停,必须在十分钟内把主切到另外一台已经准备好的从节点上。等我打开运维文档,发现手动主从切换那一节只有一句“执行 rs.stepDown()”。当时对着 mongosh 敲命令的时候,心里是真没底。从那以后我就把 MongoDB 手动主从切换列入了必须定期演练的清单,今天这篇内容就是那几次演练沉淀下来的完整过程。

很多人觉得 MongoDB 复制集的自动故障转移已经解决了高可用问题,手动切换根本用不上。这个想法大错特错。自动选举解决的是“节点进程挂了”的场景,但运维中有一大堆情况是进程没挂、心跳正常、自动选举永远不会触发,可你就是得把主切走。下面列几个我实际遇到的场景。

1.1 磁盘告警与硬件维护:最常见的切换触发条件

宿主机磁盘空间告警是头号原因。日志涨得快、慢查询日志、备份文件、mongod 的 journal,任何一个把磁盘塞满,写操作就会开始报错。磁盘快满的时候 MongoDB 不会自动把 primary 让给别人,它只是在那边硬扛,直到写不进去业务雪崩。同样的情况还有宿主机内存 ECC 报错、CPU 型号不一致导致性能差异过大、网卡固件要升级、内核要打补丁。这些统统不会触发自动选举,只能手动切。

还有一种很容易被忽略的情况:机房割接。你所在公司可能用某家云厂商,也可能自建机房,不管哪种,只要底层网络设备要维护、机柜要迁移、供电要改造,都意味着某个节点要下电。下电前必须把 primary 位置挪走,否则等你把机器关了才发现集群没有主,那真是叫天天不应。

1.2 让主节点回到更适合的位置:资源水位与角色错位

还有一种常见情况是角色错位。比如你新采购了一台高性能机器,或者某台从节点所在宿主机特别空闲,你希望 primary 落在资源更充裕的节点上。虽然 MongoDB 可以通过设置 priority 来自动促成这种结果,但 priority 机制生效需要一次选举,而正常运行时除非当前主挂掉或退位,否则不会重新选。这时候你依然需要手动 stepDown 一次,让集群重新选举,把主“弹”到目标节点。如果你拖着不处理,高性能节点空转还无所谓,真正的问题是低性能节点因为承载写入而持续高负载,早晚拖垮整个集群。

1.3 从 primary 上赶走异常流量:应用层故障隔离

还有一个我遇到过的场景,某个应用侧发了 bug,写入流量翻了几十倍,把 primary 的 CPU 和 IO 全部打满。这时候开发在紧急修复,业务又不能停,我们就得先手动把 primary 切到另一台负载较低的从节点上。注意,stepDown 并不是负载均衡器,切换之后异常流量会跟着打到新 primary 上,所以这只能算缓兵之计,真正解决问题的还是把应用的异常写入挡住。但它确实能给从节点之间争取一些喘息时间,尤其在你想快速恢复只读能力的时候,非常管用。

1.4 自动故障转移的盲区:为什么高可用不等于高可用演练

最关键的认知是:MongoDB 自动故障转移依赖心跳和选举,但很多“看起来不正常但进程没死”的情况,它根本不会自动处理。比如 primary 节点网络延迟飙升到几百毫秒,但心跳仍然能通过;比如磁盘 IO 等待时间很长,写入已经慢到不可接受,但进程还活着;比如出现死锁或长事务把某个关键操作卡死,但节点状态仍是 PRIMARY。这些场景下自动选举不会触发,必须有预案、有脚本、有人敢在凌晨执行这一步。高可用系统的价值恰恰在于:你能在它需要切换的时候,知道切换会发生什么、会丢多少数据、对应用造成多大影响。这些只能靠演练来验证,靠文档凭空想象是没有用的。

2. 演练前吃透 rs.stepDown():它到底替你做了什么事

2.1 stepDown 的三段式执行过程

rs.stepDown() 并非简单地把节点降级了事,它背后有一套协调流程。以命令rs.stepDown(60, 15)为例,整段执行大致分为三个阶段。

第一阶段,当前 primary 会停止接受新的写入,在客户端视角里它开始表现为“即将退位”。但这和进程被杀不一样,它不会立刻放弃自己的主身份,而是先检查集群中是否有从节点的 oplog 已经追平到自己的最新位置。

第二阶段是等待从节点追平。如果发现还没有从节点追平,它会启动一个等待计时,也就是第二个参数secondaryCatchUpPeriodSecs控制的窗口。在这个窗口内,从节点会尽力追赶 primary 的 oplog,追得越接近,后续切换的数据丢失窗口就越小。如果从节点在窗口内成功追上,切换会相对平滑;如果没追上,窗口时间一到,它仍然会执行降级。

第三阶段是触发选举。当前节点广播自己已经变成 SECONDARY,复制集成员重新发起 leader 选举,获得多数派票数的新节点成为 PRIMARY。而原 primary 在第一个参数seconds指定的时间内不能再次参与选举,这个冷却期就是为了防止它刚退下来又被立刻选回去,导致集群角色来回抖动。

2.2 两个参数到底怎么设置

seconds指退位冷却时间,默认为 60 秒。运维演练时我一般设 60 到 120 秒。如果设置太短,比如 10 秒,可能出现切换完成后集群刚稳定,原主又参与选举并胜出,造成二次切换。这个过程中应用的连接池会再次经历一次拓扑刷新,两波抖动叠加起来,业务侧会很痛苦。

secondaryCatchUpPeriodSecs指等待从节点追平 oplog 的时间上限,默认是 10 秒。建议演练时设 15 秒左右。这个值不是越大越好,因为如果从节点本身复制延迟已经很大,等再久也追不上,反而拖延整个切换的完成时间。10 到 15 秒对于常规健康集群已经足够。

我整理了一个参考表,方便后续演练时快速决策。

参数默认值推荐演练值备注
seconds6060 - 120冷却期越长,避免反复切回
secondaryCatchUpPeriodSecs1010 - 15给从节点追 oplog 的时间

2.3 与 kill -9 自动故障转移的本质区别

很多人觉得手动切换和直接杀死 primary 进程是一样的,反正都会触发选举。差别非常大。直接 kill 掉 primary,集群立刻进入无主状态,从节点们会马上发起选举,但新上任的 primary 的 oplog 位置可能落后于旧 primary,旧节点上最近写入的一部分数据可能在新主上不存在。

而rs.stepDown()给了一次优雅切换的机会:它在退位之前先让从节点尽量追平 oplog。虽然这不保证数据绝对不丢,但至少把丢失窗口压到了最小。真正决定数据丢不丢的其实是 writeConcern。如果应用写入时用的是writeConcern: majority,那这条数据必须被多数节点确认才返回,切换时多数节点里至少有一个持有该数据,所以不会丢。如果应用用的是w=1甚至不关心写确认,那最近的一部分写入只存在于旧 primary 内存和 oplog 里,无论优雅 stepDown 还是 kill,切换后都可能消失,这是很多业务埋下的雷,平时看不出来,一切换就炸。所以我一直强调:演练前先检查应用的写入确认级别,别等到切完再去找数据。

2.4 一个容易忽略的细节:priority 和回切

stepDown 的冷却期结束后,原 primary 会恢复参选资格。如果它的 priority 还是默认的 1,而且它的 oplog 是最新的,那么在下一轮选举中它很可能会再次胜出,变成 primary。这意味着如果你执行完 stepDown 后没有及时处理,集群可能“自动回切”到你头上。演练时如果只想让某个节点暂时退出主角色,更稳妥的做法是临时把它的 priority 改成 0,等维护完成后再改回来。但注意修改 priority 本身可能触发选举,顺序上必须先确认目标节点已经追平 oplog,再操作。

3. 完整演练过程:预检、切换、验证、回切的一个完整闭环

3.1 演练前的预检清单

磨刀不误砍柴工。我每次演练前都要过一遍检查项,任何一个不满足就直接取消演练,绝不硬上。

第一,复制集至少要有三个数据节点,或者两个数据节点加一个仲裁节点。无论如何要保证多数派可用,因为 stepDown 之后必须能选出新主。如果一个从节点已经挂了,这时候不要做演练,先把挂的节点修复或剔除。

第二,用rs.status()查看每个成员的状态。重点关注health是否等于 1,stateStr是否是 PRIMARY 或 SECONDARY,以及各节点的optimeDate差距。我在 mongosh 里常用下面这段脚本快速检查:

// 在 mongosh 中对复制集任一节点执行 rs.status().members.forEach(m => { print(m.name, m.stateStr, "health:", m.health, "lagMs:", Date.now() - (m.optimeDate ? m.optimeDate.getTime() : 0)); });

第三,检查 oplog 窗口和复制延迟。用rs.printReplicationInfo()看 oplog 总量和时间窗口,用rs.printSecondaryReplicationInfo()看从节点复制延迟,正常情况下应该是 0 秒或非常接近 0。如果从节点落后太多,先暂停演练,排查复制延迟原因。

第四,检查应用的 MongoDB 连接串。必须确认 URI 是完整的三节点副本集形式,类似mongodb://node1:27017,node2:27017,node3:27017/?replicaSet=rs0&retryWrites=true,并且驱动版本支持自动拓扑刷新。如果发现连接串只写了单节点地址,演练前必须先让应用修改配置,否则切完应用必挂。

第五,确认业务处于低峰期。虽然 MongoDB 复制集设计上能承受切换,但任何切换都有抖动窗口,选在低峰期演练,既是对业务负责,也方便从容处理意外。

3.2 执行 stepDown:命令的正确姿势与预期输出

预检通过后,登录当前主节点,先执行rs.isMaster()确认角色,再执行切换。我习惯用完整的 admin 命令格式,便于日后写脚本:

// 连接当前 primary db.adminCommand({ replSetStepDown: 60, secondaryCatchUpPeriodSecs: 15 });

执行这条命令后,mongosh 通常会卡住一小会儿,时间取决于从节点追平速度,然后会返回一个对象,常见输出是{ok: 1}。接着你在这个会话里再执行rs.isMaster(),会发现自己已经变成 SECONDARY。有些版本下命令执行期间连接会短暂断开,这也是正常的,因为服务端已经把自己降级了。

我建议在另一台终端上同时执行监控命令,实时观察集群状态变化:

// 在任一从节点上循环观察 while (true) { const s = rs.status(); const primary = s.members.find(m => m.stateStr === "PRIMARY"); print(new Date().toISOString(), primary ? primary.name : "NO PRIMARY"); sleep(1000); }

切换完成后,新 primary 会在几秒内出现。正常情况下整个选主过程在 10 秒内完成,加上 catch-up 等待,整体应该在 20 秒左右。如果超过 30 秒还没有新主,说明有异常,先看rs.status()里各节点的stateStr和lastHeartbeatMessage。

3.3 切换后的业务验证清单

角色切过去不代表演练成功,业务验证才是关键。我会按顺序做下面几件事。

第一,在应用服务器上用驱动自带的健康检查接口确认能够写读。最简单的方式是直接跑一条 insert 和 find,确认新主可写、从节点可读。如果有多个从节点,也要轮流检查一遍连通性。

第二,看应用日志和监控告警。重点看有没有连接异常报错、超时、NotPrimary 之类的关键字。驱动侧如果配置了 retryWrites,切换瞬间的写失败会自动重试,账面上可能无感,但日志里可能有重试记录,这很正常。

第三,观察新 primary 的资源水位。CPU、内存、磁盘 IO、连接数都要看,确认新主能够扛住现有流量。如果新主是老旧机器,配置明显低于原来的主,那这个切换可能不适合长期维持。

第四,让切换后的状态稳定运行至少 10 到 20 分钟,再决定是否回切。如果演练目标是“切走并保持”,那就保留新主,把原主降为从节点观察。如果只是验证切换能力,完成后要执行回切。

3.4 把主切回去:回切也要按流程来

回切操作本质上是再执行一次 stepDown,只不过方向相反。比如现在新 primary 是 node2,你想把主还给 node1,就登录 node2 执行同样的命令:

db.adminCommand({ replSetStepDown: 60, secondaryCatchUpPeriodSecs: 15 });

前提是 node1 已经追平 oplog,并且 priority 没有被改成 0。回切完成后,同样要走一遍业务验证流程。这里有个容易踩的坑:回切太急。如果刚切完不到几分钟就立刻回切,集群会在短时间内经历两次切换,业务侧的连接池和客户端拓扑缓存会连续抖动,一些老驱动甚至会因此进入不健康状态。我的做法是两次切换之间至少间隔 10 分钟,让连接池完全稳定下来再说。

下面是我每次演练都会打印的一份检查清单表格,你可以直接拿去改。

阶段检查项预期结果
切换前复制集多数派可用至少 3 个健康节点
切换前从节点复制延迟0 秒或接近 0
切换前应用连接串包含 replicaSet 和 retryWrites
切换中stepDown 返回值ok: 1
切换后新 primary 出现时间30 秒内
切换后业务写读验证成功
切换后应用日志无持续报错
回切前稳定运行观察至少 10 分钟
回切后再次业务验证成功

4. 我在演练中踩过的坑:排查链路和补救方法

4.1 应用连接池不刷新,持续报 NotPrimary

我遇到过最典型的问题:集群本身切得好好的,新 primary 也确认了,但某个应用服务一直报NotPrimaryNoSecondaryOk或NotWritablePrimary错误,日志里刷屏。第一反应会觉得是 MongoDB 的问题,其实问题出在应用侧连接串。

排查链路是这样的。先确认集群状态:用rs.isMaster()在应用服务器上验证新主地址可达。然后看应用日志,发现它一直在尝试连接旧 primary 的 IP。最后翻驱动配置,发现 URI 写死成了mongodb://old_primary_ip:27017/db,既没带replicaSet参数,也没带上其他节点地址。这种配置下驱动压根不知道这是一个副本集,只会傻傻地连一个节点,切换之后当然连不上。

解决办法是把 URI 改成完整的副本集连接串,加上replicaSet=rs0&retryWrites=true,重新发布应用。排查过程中最耗时的其实是定位,因为一开始大家都在怀疑 MongoDB 选主出了问题。所以现在我把“验证应用连接串”列进了演练前预检清单的第一优先级,宁可多花五分钟检查,也不想等切换完再被业务方找上门。

4.2 stepDown 之后集群短暂没有主:投票配置和 priority 的问题

另一次演练中,我执行 stepDown 后,集群竟然整整 40 秒没有出现新主。当时第一反应是选举被卡住了,立刻值班查看rs.status(),结果发现一个非常隐蔽的配置问题:复制集里一个数据节点的priority被设成了 0,另一个是hidden: true,而我在集群里总共只有两个数据节点加一个仲裁节点。表面看有三个投票成员,实际上真正的候选者只有一个,而那个候选者追 oplog 花了点时间,加上选举超时配置偏大,整体拉长了无主时间。

排查链路是先看rs.status()的每个成员stateStr,再看配置里priority和votes字段,最后对比选举相关的settings.electionTimeoutMillis。解决办法是把候选节点的 priority 调成正常值,确认配置后重新选举。这一步给我的教训是:复制集成员的 priority 和 votes 配置不能想当然,尤其经历过其他人调整后,必须每次演练前都验证一遍。

4.3 长事务或慢写拖住 stepDown,命令长时间不返回

还有一次执行 stepDown 时,命令迟迟不返回,像是卡死了一样。当时第一直觉是网络问题,后来在另一个会话里运行db.currentOp(),发现当前 primary 上有一个长时间运行的聚合操作,一直占用着资源。stepDown 过程中需要等节点达到一个相对安静的状态,这个慢操作导致降级流程迟迟走不完。

排查链路是这样的:新开一个 mongosh 连接,执行db.currentOp()过滤出长时间运行的写操作和事务,结合appName或client字段确认是哪个应用发起的操作。确认后和业务沟通,或者直接db.killOp(opid)终止该操作。kill 之后再次执行 stepDown,命令立刻返回,切换顺利完成。

这里必须提醒一句:killOp是双刃剑,杀错业务操作可能造成数据不一致或业务报错。所以遇到这种情况我优先联系业务方确认操作是否重要,实在联系不上才会在风险评估后再做决定。演练前先看看 currentOp,能提前避免这个麻烦。

4.4 单节点或二节点复制集上执行 stepDown:把自己玩没了

这个坑发生在一次测试环境里。某同事在一套单节点复制集上执行了rs.stepDown(),结果整个复制集没有 primary,集群直接不可写。单节点复制集本来就没有任何人能参与选举,stepDown 之后没有任何节点能顶上来,必须人工干预才能把主“救”回来。二节点复制集也一样,除非有仲裁节点,否则 stepDown 后也大概率选不出主,因为多数派票数凑不够。

正确的做法是:测试环境最差也要用“两个数据节点加一个仲裁节点”的结构,生产环境至少三节点。演练前用rs.status()数一下健康节点数,少于三直接取消操作。这个坑一旦踩了,恢复过程非常难看,甚至可能要去底层手动修改 local 库的 replication 配置,风险极高。

4.5 seconds 设太短导致的反复弹回

有一段时间我把 seconds 参数设成 10 秒做演练,想着快点结束。结果第一次切换完成后,原主在冷却期一过立刻重新参与选举,而且因为它数据最新、priority 又一样,顺利再次当选。于是集群在不到一分钟内又切了回去,应用侧经历了两次切换,产生了大量连接重连告警。后来我把 seconds 固定为至少 60 秒,回切时至少间隔 10 分钟以上再操作,这个反复弹回的问题就再也没有出现过。

5. 让演练成为常规动作:复盘模板和自动化雏形

5.1 每次演练都要有记录:复盘模板比命令更值钱

演练的意义不只是练一次手,而是通过每次演练发现周边配套系统的问题。我建议每次演练后都要填写一张简单的复盘表,至少包含以下字段。

字段示例
演练日期2024-xx-xx 02:00
演练目的验证 stepDown 流程与监控告警
执行人值班 DBA
切换前 primarynode1
切换后 primarynode2
是否回切是 / 否
无主窗口时长约 8 秒
业务影响无 / 少量重试
应用日志异常无 / 连接重试
发现的问题连接串未配置 retryWrites
改进项应用侧修改 URI 并重发

这张表的价值在于:半年后你再看到某次切换时发现“无主窗口 40 秒”,就能立刻回忆起是 priority 配置问题,而不是临时查一堆日志。复盘记录是运维团队最宝贵的资产。

5.2 演练频率和触发节点

我建议至少每季度做一次完整的手动切换演练,并且在以下几种情况下强制加练一次:复制集拓扑变更后、MongoDB 大版本升级后、机房或网络架构调整后、应用连接串统一改造后。每次演练都选在业务低峰期,提前和业务方、开发团队同步时间窗口,争取大家的配合。

5.3 自动化切换脚本的思路与边界

说到自动化,我的态度是“半自动”就好,不建议全自动。原因很简单:stepDown 本身只是一条命令,但切换前后的预检、验证、告警确认都需要人来判断。如果盲目把整个流程自动化,一旦漏掉某个前置条件(比如从节点落后、priority 配置异常、连接串单点),就是生产事故。

可以做的是把固定、机械的部分包装成脚本。比如切换命令的封装:

#!/bin/bash # 传入当前 primary 的地址,执行 stepDown 并输出结果 PRIMARY_HOST="$1" mongosh "mongodb://${PRIMARY_HOST}:27017/admin" --quiet \ --eval 'const res = db.adminCommand({replSetStepDown: 60, secondaryCatchUpPeriodSecs: 15}); printjson(res);'

再比如切换后的角色确认脚本:

// 等待新 primary 出现,最多等 60 秒 const deadline = Date.now() + 60000; let found = false; while (Date.now() < deadline) { const s = rs.status(); const p = s.members.find(m => m.stateStr === "PRIMARY"); if (p) { print("New PRIMARY:", p.name); found = true; break; } sleep(1000); } if (!found) { print("ERROR: no PRIMARY within 60s"); }

这两个脚本组合起来,基本覆盖了“切换 + 验证”的主干。但前置预检、业务验证、告警确认这些环节,我仍然坚持人工把关。

5.4 最后分享一点个人经验

演练的次数多了之后,我发现真正能反映团队运维水平的不是 stepDown 命令敲得有多熟练,而是切换前后那一整套配套动作是否完整:预检是否做足、监控是否覆盖、连接池是否能感知拓扑变化、回切是否克制、复盘是否到位。每次演练完我都会把发现的问题修复掉,而不是记录完就丢到一边。下次演练前再翻一遍上一次的复盘表,很多坑就不会再踩第二遍。如果你所在的团队还没有做过一次完整的手动主从切换演练,建议从今天开始补上,至少先从测试环境完整走一遍流程。

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

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

立即咨询