副本集(Replica Set)是 MongoDB 高可用的基石。但很多人只知道"主挂了会自动选新主",对背后的 oplog 复制、选举协议、写关注与读偏好如何影响一致性一知半解。本文把副本集的复制链路和选举机制讲清楚,顺带讲那些导致"数据丢了"或"脑裂"的线上坑。
场景:主库切换后丢了几条写
一次主节点宕机,副本集自动选出新主,服务恢复。但事后对账发现,宕机前最后几秒确认成功的写入消失了。这不是 bug,而是你没理解 write concern 与故障切换时的 rollback 机制。要讲清楚,先看数据怎么在节点间流动。
机制一:oplog 与异步复制
副本集复制的核心是 oplog(operations log)——一个固定大小的 capped collection(local.oplog.rs),记录主节点上所有改变数据的操作,且是幂等的。
幂等很关键:oplog 不记录"$inc:1",而是记录"把这个字段设为最终值"。这样从节点无论重放多少次,结果都一致。
复制流程是拉模式(pull),不是推:
1 | 1. Secondary 连到同步源(通常是 Primary),tail oplog |
注意复制是异步的——主节点写完本地、写完 oplog 就可以给客户端返回(取决于 write concern),不等从节点。这就是开头丢数据的根源:确认了但还没复制出去,主就挂了。
oplog 大小决定了"从节点能落后多久还能追上"。落后超过 oplog 覆盖的时间窗,从节点会进入 RECOVERING,只能全量重新同步(initial sync)。监控 replication lag 和 oplog window 是运维必修。
机制二:选举协议(Raft 风格)
MongoDB 的选举基于 Raft 思想,引入了 term(任期)和多数派投票。每个副本集成员维护一个 term,每次选举 term 递增。
触发选举的时机:从节点在 electionTimeoutMillis(默认 10s)内没收到主节点心跳,就认为主可能挂了,把自己变成候选人,term+1,向其他成员拉票。
赢得选举的条件:
1 | 1. 拿到多数派(majority)的票,N 个投票成员需要 floor(N/2)+1 票 |
多数派是防脑裂的关键。假设 5 节点集群分成 3+2 的网络分区:3 节点那侧能凑齐多数派(3 > 2.5)选出主并继续服务;2 节点那侧凑不齐,只能降级为只读/不可写。这样保证任何时刻最多一个主,绝不脑裂。
这也解释了为什么副本集成员数推荐用奇数。4 节点和 3 节点的容错能力一样(都只能挂 1 个还保持多数派),但 4 节点多花一份成本、还更容易在 2+2 分区时双侧都无法工作。
1 | 3 节点:多数派 2,可容忍 1 个故障 |
Arbiter(仲裁者)的取舍
凑不齐奇数又不想多存一份数据时,可以加 arbiter——只投票、不存数据。但 arbiter 有个隐患:它不持有数据,无法满足 w:majority 中需要数据落盘的语义保障,在 2 数据节点 + 1 arbiter 的配置下,一个数据节点挂了,w:majority 写会一直 hang(因为只剩 1 个数据节点,达不到数据多数派)。生产环境慎用 arbiter。
机制三:write concern 与读偏好——一致性旋钮
Write Concern 控制写入要被多少节点确认才算成功:
1 | // 只要主节点确认(快,但主挂了可能丢) |
w:"majority" 是避免开头那个"丢数据"问题的答案——只有当多数派都收到这条写,它才被确认。即使主随后宕机,新主一定在多数派里,数据不会丢。代价是延迟更高(要等网络往返)。
Read Concern / Read Preference 控制读哪里、读多新:
1 | primary(默认) :强一致,所有读走主 |
工程权衡一句话:w:1 + 读从 最快但最弱一致;w:majority + readConcern:majority + 读主最强一致但最慢。按业务对一致性的容忍度选,不要一刀切。
机制四:Rollback——故障切换时的数据回滚
当旧主在写入还没复制到多数派时宕机,新主选出后,旧主恢复时发现自己有"新主没有的写"。这些写会被回滚(rollback),写到回滚文件里(BSON),不会自动重新应用。这正是 w:1 下丢数据的具体机制。w:majority 能彻底避免它,因为被多数派确认过的写,新主一定有。
工程实践与命令
1 | # 查看副本集状态、各节点角色与复制延迟 |
调 priority 可以控制谁更容易当主(如让计算资源好的机房节点优先);hidden/votes:0 节点可用于专门跑备份或分析,不参与选举不接业务流量。
常见误区与踩坑
- 误区:有副本集就不会丢数据。默认
w:1仍可能在故障切换时丢,关键写务必w:"majority"。 - 踩坑:oplog 太小导致从节点频繁 initial sync。大批量写入(数据迁移、批处理)前评估 oplog window,必要时扩容 oplog。
- 踩坑:用 arbiter 凑数,
w:majority写卡死。优先用真实数据节点,arbiter 仅在极受限场景用。 - 误区:从节点读一定是最新的。复制异步,
secondaryPreferred会读到滞后数据,强一致场景必须读主或readConcern:majority。 - 踩坑:网络抖动触发不必要的选举。
electionTimeoutMillis太小会在网络抖动时频繁切主,每次切主都有几秒不可写窗口,跨机房部署尤其要注意。
小结
副本集 = 异步 oplog 复制 + Raft 风格多数派选举。多数派同时撑起了两件事:选举不脑裂、写入不丢失。理解 write concern 和 read preference 这两个一致性旋钮,按业务调到合适档位,再配合奇数节点和合理的 oplog 大小,就能拿到 MongoDB 高可用的真正保障。记住那条铁律:要数据不丢,就 w:"majority"。