37. MongoDB 复制集内部机制与 oplog

复制集内部机制与 oplog:oplog 结构与幂等性、容量规划与 replSetResizeOplog、初始同步三阶段与同步源选择、链式复制、w:majority 多数派提交、回滚目录处理,以及复制延迟诊断方法。

复制集对外表现为"主从加自动故障切换",内部却是一套基于 oplog 的状态机复制协议。理解 oplog 的结构、初始同步的代价、多数派提交的边界与回滚的触发条件,才能在生产事故中快速定位是复制延迟、写关注配置还是网络分区。本文从 oplog 的物理结构讲起,逐层深入到回滚与延迟诊断。

1. oplog 结构与幂等性

oplog(operation log)是复制集的心跳,它记录了主节点上所有改变数据的操作,从节点按顺序重放这些操作以保持数据一致。

1.1 oplog 文档结构

oplog 存放在 local 库的 oplog.rs 集合,每条记录是一次操作:

use local
db.oplog.rs.findOne()
{
  ts: Timestamp({ t: 1728192000, i: 1 }),
  t: Long("12"),
  h: Long("0"),
  v: 2,
  op: "i",
  ns: "shop.orders",
  ui: UUID("8f2b1c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d"),
  o: { _id: ObjectId("6521a0f1c3b4e5d6a7f80001"), amount: 100, status: "paid" },
  wall: ISODate("2026-10-06T07:00:00.000Z"),
  lsid: { id: UUID("..."), uid: BinData(0, "...") },
  txnNumber: Long("5"),
  stmtId: 0
}
字段含义
ts操作时间戳,复制集的全局序
op操作类型:i 插入、u 更新、d 删除、c 命令、n 空操作
ns命名空间,即库.集合
o操作内容
o2更新操作的查询条件
ui集合的 UUID
wall操作发生时的墙钟时间

1.2 幂等性要求

从节点重放 oplog 必须幂等,否则重复应用会导致数据不一致。这决定了两条铁律:

// 错误:基于位置的更新不幂等
db.orders.updateOne({ _id: 1 }, { $set: { "items.0.qty": 5 } })

// 正确:基于标识的更新幂等
db.orders.updateOne({ _id: 1, "items.sku": "A1" }, { $set: { "items.$.qty": 5 } })
// 数组操作符重放安全
db.orders.updateOne({ _id: 1 }, { $pull: { tags: "old" } })   // 幂等
db.orders.updateOne({ _id: 1 }, { $pop: { tags: 1 } })         // 不幂等,慎用

注意:$pop、$inc 在重放时若非基于精确条件,可能产生重复效果。虽然 MongoDB 通过 o2 记录更新条件来保证重放语义,但应用层仍应避免依赖"相对位置"的更新。

1.3 oplog 的写入路径

主节点在完成本地写后,把操作追加到 oplog,从节点通过 tailable cursor 拉取并应用。oplog 本身是 capped collection,写满后覆盖最旧记录。

// 查看 oplog 是否为 capped 集合
db.oplog.rs.stats().capped        // true
db.oplog.rs.stats().maxSize       // 字节数

2. oplog 容量与大小规划

oplog 的大小决定了从节点能落后多久仍可增量同步。oplog 写满覆盖后,落后的从节点将无法增量追赶,只能做代价高昂的初始同步。

2.1 默认大小与规划原则

默认 oplog 为可用磁盘空间的 5%,最小值 990MB,最大值 50GB。规划原则:oplog 覆盖的时间窗口应能扛住一次完整备份加恢复的时间。

// 查看当前 oplog 配置与覆盖时间
rs.printReplicationInfo()
configured oplog size:   10240MB
log length start to end: 43200secs (12hrs)
oplog first event time:  Thu Oct 05 2026 19:00:00 GMT+0800
oplog last event time:   Thu Oct 06 2026 07:00:00 GMT+0800
now:                     Thu Oct 06 2026 07:00:01 GMT+0800

若 log length start to end 只有几小时,而备份恢复需要一天,则一次故障就会触发初始同步。

2.2 调整 oplog 大小

// 在线调整 oplog 为 20GB(单位 MB)
db.adminCommand({ replSetResizeOplog: 1, size: 20480 })

// 从节点上同样执行,或临时生效
db.adminCommand({ replSetResizeOplog: 1, size: 20480, temporary: true })
参数说明注意
size新大小,单位 MB只能增大,不能低于已用空间
minRetentionHours按时间保留下限6.0 起支持,可保证时间窗口
// 6.0 起:保证 oplog 至少覆盖 24 小时
db.adminCommand({ replSetResizeOplog: 1, minRetentionHours: 24 })

决策铁律:oplog 大小不是"越大越好",而是"覆盖时间必须大于最坏恢复时间"。写入密集的集群建议按写入速率估算:所需小时数乘以每小时 oplog 生成量,再留一倍余量。

