第18章:Mongo复制集运维——故障切换、节点扩容与数据重同步
1. 项目背景业务场景复制集上线一个月本地生活电商扛住了两次服务器故障。但新问题接踵而至——A 城市的机房要停电检修 4 小时需要把 Primary 手动切到 B 机房的节点DBA 发现从库的磁盘快满了要在不中断服务的前提下加入一个新节点并顺利完成数据同步运营实习生某天手滑又误删了 500 条评论幸运的是有一个延迟从库Delayed Secondary需要在延迟从库的数据被覆盖之前把数据抢救回来。痛点复制集不是搭好了就万事大吉。日常运维中的高频操作——节点扩容、滚动升级、数据中心切换、延迟节点恢复、Initial Sync 故障处理——都需要运维团队熟练掌握。但多数人只会用rs.status()看一眼不知道数据重同步的风险有多大也不清楚rs.stepDown()的内部流程更不懂怎么在延迟节点上做时间旅行式的数据恢复。2. 项目设计小胖看着监控大屏大师我们深圳机房今晚停电 4 小时大屏上 Primary 在深圳机房。怎么把它搬到广州机房大师这叫计划内故障转移Planned Failover。用一条命令就能搞定rs.stepDown(60)// Primary 主动退位60 秒内不参与新一轮选举stepDown会让当前 Primary 主动降级为 Secondary然后其他符合条件的节点立即发起选举选出一个新 Primary。60 秒内原 Primary 不能再竞选确保新 Primary 有足够时间稳定下来。小胖那这不就跟皇帝退位让贤一样——“我退下来你们选一个新皇帝我 60 秒内不来抢”。大师形象。但要注意stepDown会先等待所有已写入 Oplog 的操作被至少一个 Secondary 赶上catchup确保退位时数据不丢。如果 catchup 时间太长可以用stepDown(60, 30)——最多等 30 秒 catchup 期限。技术映射rs.stepDown(stepDownSecs, catchUpSecs)的参数分别控制退位后多久不能再竞选和退位前最多等多久追上数据。小白那节点扩容呢我们磁盘快满了怎么加一个新节点加节点的时候会不会影响正在跑的查询大师加节点分两步——先启动一个空的 mongod 实例加入复制集然后它会自动做 Initial Sync初始同步把数据从已有节点拷过来。Initial Sync 期间会有几个影响做 Initial Sync 的源节点Sync Source会增加读负载新节点的 Initial Sync 可能持续数小时取决于数据量但是对客户端的读写完全没有影响——因为 Initial Sync 不用锁库读取 Secondary 的快照。技术映射Initial Sync 分两阶段——① 克隆所有数据库复制每个集合的文档② 在克隆期间产生的增量 Oplog 重放追赶。克隆阶段读取的是源节点的一致快照。小胖那 Oplog 窗口不够大的话Initial Sync 是不是就失败了大师对。Initial Sync 的第二步Oplog 重放需要源节点的 Oplog 中还保留着克隆开始时那个时间点的记录。如果克隆耗时 3 小时但 Oplog 窗口只有 2 小时——第二步就会因为找不到起始 Oplog 而失败新节点必须从头再来。技术映射Initial Sync 对 Oplog 窗口有硬性要求——窗口必须大于克隆数据库的总时长。当数据库很大时TB 级建议用文件系统快照的方式完成 Initial Sync把源节点的数据目录直接拷贝过去比逻辑克隆快得多。小白那延迟节点Delayed Secondary怎么用来救命我同事说它就是 MongoDB 的回收站。大师精准延迟节点故意慢 Primary 一段时间如 1 小时它上面的数据是 1 小时前的快照。如果有人误删了数据你可以在延迟节点上查到被删之前的数据然后在 Primary 上恢复。恢复步骤找到延迟节点rs.conf().members.find(m m.secondaryDelaySecs 0)关掉延迟节点让它在误删操作执行前停下来。从延迟节点的数据中导出被删的文档。导回到 Primary。大师总结复制集运维三件事——手动切换用stepDown扩容加节点关注 Oplog 窗口够不够误删恢复用延迟节点的时间旅行能力。3. 项目实战3.1 环境准备沿用第 17 章的 3 节点复制集。3.2 分步实现步骤一手动 Primary 切换——stepDown目标练习安全的 Primary 切换流程。// 在 Primary 节点上操作// 1. 确认当前 PrimaryconstprimaryInfors.isMaster()print(当前 Primary:,primaryInfo.primary)// 2. 查看所有节点的同步延迟rs.status().members.forEach(m{constlagm.optimeDate?(Date.now()-m.optimeDate.getTime())/1000:N/Aprint(${m.name}|${m.stateStr}| 延迟:${lag}s)})// 3. 执行 stepDownrs.stepDown(60,30)// PRIMARY 降级为 SECONDARY30 秒后触发选举# 观察选举过程# 在该节点降级后连接另一个节点dockerexecmongo-rs2 mongosh--quiet--eval const status rs.status(); print(新 Primary:, status.members.find(m m.stateStr PRIMARY)?.name || 选举中...); 步骤二新增节点——扩容复制集目标启动第 4 个 mongod 并加入复制集。# 1. 在 docker-compose-rs.yml 中添加 rs4 服务# 2. 启动新节点dockercompose-fdocker-compose-rs.yml up-drs4# 3. 在 Primary 上将 rs4 加入复制集dockerexecmongo-rs1 mongosh--quiet--eval rs.add({ host: rs4:27017, priority: 1 }) // 监控 Initial Sync 进度// 在 rs4 上查看db.currentOp({desc:{$regex:/initial sync/i}})// 或查看日志// docker logs -f mongo-rs4// 查看 rs4 的同步状态rs.status().members.find(mm.name.includes(rs4))// stateStr 会经历// STARTUP → STARTUP2正在 Initial Sync→ RECOVERING → SECONDARY// 整个 Initial Sync 完成后 stateStr SECONDARY步骤三移除节点——缩容复制集目标从复制集中安全移除一个节点。// 在 Primary 上执行// 移除节点前确保它不当前 Primary且移除后仍有足够的投票节点// 1. 查看配置constconfrs.conf()print(当前成员数:,conf.members.length)// 2. 移除 rs4非 Primary 节点rs.remove(rs4:27017)// 注意host 字符串必须与 rs.conf() 中完全一致// 验证print(移除后成员数:,rs.conf().members.length)步骤四修改节点优先级和角色目标在线修改节点属性而不重启。// 获取当前配置constcfgrs.conf()// 修改 rs2 的优先级priority 越高当选 Primary 概率越大cfg.members.find(mm._id1).priority3// 将 rs3 设为 Hidden 节点不接受客户端读流量constmember3cfg.members.find(mm._id2)member3.priority0member3.hiddentrue// 应用配置更新rs.reconfig(cfg,{force:false})// force: false 需要多数节点在线才能重配安全// force: true 允许在少数节点在线时强制作危险仅在恢复场景使用步骤五Oplog 管理与窗口监控目标调整 Oplog 大小并监控 Oplog 窗口。// 查看当前 Oplog 信息constoplogStatsdb.getSiblingDB(local).oplog.rs.stats()print(Oplog 大小:,(oplogStats.maxSize/1024/1024).toFixed(0),MB)print(Oplog 使用:,(oplogStats.size/1024/1024).toFixed(0),MB)// 查看 Oplog 窗口最早和最晚记录的时间差constoplogdb.getSiblingDB(local).oplog.rsconstlastoplog.find().sort({$natural:-1}).limit(1).next()constfirstoplog.find().sort({$natural:1}).limit(1).next()if(firstlast){constwindowHours(last.ts.getHighBits()-first.ts.getHighBits())/3600print(Oplog 窗口:,windowHours.toFixed(1),小时)}// 修改 Oplog 大小所有节点上都要执行// 方法先删掉旧 oplogMongoDB 重建时指定大小// 1. 通过 rs.stepDown() 确保当前节点不是 Primary// 2. 重启 mongod 时加入 --oplogSize 4096单位 MB// 或在 docker-compose 的 command 中: --oplogSize 4096步骤六利用延迟节点恢复误删数据目标演示用 Delayed Secondary 做时间旅行恢复。// 模拟在 Primary 上插入数据use local_life db.delay_recovery.insertOne({docId:IMPORTANT_001,data:这条数据很重要,createdAt:newDate()})constinsertTimenewDate()print(插入时间:,insertTime)// 等待 5 秒后模拟误删db.delay_recovery.deleteOne({docId:IMPORTANT_001})print(已删除模拟误删)# 数据恢复流程在延迟节点上操作# 1. 在 Primary 上停止延迟节点的同步# 延迟节点的数据停留在 insertTime 附近还没有收到 delete 操作# 方法在延迟节点上执行dockerexecmongo-rs5 mongosh--quiet--eval // 使延迟节点停止同步临时变成 standalone 模式 db.adminCommand({ replSetFreeze: 9999 }) # 2. 从延迟节点导出被删数据dockerexecmongo-rs5 mongodump\--dblocal_life--collectiondelay_recovery\--query{docId:IMPORTANT_001}\--out/tmp/recovery# 3. 将数据恢复到 Primarydockerexecmongo-rs1 mongorestore\--dblocal_life--collectiondelay_recovery\/tmp/recovery/local_life/delay_recovery.bson// 验证恢复use local_lifeconstrecovereddb.delay_recovery.findOne({docId:IMPORTANT_001})print(恢复成功:,recovered?PASS:FAIL)3.3 完整代码清单文件用途mongodb-lab/replicaset/stepdown-test.jsstepDown 切换演练mongodb-lab/replicaset/add-node-test.js新增/移除节点脚本mongodb-lab/replicaset/oplog-monitor.jsOplog 窗口监控mongodb-lab/replicaset/delayed-restore.js延迟节点数据恢复流程3.4 测试验证// 在 Primary 上运行// 1. stepDown 验证constprePrimaryrs.isMaster().primary rs.stepDown(30,10)sleep(15000)constpostPrimaryrs.isMaster().primaryprint(Primary 切换:,prePrimary!postPrimary?PASS (已切换):FAIL)// 2. 节点加入验证constmemberCountrs.status().members.lengthprint(成员数:,memberCount3?PASS:FAIL)// 3. Oplog 窗口验证constfirstdb.getSiblingDB(local).oplog.rs.find().sort({$natural:1}).limit(1).next()constlastdb.getSiblingDB(local).oplog.rs.find().sort({$natural:-1}).limit(1).next()constwindow(last.ts.getHighBits()-first.ts.getHighBits())/3600print(Oplog 窗口:,window.toFixed(1),小时,window1?PASS:⚠ 建议增大Oplog)4. 项目总结4.1 运维操作速查表操作命令注意事项计划内切换rs.stepDown(secs, catchupSecs)先确认 Secondary 同步状态加数据节点rs.add(host:port)确保 Oplog 窗口 Initial Sync 耗时加 Arbiterrs.addArb(host:port)Arbiter 不存数据省磁盘移除节点rs.remove(host:port)确保移除后至少有 2 个数据节点修改优先级cfg.members[n].priority X; rs.reconfig(cfg)仅影响下一次选举长 Oplog--oplogSize 4096重启时设置需逐个节点滚动重启紧急恢复rs.reconfig(cfg, {force: true})仅当多数节点不可用时使用冻结节点rs.freeze(seconds)冻结期间不参与选举4.2 适用场景本章操作适用机房停电/搬迁——通过 stepDown 做计划内 Primary 迁移。节点磁盘告急——加入新节点扩容量后移除旧节点。版本升级——逐个节点滚动升级 mongod 版本。误删恢复——从延迟节点抢救数据。Oplog 优化——监控窗口大小并动态调整。4.3 注意事项注意事项说明rs.reconfig必先获取最新 config不要用旧的rs.conf()修改后再 reconfig——中间可能已被别人改过Initial Sync 不要从 Secondary 做如果源 Secondary 的 Oplog 不如 Primary 新可能导致 Initial Sync 后不一致延迟节点不能无限延迟secondaryDelaySecs太大 24h可能导致 Oplog 覆盖、从库永远追不上移除节点≠回收磁盘rs.remove只是从复制集配置中踢出该节点上的磁盘数据仍在force: true是双刃剑强制定会导致[term: X, version: Y]的配置冲突事后需手动校验所有节点配置一致4.4 常见踩坑经验故障案例一Initial Sync 无限循环某节点因 Oplog 窗口不够导致 Initial Sync 失败后自动重试每次重试都是从头克隆 Oplog 追不上 → 失败 → 重试陷入无限循环。根因Oplog 窗口 克隆全量数据的时间。解决临时增加到 4 倍正常大小的 Oplog等新节点 sync 完成后再调回去。故障案例二rs.reconfig后节点被踢出某运维修改了rs.conf()中一个节点的 host 名从 IP 改为域名。结果该节点因host 名不匹配被复制集自动踢出。根因MongoDB 的复制集成员身份由_id和host共同来确定修改 host 等于踢掉旧节点加入新节点而不是重命名。解决如果必须改 host 名先rs.remove再rs.add。故障案例三延迟节点时间设置错误导致数据丢失某团队把secondaryDelaySecs设为8640024 小时以为可以用作昨天的数据备份。结果 Primary 写入速度峰值期间 Oplog 窗口缩小到 8 小时——延迟节点等待 24 小时后发现 Oplog 里已经没有了 24 小时前的记录同步永久中断。解决secondaryDelaySecs必须小于 Oplog 窗口的最差情况建议设为 Oplog 窗口的 30%-50%。4.5 思考题如果复制集的 3 个数据节点全部宕机恢复时应该先启动哪个节点顺序重要吗Hidden 节点和 Delayed 节点的数据都不直接为客户端查询服务但两者在 Oplog 拉取行为上有何本质区别答案将在第 19 章末尾揭晓上一章思考题答案Primary 写入 Oplog 后立刻宕机Secondary 还没拉到这条 Oplog——这些写入会丢失。但从客户端视角如果writeConcern: w:1只等 Primary 确认客户端收到成功响应后数据丢失了——这叫已被确认但未传至副本的写入丢失。如果writeConcern: majorityPrimary 在至少一个 Secondary 也确认之前不会返回成功——不会丢失。所以高价值数据务必使用writeConcern: majority。Arbiter 能参与选举是因为 Raft 协议不要求存数据才能投票——Arbiter 被赋予投票权只需检查候选者的 term 和 log index 是否足够新。如果 Arbiter 被网络隔离到少数派一侧它会和少数派一起无法联系多数派——少数派无法获得过半票数无法选举出新 Primary多数派不受影响。但极端情况下如果 Arbiter 和 Primary 在一起被隔离而 2 个 Secondary 在另一侧——只有 PrimaryArbiter 一侧有 2 票Secondary 一侧也有 2 票谁也选不出来3 号票没到位。延伸阅读与资源python入门Rquests从菜鸟脚本到企业级SDK的网络实战圣经Milvus向量数据库实战修炼从 0 到 1精通向量检索与生产落地后端工程师的 AI 转型第一课Ollama 与私有化大模型实战10倍开发者的 Dify 魔法书从零构建全栈 AI 应用后端工程师转型AI第一课-Ollama 与私有化大模型实战大型语言模型(LLM) vLLM 高性能推理落地实战Agent开发之LlamaIndex 实战修炼与源码进阶大语言模型Transformers 实战修炼与源码剖析
