副本集与高可用
架构
副本集(Replica Set)= 1 个 Primary(主,唯一可写)+ 多个 Secondary(从,可读)。
- 成员上限 50 个,其中最多 7 个有投票权
- 推荐 3 节点(1 主 2 从)或 5 节点,必须是奇数(偶数在网络分区时可能双方都拿不到多数)
- Arbiter(仲裁节点) —— 只投票、不存数据。用于「只有 2 台机器但想要自动故障转移」的场景
:::warning 生产不推荐 Arbiter
官方现在明确不建议用仲裁节点。原因:它不存数据,所以 w: "majority" 的写入在「1 主 1 从 1 仲裁」架构下,从节点挂掉后写入会一直阻塞(多数派 = 2,但只剩主节点能确认);且丢失了一份数据副本,可靠性下降。宁可用 3 个数据节点。
:::
oplog
oplog(operations log)是副本集复制的基础,理解它等于理解了 MongoDB 复制。
- 本质是
local数据库里的一个固定集合(capped collection):local.oplog.rs - 默认大小为磁盘可用空间的 5%(最小 990MB,最大 50GB),4.4+ 支持在线调整
- 写满后循环覆盖最旧的记录
关键特性:oplog 的操作必须是幂等的。所以 MongoDB 会把非确定性操作改写成确定性的 —— {$inc: {n: 1}} 在 oplog 里会被记录成 {$set: {n: 实际结果值}}。这样同一条 oplog 重放多次结果一致,是故障恢复和重放的前提。
这个设计可以和 MySQL binlog 的 STATEMENT vs ROW 格式对照:oplog 相当于强制使用 ROW 格式。
复制流程:
- Primary 执行写操作,记录 oplog
- Secondary 主动拉取(pull) Primary 的 oplog(注意是拉而不是推,与 MySQL 的主库 dump 线程推送相反)
- Secondary 重放 oplog,并写入自己的 oplog(所以可以做链式复制)
默认是异步复制,所以存在复制延迟。
:::warning oplog 太小导致的全量重同步
Secondary 宕机或延迟太久,等它恢复时所需的 oplog 位点已被覆盖,就会进入 RECOVERING 状态并需要全量重新同步(initial sync)。TB 级数据的全量同步可能耗时数小时,期间该节点不可用。
这是生产常见故障,排查看 rs.printReplicationInfo() 的 oplog 时间窗口(oplog first/last event time)。窗口应该远大于「可能的最长停机时间」,通常建议至少能覆盖 24~72 小时的写入量。
:::
选举
类 Raft 协议(protocolVersion 1,3.2+)。
触发条件:Secondary 在 electionTimeoutMillis(默认 10 秒)内没收到 Primary 的心跳。心跳间隔是 2 秒。
流程:
- 候选节点自增 term,向其他节点请求投票
- 获得超过半数投票者的票即当选
- 每个投票者在一个 term 内只能投一票
谁能当选(优先级依次判断):
priority更高的优先(设为 0 则永不当选,常用于灾备机房节点或分析专用节点)- oplog 更新(数据更接近 Primary)
- 不能是
hidden、delayed、arbiter等特殊节点 - 候选节点的 oplog 必须不落后于投票者
故障转移耗时通常 10~12 秒(检测 10 秒 + 选举 1~2 秒)。这期间写入不可用,驱动会把请求缓存并重试(retryable writes 默认开启)。
怎么防脑裂:靠多数派。网络分区时,只有包含超过半数节点的那一侧能选出 Primary,少数派那一侧的原 Primary 会自动降级为 Secondary 并停止接受写入。这是比 MySQL 哨兵更干净的设计 —— MySQL 需要手动配 min-replicas-to-write 才能避免分区中的旧主继续接受写入。
writeConcern
写入要多少个节点确认才返回,直接决定「会不会丢数据」:
| 配置 | 含义 |
|---|---|
{w: 0} | 不等确认,发出去就算成功(fire and forget),最快最不可靠 |
{w: 1}(默认) | 主节点内存写入成功即返回 |
{w: "majority"} | 多数派节点确认,保证故障转移后数据不丢 |
{w: N} | 指定 N 个节点确认 |
{j: true} | 额外要求写入 journal 已落盘 |
{wtimeout: 5000} | 超时时间,必须设,否则节点故障时会无限阻塞 |
关键认知:默认的 {w: 1, j: false} 意味着 —— 客户端收到成功响应后,如果主节点立刻崩溃,这条数据可能彻底丢失(还没写 journal、也没传给从节点)。
不能丢的数据必须用 {w: "majority", j: true, wtimeout: 5000},代价是延迟从亚毫秒级上升到几毫秒甚至几十毫秒(取决于节点间网络)。
:::tip 这是「MongoDB 会丢数据」争议的根源
早年 MongoDB 被诟病「丢数据」,很大程度上是因为默认配置太激进(早期版本默认甚至是 w: 0)。现在默认是 w: 1,4.4+ 的分片集群默认 w: "majority"。
面试被问「MongoDB 可靠吗」,正确答法是:可靠性是可配置的,取决于 writeConcern,不是引擎的固有属性。顺便能对照 MySQL 的 innodb_flush_log_at_trx_commit 和半同步复制。
:::
readPreference
从哪个节点读:
| 模式 | 含义 |
|---|---|
primary(默认) | 只读主节点,强一致 |
primaryPreferred | 主可用读主,否则读从 |
secondary | 只读从节点 |
secondaryPreferred | 优先读从,没有可用从节点才读主 |
nearest | 读网络延迟最低的节点(不区分主从) |
读从节点的代价是可能读到旧数据(复制延迟)。典型坑是「写完立刻读」读不到自己刚写的数据 —— 和 MySQL 主从延迟的问题完全一样。
nearest 适合多地域部署,配合 tags 可以实现「就近读」:给每个节点打上机房标签,客户端指定读本机房的节点。
readConcern
读到的数据需要满足什么一致性级别,容易和 readPreference 混淆 —— readPreference 决定从哪读,readConcern 决定读什么版本:
| 级别 | 含义 |
|---|---|
local(默认) | 读节点上的最新数据,可能是未被多数派确认的(故障转移后可能被回滚) |
available | 分片集群下最快,可能返回孤儿文档 |
majority | 只读已被多数派确认的数据,保证不会被回滚 |
linearizable | 线性一致,读之前确认自己仍是 Primary。很慢,必须配 maxTimeMS |
snapshot | 事务中使用,读一致性快照 |
local 的风险要讲清楚:读到的数据如果还没被多数派确认,而此时发生故障转移,这条数据会在 rollback 中被撤销。也就是「读到了一条后来不存在的数据」—— 这是脏读的一种形式。金额、库存等关键读取应该用 majority。
因果一致性会话(Causal Consistency) —— 用 startSession({causalConsistency: true}) 可以保证「读己之写」和单调读,解决读从节点读不到自己刚写的数据的问题。原理是客户端携带 operationTime,从节点等到追上该时间点才响应。
Rollback
Primary 写入了数据但还没复制给多数派就宕机,新 Primary 选出后,老 Primary 恢复时发现自己有「多余」的 oplog —— 这些数据会被回滚。
- 回滚的数据写入
rollback目录的 BSON 文件(可人工找回,但基本无法自动恢复) - 4.0+ 单次回滚上限是 300MB,超过则该节点需要全量重新同步
- 用
w: "majority"写入就不会被回滚 —— 这是避免此问题的根本办法
特殊节点
| 类型 | 配置 | 用途 |
|---|---|---|
| Hidden | hidden: true, priority: 0 | 客户端看不到,用于备份、离线分析 |
| Delayed | slaveDelay: 3600 | 刻意延迟 N 秒,误删数据的时间机器 |
| Arbiter | arbiterOnly: true | 只投票,不推荐 |
| Non-voting | votes: 0 | 超过 7 个成员时的额外只读副本 |
Delayed 节点是实用的防误操作手段 —— 有人 drop 了集合,可以从延迟节点捞回来(前提是在延迟窗口内发现)。注意它必须同时是 hidden 且 priority: 0。