马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
1. 项目配景
业务场景:本地生存电商上线三个月,单台 MongoDB 开始力不从心。某天凌晨 2 点,服务器突然宕机——只是普通的内存故障,但因为只有一台 MongoDB,整个系统直接不可用,直到早上 8 点运维重启服务器才规复。6 小时的停服直接导致 3000 个订单丢失、用户投诉爆表。CTO 拍板:必须做高可用。但团队的困扰在于——"MongoDB 的主从复制怎么做?一个 Primary 两个 Secondary 够不够?如果 Primary 挂了,客户端怎么自动切到新 Primary?应用代码需要改吗?"
痛点:没有复制集(Replica Set)的单机 MongoDB,任意故障(历程崩溃、系统重启、磁盘满、网络断)都会导致业务全部中断。更糟糕的是——很多人第一次搭复制集就踩坑:节点之间网络不通导致选举死锁;writeConcern: majority 不懂含义导致写入性能和一致性之间做错取舍;误以为读 Secondary 能分摊读压力,结果延时导致数据不一致;Arbiter 节点不懂其作用直接部署了 3 个数据节点花冤枉钱。
2. 项目设计
小胖(拿着高可用架构图):大师,我听说过主从复制——一个主库写入,从库同步数据。但 MongoDB 不是还搞了个什么"复制集"吗?和 MySQL 的主从复制有啥不同?
大师:区别蛮大的。MySQL 的主从复制是单向的——主库写,从库只读,主库死了你需要手动把从库提成主库。MongoDB 的复制集是自管理的——节点之间通过心跳(Heartbeat)互相监控,Primary 挂了,剩下的节点自动投票选举出新的 Primary,整个过程无需人工干预。
小胖:自动选举?这不是跟选班长一样吗——班长不在,剩下的同学投票选新班长?
大师:贴切。选举遵循 Raft 协议——MongoDB 的复制集本质上是一个基于 Raft 的分布式一致性集群。每个节点都有 term(任期号),每次选举 term 加 1,获得多数票(过半数)的节点当选 Primary。
技术映射:MongoDB 复制集是自管理的分布式状态机——Primary 负责所有写入(Oplog 记入日志),Secondary 通过拉取 Oplog 不断地重放操作来追赶 Primary 的状态。当 Primary 不可达时,存活的节点通过 Raft 共识协议选举新 Primary。
小白:那复制集有哪几种节点角色?我知道 Primary 和 Secondary,还有什么 Arbiter、Hidden、Delayed 的?各有啥用?
大师:MongoDB 复制集中的 5 种角色:
角色存储数据参与选举用途Primary是是唯一接收写入的节点Secondary (priority > 0)是是同步数据 + 可参与选举Arbiter(仲裁节点)否是只投票,不存数据,打破平票僵局Hidden(隐藏节点)是否(priority=0)备份 / 报表专用,不接收读流量Delayed(延迟节点)是否(priority=0, 延迟 slaveDelay 秒)误删规复的"后悔药"小胖:Arbiter 不存数据只投票?那不是省了一个服务器的钱?我直接用 Arbiter 组一主一从一 Arbiter 不就好了?
大师:Arbiter 确实省钱,但也有代价。Arbiter 不存数据意味着——如果 Primary 挂了,Secondary 当选,但如果 Primary 挂了且 Arbiter 也不可用了,Secondary 独自得不到多数票,无法成为 Primary。所以 Arbiter 只是用低本钱凑足 3 票(选举需要超过半数),但不增加数据的容灾副本数。
技术映射:复制集选举要求获得 "大多数"选票。3 节点(2 数据 + 1 Arbiter)的多数是 2,任意 1 个数据节点挂掉仍能选举。但 2 个数据节点都挂掉,Arbiter 独木难支——因为 Raft 要求 quorum 中必须有包含最新日志的节点。
小白(追问):Oplog 是什么?我总是听到这个词但不太理解。
大师:Oplog(Operation Log)是 MongoDB 复制的核心。你可以把它理解成"主节点的施工清单"——Primary 实行的每一个写操作(insert/update/delete)都会生成一条 Oplog 条目,Secondary 不断地拉取这个清单,在自己的数据库上按次序重放一遍。
- Oplog 是一个固定巨细的 Capped Collection(环形缓冲区),巨细通过 oplogSizeMB 设置。
- 如果 Secondary 断开连接太久,Oplog 窗口里已经没有它错过的那些操作——此时 Secondary 无法追赶,必须做 Initial Sync(全量重新同步)。
技术映射:Oplog 窗口 = Oplog 巨细 / 写入速率。比如 Oplog 2GB,写入速率 100MB/h,窗口就是 20 小时。因此,写入高峰期 Oplog 窗口会缩短,需要监控。
大师(总结):复制集是 MongoDB 高可用的最小单元——3 个数据节点(PSS 或 PSA)是最常见的生产配置。今天记住三点:自动选举靠 Raft 协议;写入复制靠 Oplog 拉取;数据一致性靠 writeConcern 控制。
3. 项目实战
3.1 环境准备
用 Docker Compose 搭建 3 节点复制集(PSS 架构):- # mongodb-lab/replicaset/docker-compose-rs.yml
- version: '3.8'
- services:
- rs1:
- image: mongo:8.0
- container_name: mongo-rs1
- ports:
- - "27017:27017"
- command: mongod --replSet myRS --bind_ip_all --port 27017
- volumes:
- - ./data/rs1:/data/db
- networks:
- - rs-net
- rs2:
- image: mongo:8.0
- container_name: mongo-rs2
- ports:
- - "27018:27017"
- command: mongod --replSet myRS --bind_ip_all --port 27017
- volumes:
- - ./data/rs2:/data/db
- networks:
- - rs-net
- rs3:
- image: mongo:8.0
- container_name: mongo-rs3
- ports:
- - "27019:27017"
- command: mongod --replSet myRS --bind_ip_all --port 27017
- volumes:
- - ./data/rs3:/data/db
- networks:
- - rs-net
- networks:
- rs-net:
- driver: bridge
复制代码 3.2 分步实现
步骤一:初始化复制集
目标:启动 3 个 mongod 并初始化复制集。- # 启动容器
- docker compose -f docker-compose-rs.yml up -d
- docker compose ps
- # 进入 rs1 初始化复制集
- docker exec -it mongo-rs1 mongosh --eval '
- rs.initiate({
- _id: "myRS",
- members: [
- { _id: 0, host: "rs1:27017", priority: 2 },
- { _id: 1, host: "rs2:27017", priority: 1 },
- { _id: 2, host: "rs3:27017", priority: 1 }
- ]
- })
- '
- # 期望输出: { "ok": 1 }
复制代码- // 查看复制集状态
- rs.status()
- // 关键字段:
- // members[*].stateStr → "PRIMARY" / "SECONDARY"
- // members[*].health → 1
- // members[*].optime → 该节点的最新 Oplog 时间
复制代码 步骤二:观察复制行为
目标:在 Primary 上写入数据,观察 Secondary 是否实时同步。- // 连接到 Primary(可能是 rs1 的 27017)
- // 确认当前是 Primary
- use test_rs
- db.products_rs.insertOne({
- name: "复制集测试商品",
- price: 199,
- createdAt: new Date()
- })
- print("写入成功,等待同步...")
复制代码- // 连接到 Secondary(27018 或 27019),需要先允许从库读
- // mongosh "mongodb://localhost:27018"
- // 从库默认不允许读,需要设置
- db.getMongo().setReadPref("secondary")
- // 或者在连接串中:mongosh "mongodb://localhost:27018/?readPreference=secondary"
- use test_rs
- const doc = db.products_rs.findOne({ name: "复制集测试商品" })
- print("从库中的商品:", doc ? doc.name : "未同步(稍等几秒)")
- // 刚插入可能还没同步(< 50ms 延迟),稍等后应出现
复制代码 步骤三:模拟 Primary 宕机 + 自动选举
目标:停止 Primary 容器,观察自动选主过程。- # 找到当前 Primary
- docker exec mongo-rs1 mongosh --quiet --eval "rs.isMaster().primary" | grep -v "^$"
- # 假设 Primary 是 rs1:27017
- # 模拟宕机:停止 rs1 容器
- docker stop mongo-rs1
- # 查看 rs2 的复制集状态
- docker exec mongo-rs2 mongosh --quiet --eval '
- const status = rs.status();
- print("Primary:", status.members.find(m => m.stateStr === "PRIMARY")?.name || "无");
- '
- # 期望:rs2 或 rs3 中的一个被选举为 Primary
- # 选举时间通常在 5-12 秒之间
复制代码- // 查看选举日志
- docker exec mongo-rs2 cat /var/log/mongodb/mongod.log 2>/dev/null | grep "election" | tail -5
复制代码 步骤四:规复宕机节点——重新到场复制集
目标:将被 Kill 的 rs1 重新启动,观察它如何追回数据。- # 重启 rs1
- docker start mongo-rs1
- # 等待 10 秒,查看 rs1 的角色
- docker exec mongo-rs1 mongosh --quiet --eval '
- const status = rs.status();
- const me = status.members.find(m => m.name.includes("rs1"));
- print("rs1 状态:", me.stateStr);
- print("rs1 健康:", me.health);
- '
- # 期望:rs1 以 SECONDARY 身份重新加入(原 Primary 已降级)
复制代码 步骤五:配置节点的优先级和延迟
目标:设置 Hidden 节点和 Delayed 延迟节点。- // 在 Primary 上执行
- // 添加 Hidden 节点(不接收客户端读流量)
- rs.add({
- host: "rs4:27017",
- priority: 0, // 0 = 不能成为 Primary
- hidden: true // 隐藏节点
- })
- // 添加 Delayed 节点(同步延迟 1 小时,误删恢复用)
- rs.add({
- host: "rs5:27017",
- priority: 0,
- hidden: true,
- secondaryDelaySecs: 3600 // 1 小时延迟
- })
- // 查看配置
- rs.conf().members.forEach(m => {
- print(`${m.host}: priority=${m.priority}, hidden=${m.hidden || false}, delay=${m.secondaryDelaySecs || 0}s`)
- })
复制代码 步骤六:writeConcern 实战——对比写入确认级别- use test_rs
- // 场景 1:w:1(只 Primary 确认,最快但不安全)
- const t1 = Date.now()
- db.orders_rs.insertOne(
- { orderNo: "WC_TEST_1", amount: 100 },
- { writeConcern: { w: 1 } }
- )
- print("w:1 耗时:", Date.now() - t1, "ms")
- // 约 2-5ms
- // 场景 2:w:"majority"(多数成员确认,安全但稍慢)
- const t2 = Date.now()
- db.orders_rs.insertOne(
- { orderNo: "WC_TEST_2", amount: 200 },
- { writeConcern: { w: "majority", wtimeout: 5000 } }
- )
- print("w:majority 耗时:", Date.now() - t2, "ms")
- // 约 10-50ms(取决于副本之间的网络延迟)
- // 场景 3:w:3(所有 3 个数据节点都确认,最慢)
- const t3 = Date.now()
- db.orders_rs.insertOne(
- { orderNo: "WC_TEST_3", amount: 300 },
- { writeConcern: { w: 3, j: true, wtimeout: 5000 } } // j:true = 需要持久化到 Journal
- )
- print("w:3+j 耗时:", Date.now() - t3, "ms")
复制代码 3.3 完整代码清单
文件用途mongodb-lab/replicaset/docker-compose-rs.yml3 节点复制集 Docker 配置mongodb-lab/replicaset/init-rs.js复制集初始化脚本mongodb-lab/replicaset/failover-test.js故障切换验证脚本mongodb-lab/replicaset/writeconcern-demo.jswriteConcern 性能对比3.4 测试验证
- // 连接到 Primary 运行
- // 1. 确认复制集状态
- const status = rs.status()
- print("复制集名称:", status.set)
- print("成员数:", status.members.length)
- print("Primary:", status.members.filter(m => m.stateStr === "PRIMARY").length === 1 ? "PASS" : "FAIL")
- // 2. 验证写入复制
- db.test_rs.validation.insertOne({ msg: "验证复制", ts: new Date() }, { writeConcern: { w: "majority" } })
- // 在 Secondary 上验证
- // mongosh "mongodb://localhost:27018/?readPreference=secondary" --eval '
- // const doc = db.getSiblingDB("test_rs").validation.findOne({msg:"验证复制"});
- // print(doc ? "PASS: 数据已同步" : "FAIL: 未同步");
- // '
- // 3. 验证自动选举
- // 记下当前 Primary,停掉它
- // 等待 15 秒在另一个节点查看,应有新 Primary
- // 4. 清理
- // docker compose -f docker-compose-rs.yml down -v
复制代码 4. 项目总结
4.1 复制集 vs 单机 vs 分片集群
维度单机复制集分片集群高可用无自动故障切换每分片独立高可用数据冗余无多副本每分片多副本写性能最高略降(writeConcern 开销)水平扩展读性能单机可读 Secondary 分担多分片并行读部署复杂度低中高适用数据规模< 100GB< 500GB500GB+4.2 适用场景
复制集适用:
- 所有生产环境——单机仅限本地开发和 CI。
- 需要"业务无感知"的自动故障转移。
- 读多写少——读写分离,读流量分担到 Secondary。
- 异机房容灾——跨机房部署 Secondary 节点。
不适用场景:
- 单机开发环境——复制集增加 3 倍资源消耗。
- 写入优先且可接受停机的批量处理——单机更快。
4.3 注意事项
注意事项说明奇数个选举节点3/5/7 个有投票权的节点,避免偶数(需要过半票)Oplog 巨细默认是磁盘的 5%,生产发起设为 24-48 小时的写入量Secondary 读不是"免费"的Secondary 追不上 Primary 时会产生读延迟网络分区被隔离的少数派节点无法选举,服务不可用rs.stepDown() 不是重启会触发安全的 Primary 降级,让出位置给其他节点4.4 常见踩坑经验
故障案例一:偶数节点导致网络分区后无法选举
某团队部署了 4 节点(2 个完整节点 + 2 个 Arbiter),机房断网后 2+2 各在一侧,每一侧都只有 2 票(需要过半 = 3 票)——两侧都无法选举 Primary,导致服务完全中断。根因:偶数节点 + 对称分布导致分裂脑(Split Brain)。办理:始终使用奇数个投票节点,关键节点放在奇数侧。
故障案例二:Oplog 窗口太小导致从库追不上
某促销期间订单量暴增 10 倍,写入速率从 50MB/h 飙升 500MB/h。Oplog 2GB 的窗口从 40 小时压缩到 4 小时。一个做全量备份后重启的从库,发现自己需要追 6 小时前的数据,但 Oplog 只保存了 4 小时的——必须做 Initial Sync,耗时 3 小时。办理:增大 Oplog 至 10GB+;监控 Oplog 窗口时间,低于 12 小时告警。
故障案例三:writeConcern: majority 在 2 节点复制集下写不进去
某团队用 1 Primary + 1 Secondary(2 节点),设置 writeConcern: majority。多数 = ceil(3/2)=2——需要两个节点都确认。其中一个节点挂了,写入因为多数无法告竣直接报超时。根因:2 节点复制集的"多数"便是"全部",任意一个节点故障写入全停。办理:始终用至少 3 个数据节点;或用 writeConcern: 1 捐躯一致性保可用性。
4.5 思考题
- 如果 Primary 写入 Oplog 后立刻宕机(Oplog 还没传到任何 Secondary),新选出的 Primary 会丢失这些写入吗?对客户端而言,这些写入的返回结果是什么?
- Arbiter 节点不存数据,为什么也能参与选举?如果 Arbiter 被网络隔离到少数派一侧,会发生什么?
(答案将在第 18 章末尾揭晓)
上一章思考题答案:
- 两个需要事务的跨聚集场景:① 下单 + 库存预扣——创建订单和扣减库存必须在同一事务中,如果不消事务,替换方案是"先预扣库存(原子 $inc)+ 后创建订单,订单创建失败后补偿规复库存"(Saga 模式),代码复杂且补偿失败有数据不一致风险。② 订单完成 + 更新用户统计——订单标记完成和用户总消费金额更新。替换方案是 Change Streams 异步更新统计数据,接受短暂的不一致。
- Spring Boot 自动管理索引(spring.data.mongodb.auto-index-creation=true + @Indexed)的优点:开发服从高,不需要手写 DDL 脚本;风险:① 启动时同步建索引会阻塞应用启动(大聚集建索引需要数分钟);② 生产环境 @Indexed 注解的修改会意外地在数据库中创建非预期索引;③ 无法控制 background/partialFilterExpression 等高级参数。推荐方案:开发环境用自动建索引快速迭代,生产环境关闭自动建索引,通过变更管理流程实行索引变更脚本。
延伸阅读与资源
Python 3实战精进:从脚本到高并发订单引擎
MongoDB 实战进阶与内核修炼
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经
Milvus向量数据库实战修炼:从 0 到 1精通向量检索与生产落地
后端工程师的 AI 转型第一课:Ollama 与私有化大模型实战
10倍开发者的 Dify 邪术书:从零构建全栈 AI 应用
后端工程师转型AI第一课-Ollama 与私有化大模型实战
大型语言模型(LLM) vLLM 高性能推理落地实战
Agent开发之LlamaIndex 实战修炼与源码进阶
大语言模型Transformers 实战修炼与源码剖析
|