3. 初始同步三阶段

当新节点加入复制集,或落后太多的节点无法增量追赶时,会触发初始同步(initial sync)。它分为三个阶段。

3.1 三阶段流程

阶段动作耗时来源
克隆(Clone)复制源的所有数据,本地库除外数据量、网络带宽
应用(Apply)应用克隆期间产生的新 oplog写入速率
追平(Catch-up)构建索引并应用剩余 oplog索引数量
// 观察初始同步进度
db.adminCommand({ replSetGetStatus: 1 }).members.forEach(m => {
  print(m.name, m.stateStr, m.syncSourceHost)
})
rs-shard02:27018  STARTUP2   rs-shard01:27018

STARTUP2 即表示正在初始同步。同步期间该节点不可读(除非配置了 secondaryOk)。

3.2 选择同步源

initialSyncSourceReadPreference 决定初始同步从哪个节点拉数据。默认 primaryPreferred,可改为优先从从节点拉取,避免拖垮主节点。

// 通过副本集配置设置同步源偏好
cfg = rs.conf()
cfg.settings.initialSyncSourceReadPreference = "nearest"
rs.reconfig(cfg)
取值含义适用
primaryPreferred优先主节点(默认)常规
secondaryPreferred优先从节点减轻主节点压力
nearest最近节点多机房部署
primary强制主节点强一致要求

3.3 加速初始同步的手段

  • 用文件系统快照(如 LVM、云盘快照)拷贝数据目录,跳过克隆阶段
  • 从同机房的节点同步,减少网络延迟
  • 同步期间暂停非必要业务写入
# 通过文件系统快照做种子数据(示意)
mongodump --host rs-shard01:27018 --db shop --out /seed/shop
mongorestore --host rs-shard02:27018 --dir /seed/shop --oplogReplay

重要:初始同步会全量拉取数据,对源节点造成显著压力。生产环境应避免让多个节点同时做初始同步,并优先用快照种子缩短窗口。

4. 链式复制与 chainingAllowed

链式复制允许从节点从其他从节点同步,而不是全部从主节点拉取,从而减轻主节点压力。

4.1 开启与关闭

cfg = rs.conf()
cfg.settings.chainingAllowed = true    // 默认开启
rs.reconfig(cfg)

关闭链式复制后,所有从节点都直接从主节点同步:

cfg.settings.chainingAllowed = false
rs.reconfig(cfg)

4.2 同步源的观察

db.adminCommand({ replSetGetStatus: 1 }).members.forEach(m => {
  print(m.name, m.stateStr, "syncSource:", m.syncSourceHost || "(primary)")
})
rs-shard01:27018  PRIMARY    syncSource: (primary)
rs-shard02:27018  SECONDARY  syncSource: rs-shard01:27018
rs-shard03:27018  SECONDARY  syncSource: rs-shard02:27018
拓扑主节点压力延迟传播适用
全从主同步高短节点少
链式复制低逐跳累加节点多、跨机房
混合中中常规生产

注意:链式复制会放大延迟,链越深尾节点落后越多。对延迟敏感的从节点(如读偏好 secondary 的报表节点)应显式指定从主节点同步,或关闭链式复制。

5. 写关注与多数派提交

5.1 w:majority 与多数派提交

w:majority 表示写操作被多数节点确认后才算成功。多数派提交点(majority commit point)决定哪些数据"已提交且不会回滚"。

db.orders.insertOne(
  { orderId: "A1001", amount: 100 },
  { writeConcern: { w: "majority", wtimeout: 5000 } }
)

5.2 writeConcernMajorityJournalDefault

该参数决定 w:majority 是否隐含 j:true(写入 journal 才算确认)。默认 true。

// 查看副本集配置中的该参数
rs.conf().settings.writeConcernMajorityJournalDefault    // true
// 关闭时(不推荐):多数派确认不等待 journal
cfg = rs.conf()
cfg.settings.writeConcernMajorityJournalDefault = false
rs.reconfig(cfg)
配置语义风险
true(默认)多数派写入 journal 后确认延迟略高,最安全
false多数派写入内存后确认崩溃可能丢已确认写
// 查看当前各节点的提交点
db.adminCommand({ replSetGetStatus: 1 }).optimes
// { lastCommittedOpTime: {...}, appliedOpTime: {...}, durableOpTime: {...} }

决策铁律:涉及资金的集合一律 w:majority 加 j:true。w:1 只保证主节点接收,主节点崩溃时未复制的写会回滚。写关注是可用性与一致性的旋钮,必须按业务等级分级配置。

5.3 读关注与写关注搭配

// 强一致读:只读多数派已提交的数据
db.orders.find({ orderId: "A1001" }).readConcern("majority")

