3. 跨地域复制协议与数据同步原理

跨地域复制并不是把数据文件整块搬运到远端,而是由主节点把每一次可复制的数据变更记录成一条条操作日志,从节点按顺序拉取并重放。理解这条链路,才能判断跨地域场景下延迟从哪里来、一致性如何保证、故障切换后会不会丢数据。本节围绕 oplog 机制、复制过程、写关注与读关注四个关键点展开。

oplog 是复制协议的唯一事实来源

oplog(operations log)是 local 库中的固定大小集合,默认集合名为 local.oplog.rs。它采用 capped collection 语义:写入顺序追加,容量写满后最旧的记录被覆盖。每条 oplog 记录包含几个关键字段:ts 是 BSON 时间戳,由秒级时间与同一秒内递增序号组成,是全局单调递增的复制位点;op 表示操作类型,常见有 i(insert)、u(update)、d(delete)、c(command,如建索引、建集合);ns 是命名空间,形如 db.collection;o 是操作内容,o2 通常保存更新时的 _id 条件。幂等性是重放的前提:insert 重放为 upsert,update 用 $set 等修饰符表达,delete 重放为幂等删除,因此从节点重复应用同一条记录不会产生额外副作用。

oplog 容量决定了从节点可落后主节点的最大时间窗口。假设业务峰值写入速率为 2000 条 oplog 记录/秒,平均每条记录 200 字节,则每秒产生约 400 KB。若 oplog 配置为 10 GB,理论覆盖窗口约为 10 GB ÷ 400 KB/s ≈ 25000 秒,约 7 小时。跨地域链路一旦中断超过这个窗口,从节点将因找不到起始位点而进入 RECOVERING 状态,必须重新做初始同步。因此跨地域部署时 oplog 容量通常要按“最长可容忍中断时间 × 峰值写入速率”来估算,而不是沿用单机房默认值。

跨地域复制链路:oplog 生成 → 拉取 → 重放 → 位点确认 主节点(Region A) 客户端写入 + writeConcern w:majority / j:true local.oplog.rs(capped) ts / op / ns / o / o2 从节点(Region B) tailable cursor 拉取 按 ts 顺序批量获取 oplog 应用线程重放 幂等:i→upsert, u→$set 位点与确认 lastApplied / lastDurable 记录已应用与已持久位点 多数派确认后推进 commit point 前移 getMore ack 跨地域延迟构成与写关注的关系 本地写入 + 落盘 约 1~5 ms 跨域 RTT(A↔B) 约 30~120 ms 从节点重放 + 确认 约 5~20 ms w:majority 返回 ≈ 本地 + RTT 读关注影响:readConcern:"local" 读本地最新,可能读到尚未被多数派确认的数据; readConcern:"majority" 只读已提交位点之前的数据,跨域下需等待 commit point 推进,延迟更高但不会回滚。

复制过程:拉取、重放与位点推进

从节点启动后,先与主节点握手,确定同步源,然后打开一个针对 local.oplog.rs 的 tailable cursor,用 getMore 批量拉取新记录。拉取到的记录进入应用队列,由重放线程按 ts 顺序逐条应用。应用完成后,从节点更新自己的 lastApplied 位点;当该位点被写入并持久化后,再更新 lastDurable。主节点通过从节点上报的位点维护每个成员的复制进度,并据此计算多数派提交点(commit point)。

跨地域场景下,拉取和应用是流水线并行的:网络在传下一批,本地在重放上一批。因此实际复制延迟约等于“网络传输时间 + 队列等待时间 + 重放时间”。如果从节点重放能力不足,队列会持续增长,表现为 replication lag 上升,此时即使网络带宽充足也无法降低延迟。判断复制健康度可以观察 rs.status() 中成员的 optimeDate 与主节点 optimeDate 的差值,以及 db.serverStatus().metrics.repl.buffer 中的队列大小。

写关注对跨地域复制的影响

写关注决定“写操作在什么条件下才算成功”。跨地域部署中,w:1 只要求主节点本地确认,延迟最低,但主节点故障时可能丢失尚未复制到远端的写入;w:majority 要求多数派成员确认,跨三地域五节点部署时通常需要至少三个节点确认,其中可能包含一个远端节点,因此写延迟会明显受跨域 RTT 影响。若再叠加 j:true,还要求写入 journal 后才返回,进一步增加延迟但提升持久性。

一个常见的跨地域副本集布局是:Region A 两个节点、Region B 两个节点、Region C 一个仲裁或低优先级节点。此时多数派为 3。若客户端在 Region A 写入并设置 w:majority,主节点需要等待 Region B 的至少一个节点确认,写延迟约等于 A↔B 的 RTT。若把 w 设为 "majority" 且 wtimeout 设为 5000 ms,当跨域链路抖动导致确认超时,写操作会返回 WriteConcernError,但数据可能已经写入主节点,需要业务侧根据 wtimeout 语义决定是否重试。

// 跨地域写入示例:要求多数派确认并落盘,超时 5 秒
db.orders.insertOne(
  { _id: 1001, region: "A", amount: 268.00, ts: new Date() },
  { writeConcern: { w: "majority", j: true, wtimeout: 5000 } }
)

// 查看各成员复制位点与延迟
rs.status().members.map(m => ({
  name: m.name,
  state: m.stateStr,
  optime: m.optimeDate,
  lagSec: (rs.status().date - m.optimeDate) / 1000
}))

读关注对跨地域复制的影响

读关注决定“读操作能看到哪个位点的数据”。readConcern:"local" 读取节点本地已应用的最新数据,延迟最低,但在跨地域场景下可能读到尚未被多数派确认、未来可能回滚的数据;readConcern:"majority" 只读取已提交位点之前的数据,保证不会读到回滚数据,但需要等待 commit point 推进,跨域下读延迟更高;readConcern:"linearizable" 进一步要求读操作看到此前所有已确认写入,仅适用于主节点读,延迟最高。

读偏好与读关注需要配合使用。若业务希望就近读取以降低延迟,可设置 readPreference:"nearest",但必须接受该节点可能落后于主节点;若业务要求强一致,应使用 readPreference:"primary" 配合 readConcern:"majority",代价是读请求必须回到主节点所在区域。跨地域系统的常见折中是:核心交易读走主节点 + majority,报表与查询类读走就近从节点 + local,并接受秒级延迟。

判断复制是否健康,优先看三个量:主从 optimeDate 差值、oplog 剩余可覆盖时间窗口、以及从节点应用队列长度。三者中任意一个持续恶化,都意味着跨地域复制链路存在瓶颈。
oplog 窗口不足是跨地域复制最常见的隐性风险。链路中断时间一旦超过 oplog 覆盖窗口,从节点无法增量追赶,只能重新初始同步,期间该节点不可用于读扩展,也会加重主节点与网络负担。

把四个概念串起来看:oplog 是复制的内容载体,复制过程决定数据如何从主节点流到远端,写关注决定客户端何时认为写成功,读关注决定客户端能看到哪个一致性的数据。跨地域部署的所有延迟与一致性权衡,最终都落在“多数派确认需要跨几次域、读请求是否允许读到未提交位点”这两个判断上。设计时先明确业务对丢失与回滚的容忍度,再反推 w、j、readConcern 与 readPreference 的组合,而不是先选参数再补一致性论证。