MongoDB 副本集的手动主从切换,是我在运维工作中用得非常频繁的一个动作。线上维护、版本升级、硬件下架,很多场景都需要通过rs.stepDown()让 primary 优雅让位,再对节点动手。这篇文章我会把从原理到参数再到完整演练的全过程记录下来,特别是执行这条命令之前要做的那些检查,以及切换之后怎么验证,帮还在踩坑的同行少走弯路。如果你刚接手 MongoDB 副本集的运维,或者正在更新团队的高可用演练手册,这篇内容可以直接参考。1. 为什么需要手动主从切换:四个最典型的运维场景1.1 硬件下架与基础维护物理机的生命周期里总有下架的那一天。某次巡检发现一台数据库节点的 RAID 卡开始报错,或者内存条需要更换,又或者是机房要调整机柜布局,这些情况都要求我们把这台机器上的 MongoDB 实例安全地停下来。如果这台机器恰好是副本集的 primary,直接关机固然可以——副本集会通过心跳超时感知到主节点失联,然后自动发起选举。但问题在于,自动选举的时机不可控,写入中断的窗口取决于心跳超时参数和当时的网络状况,极端情况下业务会感知到一段明显的拒绝服务。更好的做法是先在计划窗口内执行rs.stepDown(),让这台实例优雅降级为 secondary。让位之后,它会正常参与心跳,数据复制也不会中断,你可以在任意时间从容地停掉实例、处理硬件、再重新启动。整个过程对业务来说,只是连接池里主节点的位置换了一台,写入路径几乎无感。这个动作我在处理硬件告警时反复用过,哪怕最后发现只是虚惊一场,切换一次也让团队对流程更熟悉。1.2 版本滚动升级的标准姿势MongoDB 的大版本升级不建议在同一节点原地直接升,更稳妥的方式是逐个节点滚动升级。滚动升级的前提就是先把 primary 让位出去,优先升级那些当前没有承载写的节点。等副本集里的多数节点都已经升到新版本,最后再处理原来的 primary,或者让它保持 secondary 继续追数据。这里有个容易忽略的细节:很多生产副本集可能已经启用了 featureCompatibilityVersion 之类的版本门禁特性。如果你在旧版本 primary 还在服务写入时,就去升级别的节点,大概率不会触发什么问题;但如果你试图让一个跑着旧版本的节点继续承担 primary 角色,而其他节点已经升到新版本,可能出现版本兼容性上的警告甚至报错。提前 stepDown 可以让你掌握切换的主动权,先在低峰期完成版本过渡,再决定什么时候让原主节点重新参与竞选。1.3 高可用演练不能只做形式很多公司的合规和稳定性要求里,明确写了数据库每半年或每季度要做一次主备切换演练。演练的目的不是把副本集切坏,而是验证整套故障转移链路是否真的可靠:监控告警能不能在切换瞬间发出;应用连接池能不能快速感知新主节点;备份脚本在 primary 漂移之后是否仍然指向正确节点;值班人能不能在日志里判断切换是否正常完成。如果从来不演练,等真正故障发生时再去看文档就是临时抱佛脚。我见过有团队在演练时才发现,某个从库因为故障被标记为 FATAL 状态已经有三个月,health 一直是 0,但在日常监控里没人注意。等到需要它顶上 primary 时才暴露问题,那就不是演练该干的事了。所以建议把手动主从切换当成例行巡检的一部分,每次演练都用rs.stepDown()真正把角色换一遍,再换回来。1.4 多机房拓扑下的流量收敛当副本集成员分布在两个机房,应用的流量却集中在一个机房时,我们通常希望 primary 落在离应用更近的那个机房,减少跨机房写延迟。如果因为某次故障转移导致 primary 漂到了远端机房,写路径延迟可能从 1ms 涨到几十 ms,业务响应时间会明显劣化。这时候手动切换就是最直接的解决办法。你可以通过rs.stepDown()把远端机房的 primary 降级,并临时把它设置为不可竞选状态,让本地机房的 secondary 通过选举成为新主。如果想让这样的角色分配长期稳定,光靠一次 stepDown 还不够,后面要配合 priority 设置,否则下次排障时误操作又可能让 primary 飘回去。这部分在第 5 章我会展开讲。2. 动手之前先把 rs.stepDown() 的原理和参数吃透2.1 一条命令背后发生了什么rs.stepDown()是 MongoDB 提供给 shell 的辅助方法,底层调用的其实是replSetStepDown管理命令。在 primary 上执行它时,节点会做的事情可以拆成几步:首先把自己的内部状态从 PRIMARY 切换为 SECONDARY;然后向副本集中的其他成员广播一个信号,告诉它们我不再是主,你们可以发起新的选举;最后,当前节点会进入一个禁选窗口,在这个窗口内它不会参与 primary 竞选。为什么要设置禁选窗口?这是防止主节点反复横跳的关键设计。如果没有窗口,节点刚让位出去,下一秒别的节点还没来得及完成选举,它发现副本集没有 primary,又原地把自己选回去,主角色就会在两个节点之间来回抖动。数据库的角色切换虽然不重,但每次都意味着连接池重连、缓存失效、未完成的事务回滚,频繁发生对业务伤害很大。默认情况下这个窗口是 60 秒,意味着 stepDown 后至少一分钟内,这台节点不会重新成为 primary。另外要注意,rs.stepDown()必须连接到当前的 primary 上执行。如果你连到一台 secondary 上执行,会收到类似 not primary 的错误,因为让位这件事只有主节点才有资格做。这也是很多新手第一次操作时报错的原因。2.2 seconds 和 force 参数怎么选方法签名是rs.stepDown(seconds, { force: boolean })。第一个参数就是上面说的禁选窗口时长,默认 60 秒。我在生产环境一般习惯给到 120 到 300 秒,原因很简单:让位之后运维人员需要时间去观察新主是否正常、应用是否重连、有没有异常告警。如果窗口太短,大概率会出现你还在排查问题时,旧节点已经重新具备竞选资格,又因为优先级最高被选回去了,等于白切一次。第二个参数force要谨慎使用。默认force: false时,如果副本集当前没有足够多的投票节点在线来选出一个新主,stepDown 会失败或阻塞。force: true则是告诉节点不管有没有谁能接任,你都立刻降级。这种操作通常用于极端情况,比如多数派投票节点已经彻底失联,但你仍然希望当前节点尽快让位去配合其他动作。代价是副本集可能在一段时间内完全没有 primary,所有写请求都会失败,直到网络恢复或人工干预。我在实际运维中几乎不会在正常计划内用force: true,它更适合故障场景下的兜底,而不是高可用演练的常规动作。如果你发现自己经常需要 force 才能完成切换,那说明副本集的架构设计本身有问题,更应该先回去审视投票节点的布局。2.3 和 freeze、底层命令的横向对比搞运维的通常会同时看到几个相似操作,容易混。我把常用的整理成一个对比表:操作适用角色核心作用典型使用场景rs.stepDown(sec, {force})primary立刻让位,并在指定秒数内拒绝当选 primary主动维护、升级前让位rs.freeze(sec)任意成员指定时间内不参与 primary 竞选;在 primary 上执行同样会触发让位让某节点在窗口期内回避竞选db.adminCommand({replSetStepDown:1, force:true})primary底层命令,shell 方法只是它的封装脚本化运维、精确控制参数rs.freeze()的语义和 stepDown 比较接近,但它更偏向选举策略控制。比如你想把一台性能较差的节点排除在选举之外,或者你想保证某个维护窗口内 primary 不会被自动切换走,给相关节点 freezes 一段时间就够用。而 stepDown 的目的很直接:我现在就要退出主角色,你来顶上。另外有人会拿kill或db.shutdownServer()做对比。强杀进程确实会触发自动选举,但那是被动高可用,窗口较长且不受控;stepDown 是协作式让位,主动通知其他成员,业务中断窗口通常只有秒级,甚至更低。对于计划内动作,永远优先用 stepDown。3. 主从切换完整演练:从检查清单到回切验证3.1 演练前的环境评估清单我把每次做切换演练前必查的内容整理成了一张清单,照着走基本不会漏:rs.status()检查所有成员的状态,确认每个节点都是 health:1,stateStr 符合预期(PRIMARY/SECONDARY/ARBITER),没有 FATAL 或 STARTUP 状态。检查投票节点是否都在线。必要时用db.hello()或rs.isMaster()看主节点视角下的成员列表。查看复制延迟:rs.printSecondaryReplicationInfo()会打印每个 secondary 的复制进度和滞后量。延迟过大的节点不适合被选为新主。确认应用连接串里配置了多个副本集成员地址,并且开启了retryWritestrue。如果连接串只指向旧 primary 一台机器,切换后应用根本找不到新主。确认写关注级别。建议生产环境至少用w: majority,这样在切换瞬间未提交到大多数节点的写操作不会轻易回滚。检查当前正在执行的长事务和慢查询:db.currentOp()里如果有长时间运行的写操作,最好等它们结束再切换,或者评估中断成本。确认备份任务没有正在运行。如果这个时间点恰好有全量备份在跑,切换会打断备份链路,甚至导致备份产物不完整。这些检查不是每次都能做全,但至少在计划内维护时必须逐项过一遍。尤其是复制延迟和连接串配置两件事,是演练翻车的高发点。3.2 标准切换动作逐步拆解第一步,登录当前主节点,先确认角色。这一步虽然看起来多余,但强烈建议保留,尤其是多套环境并存时,很容易记错节点。执行:rs.isMaster()如果输出里ismaster : true,说明你确实连到了 primary。接着执行让位命令:rs.stepDown(120)这里我给了 120 秒的禁选窗口。命令一般会很快返回,也可能等几秒,取决于副本集当前的心跳状态。返回后,当前节点已经变成 secondary。第二步,在另一台终端快速观察副本集全貌:rs.status().members.forEach(m { print(m.name, -, m.stateStr, health:, m.health); });正常情况你会看到某台 secondary 在几秒内变成了 PRIMARY。整个选举过程一般小于 5 秒。如果超过 10 秒还没看到新主,就需要进入第 4 章的排查流程了。第三步,验证新主可写。登上新 primary,插入一条带标记的数据:db.getSiblingDB(test).health_check.insertOne({ tag: manual-failover-20250115, ts: new Date() });能写入且返回成功,说明新主已经能正常对外提供服务。再把这条数据在旧 primary(现在是 secondary)上查一下,确认复制没有断。通过这两个简单的操作,就能同时验证写入路径和复制链路。3.3 切换后的验证闭环与回切切换不是换个 primary 就结束,后续验证才是整个演练的重点。我做演练时至少会确认这四件事:副本集所有成员状态正常;新 primary 的复制进度是最新的;应用日志里没有持续报错,连接池已经重建;监控面板上出现了主节点变更事件,告警按预期发出。如果需要回切,也就是让原 primary 重新变回主角色,不要立刻执行。先观察一段时间,确认新的主节点稳定扛住了业务流量,旧节点也已经追平 oplog 延迟。回切方式和新主上再执行一次 stepDown 相同:rs.stepDown(120)同样需要等待选举完成并验证。需要注意的是,如果旧节点追平数据之前就回切,可能出现数据不一致的窗口,因此确认落后量为 0是回切的硬性前置条件。4. 切换实操里最常踩的坑和排查思路4.1 执行完 stepDown 却迟迟没有新主这是我在实战里见过最多的问题。发生的原因主要有三类:一是副本集当前只有两个在线成员,另一个节点又不够票数,导致选举无法形成多数派;二是所有可选举节点的 priority 都被设成了 0,没有节点能发起有效竞选;三是网络分区,当前节点能访问到某些节点,但它们之间互相不可达,无法达成多数派共识。排查时先看rs.status()里每个成员的实际状态和最近心跳时间。如果成员显示 unreachable,问题多半在网络层;如果状态正常但没有节点竞选,就检查 priority 配置。日志方面,重点关注 MongoDB 日志里的Replication组件输出,里面会有election succeeded或could not construct a stable majority这类关键信息。如果只是临时演练遇到这个问题,最快的恢复方式是登录一台健康的 secondary,手动把它加回可竞选队列,或者临时调高 priority 触发一次竞选。但治本的做法还是从副本集拓扑上解决:保证至少三个投票成员,并且至少有 2 个成员可以被选举。4.2 切换瞬间应用写入抖动怎么定位切换瞬间应用端出现少量写入失败或延迟升高是正常的,但如果抖动时间过长,通常不是 MongoDB 本身的问题,而是应用层的连接机制没有跟上。默认的驱动行为是发现主节点角色变化后,重新执行拓扑扫描,再重建连接池。这个过程中的任何延迟都会表现为写失败。排查时先看应用日志里有没有类似 not master / node is recovering 的报错;如果有,说明连接池还指向旧主,等待驱动刷新即可。另一个常见诱因是应用端没有开启retryWrites,那么当一次写入刚好落在切换窗口内时,客户端会直接抛错而不是自动重试一次。把retryWritestrue打开是最基本的改善手段。此外,如果连接池的 maxPoolSize 定得非常大,切换后所有应用实例同时向新主发起连接,有概率造成新主瞬间负载过高,反而引发慢查询。稳妥的做法是控制连接池上限,并考虑在连接串里把多个节点地址全部写上,让驱动主动感知候选节点,减少切换时的重连压力。4.3 复制延迟与隐藏节点带来的连锁问题副本集里如果有 hidden 节点或 delayed 节点,它们通常不参与竞选,但在切换后仍然需要持续从当前 primary 拉取 oplog。如果切换动作太快,或者演练期间刚好撞上备份任务,隐藏节点的复制延迟会瞬间拉高,影响后续的备份一致性。排查延迟问题,先看隐藏节点上是否有长时间运行的备份或统计任务。可以执行db.currentOp()查看applier线程是否在等待锁,以及后续复制任务是否被大查询阻塞。另一个常见原因是 oplog 太小,历史写量大而 oplog 窗口只覆盖几小时,一旦延迟稍高就会超出 oplog 范围,导致该节点只能重新做全量同步,这是很痛的教训。针对这类问题,我的建议是给 hidden 节点单独设置合理的维护窗口,并且不要让它承载临时高频查询。演练前查一遍rs.printSecondaryReplicationInfo(),看到隐藏节点的落后量超过一定阈值就先不要切换,等它追平再说。5. 从一次演练反推副本集的高可用设计5.1 多数派、仲裁者与投票节点怎么规划做手动主从切换演练做得多了,你会越来越清楚地感受到多数派这三个字的分量。一次 stepDown 能否成功,本质上不取决于你的操作命令,而取决于副本集有没有足够的投票节点在线来完成一次合法选举。三个节点丢一个还能选,两个节点丢一个就绝对选不出来。因此最基础的拓扑建议至少是 3 个投票成员,可以是 3 个数据节点,也可以是 2 个数据节点加 1 个仲裁者。仲裁者不保存数据,只参与投票,占资源极少,适合那种不想多堆数据副本的部署。但如果只有 2 个数据节点且没有仲裁者,遇到一次 stepDown 演练就会卡在第 4.1 节说的那个坑里,不建议在生产这样玩。另外有些团队喜欢把所有节点都设置为投票成员,这本身没问题,但要注意特殊节点的票数设置。比如说 hidden 节点和 delayed 节点,如果它们参与投票,那么它们在多数派里就要被算进来,这会抬高举手机位。合理规划时,通常会把这类节点设为votes: 0,让它们专注复制和备份,不影响选举成败。5.2 priority 和标签路由决定主库落在哪单次手动切换可以把 primary 从 A 节点挪到 B 节点,但如果 A 节点的 priority 配置得更高,下次故障转移时 MongoDB 还是会优先把主选回 A。这本身不是问题,反而应该被利用。如果你们有明确的主库偏好,比如某机房机器性能更好,或者某节点离业务入口更近,就把那台节点的 priority 调得比其他节点高。调法通过rs.reconfig()完成。例如想把某节点的 priority 从 1 提到 2,先从rs.conf()拿到当前配置,修改对应成员后重新提交。这里要提醒一句:reconfig本身也是一次可能引起短暂选举的敏感操作,不要在业务高峰期贸然执行。如果业务需要更精细的路由策略,比如读请求希望打到特定机房,或者某些写操作要发送到特定标签节点,可以为成员定义tags,然后应用侧配置相应readPreference。这样即使一次 stepDown 后 primary 发生漂移,读流量也能按标签路由到预期节点,不会全部垮掉。5.3 演练本身也是监控告警的校验场每次主从切换演练,都是校验监控和告警的最佳时机。平时的监控面板再怎么绿,也只有在真实切换发生时才能确认告警通道是否畅通、值班流程是否有漏洞。演练前,先确认监控系统已经接入了 MongoDB 的 state change 事件,比如 PRIMARY 身份变化、选举超时、成员不可达等关键指标。演练过程中,专门安排一个人盯着告警平台,记录从切换发生到告警触发的时间差。我遇到过的情况是:数据库已经切了 20 秒,告警邮件才到;还有一次切换直接触发了错误告警,值班人被惊吓,后来排查发现是监控 exporter 对角色变更的解析有 bug。这种小问题平时发现不了,只有真正演练时才会暴露,不演练的团队等于把这些问题埋到真实故障里一并引爆。坦白说,手动主从切换本身只是一个半分钟的操作,难的是操作前的评估、操作后的验证,以及整套高可用意识的养成。我自己的体会是,把rs.stepDown()演练变成每月一次的常规动作之后,团队对副本集的信任度明显不一样——不再是反正有副本集就不会挂的那种盲目信任,而是真正知道每一步切换会带来什么影响、哪些环节需要看、哪些日志值得关注。最后再留一个小建议:第一次做演练,务必选在业务低峰期,并且提前准备一份回退方案,哪怕只是如果 30 秒内没有新主,立刻确认多数派是否在线这样一行字,关键时刻都比临时翻文档管用。