跳到主要内容

副本集与高可用

架构​

副本集(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 格式。

复制流程:

  1. Primary 执行写操作,记录 oplog
  2. Secondary 主动拉取(pull) Primary 的 oplog(注意是拉而不是推,与 MySQL 的主库 dump 线程推送相反)
  3. 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 秒。

流程:

  1. 候选节点自增 term,向其他节点请求投票
  2. 获得超过半数投票者的票即当选
  3. 每个投票者在一个 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" 写入就不会被回滚 —— 这是避免此问题的根本办法

特殊节点​

类型配置用途
Hiddenhidden: true, priority: 0客户端看不到,用于备份、离线分析
DelayedslaveDelay: 3600刻意延迟 N 秒,误删数据的时间机器
ArbiterarbiterOnly: true只投票,不推荐
Non-votingvotes: 0超过 7 个成员时的额外只读副本

Delayed 节点是实用的防误操作手段 —— 有人 drop 了集合,可以从延迟节点捞回来(前提是在延迟窗口内发现)。注意它必须同时是 hidden 且 priority: 0。