// 线性化读:配合 w:majority 写,保证读到最新已提交
db.orders.find({ orderId: "A1001" }).readConcern("linearizable")

6. 回滚处理与复制延迟诊断

6.1 回滚的触发条件

当主节点在分区期间接受了写入,但未复制到多数派就被降级,重新加入时它上面那些"多数派没有的写"必须回滚。回滚数据被写入 dbPath 下的 rollback/ 目录。

// 查看是否发生回滚
db.adminCommand({ replSetGetStatus: 1 }).members.forEach(m => {
  if (m.stateStr === "ROLLBACK") print(m.name, "ROLLBACK")
})
# 回滚目录示例
ls /var/lib/mongodb/rollback/
# shop.orders.2026-10-06T07-12-33.0.bson
# shop.orders.2026-10-06T07-12-33.0.bson

6.2 回滚数据的处理

回滚的 BSON 文件需要人工审查,决定哪些操作要重新提交:

# 用 bsondump 查看回滚内容
bsondump /var/lib/mongodb/rollback/shop.orders.2026-10-06T07-12-33.0.bson
// 审查后手工重放(示例,需谨慎)
db.orders.insertOne({ orderId: "A1002", amount: 200 })   // 从回滚文件恢复
场景处理方式
回滚数据是重复写丢弃
回滚数据是有效业务写审查后重放
频繁回滚检查网络稳定性与写关注配置

重要:回滚是数据丢失的信号。回滚窗口默认 30 分钟(rollbackTimeLimitSecs),超过则节点需要重新初始同步。若频繁出现回滚,说明网络分区与写关注配置存在系统性问题。

6.3 复制延迟诊断

// 查看各从节点落后主节点的时间
rs.printSecondaryReplicationInfo()
source: rs-shard01:27018
  syncedTo: Thu Oct 06 2026 07:00:00 GMT+0800
  0 secs (0 hrs) behind the primary
source: rs-shard01:27018
  syncedTo: Thu Oct 06 2026 06:59:42 GMT+0800
  18 secs (0 hrs) behind the primary
// 深入指标:oplog 应用队列与批量情况
db.serverStatus().metrics.repl
{
  executor: { counters: { ... }, queues: { networkInProgress: 0, networkInProgress: 0 } },
  apply: {
    batches: { num: 1284, totalMillis: 5120 },
    ops: 98765,
    earliestOplogEntryTime: ISODate("2026-10-06T07:00:00Z"),
    latestOplogEntryTime: ISODate("2026-10-06T07:00:18Z")
  },
  buffer: { count: 0, maxSizeBytes: 268435456, sizeBytes: 0 },
  network: { bytes: 104857600, getmores: { num: 1024, totalMillis: 320 } },
  initialSync: { completed: 1, failedAttempts: 0 }
}
指标含义异常信号
apply.ops已应用的 oplog 条数增速远低于主节点写入
apply.batches.totalMillis应用批次的耗时持续偏高说明从节点 CPU 瓶颈
buffer.sizeBytes待应用的 oplog 缓冲持续增长说明应用跟不上
repl.network.getmores拉取次数与耗时耗时高说明网络瓶颈
// 查看从节点是否在读自己的 oplog(读偏好 secondary 时的常见问题)
db.serverStatus().oplogTruncation    // 截断信息

决策铁律:复制延迟先分清是"网络拉取慢"还是"本地应用慢"。看 network.getmores.totalMillis 高则是网络,看 apply.batches.totalMillis 高则是磁盘或 CPU。定位到环节再优化,不要盲目加索引或扩机器。

7. 总结与最佳实践

  • oplog 结构:capped 集合,按 ts 排序,重放必须幂等,避免位置依赖的更新
  • 容量规划:覆盖时间大于最坏恢复时间,用 minRetentionHours 兜底时间窗口
  • 初始同步:三阶段克隆、应用、追平,用快照种子加速,避免多节点同时同步
  • 链式复制:默认开启可减轻主节点压力,但会放大尾节点延迟
  • 写关注:核心数据一律 w:majority 加 j:true,writeConcernMajorityJournalDefault 保持默认 true
  • 回滚处理:回滚目录的 BSON 需人工审查,频繁回滚要查网络与写关注配置
  • 延迟诊断:区分网络拉取与本地应用两个环节,再针对性优化

决策铁律:复制集的一切异常最终都能落到三个指标上——oplog 覆盖时间、多数派提交点、从节点应用延迟。把这三点纳入监控大盘,比事后翻日志高效得多。写关注与读关注的组合必须与业务一致性等级一一对应,不能全站一刀切。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「mongodb」更多文章

  1. 数据生命周期、TTL 与冷热归档
  2. $graphLookup 与层次结构建模
  3. GridFS 与大文件存储实践