原文连接:https://cloud.tencent.com/developer/article/1592500
导语
在腾讯云MongoDB的运营过程中,发现较多用户对副本集主从复制流程的理解还有些偏差。这些偏差在一定程度上影响了应用程序设计和平时的运营。
本文会聚焦下面几个问题:
- 写大多数节点是如何完成的?
- 从节点拉取oplog和回放oplog是否会有阻塞,如何调优?
- Mongo Shell 上执行 printSlaveReplicationInfo 命令看主从延迟,系统压力不大时也在秒级,是否正常?
- printSlaveReplicationInfo 为什么不能优化到毫秒级别?
- 在从节点上执行 printSlaveReplicationInfo 命令,发现从节点的数据领先主节点,是否正常?
- 什么是链式复制?哪些场景适合开启,哪些不适合?
主从复制架构分析
主从复制大致流程
MongoDB副本集模式下,用户向主节点写入数据,并记录oplog. 从节点通过oplog进行数据同步,最终保证副本集中的各个节点的数据一致性。
客户端可以指定写入请求的一致性级别(WriteConcern),比如对于数据一致性较高的场景,可以设置数据复制到“大多数”节点才返回成功。这样能够保证即使主节点重启后不会回滚掉之前写入的数据。
一个常见的误解:写大多数节点模型下,客户端需要将数据发到多个节点,是否会增加客户端的负担?
“写大多数”请求的流程如下,客户端只需要向主节点写入数据即可(不需也不能向从节点直接写数据);从节点进行oplog同步之后,会将自身已经同步的oplog时间点通知给主节点;主节点维护了副本集中各个从节点的oplog同步情况,如果确定数据已经到了大多数节点上(包括自己),则给客户端返回成功。如果数据同步发生了异常,或者同步太慢,则可能触发超时。
同理,ReadConcern Majority也不是客户端去读多个节点,这里不详细讨论
详细的主从同步流程如下图所示(以 1 Primary 1 Secondary 为例):
主要步骤如下:
- 主节点接受用户的写请求,更新用户表和oplog表。如果用户设置了 writeConcern:majority,此时由于不符合写入成功的返回条件,处理线程会阻塞
- 从节点上的 "rsBackgroundSync" 后台线程通过 find/getmore 命令到主节点上获取oplog,并放入到 OplogBuffer中;"replBatcher" 线程感知到OplogBuffer中的数据并消费,保存到OpQueue中;"OplogApplier" 线程感知OpQueue中的新数据,通过多个(默认16个)worker线程回放Oplog,并更新lastAppliedOpTime 和 lastDurableOpTime
- 从节点上的 "SyncSourceFeedback" 后台线程感知到有新数据写入成功,将自身最新的 lastAppliedOpTime和lastDurableOpTime 等信息通过 "replSetUpdatePosition" 内部命令返回给主节点
- 主节点接受到各个从节点 最新的 lastAppliedOpTime 和 lastDurableOpTime(writeConcernMajorityJournalDefault 配置项决定了具体以哪个时间为准),计算大多数节点(包括自己)当前的数据同步进展,并更新 lastCommittedOpTime, 然后唤醒正在等待的请求处理线程
- 主节点上的用户处理线程给用户返回处理结果
常见误解说明: 误解1:从节点拉取 oplog 回放完之后,才会拉取下一批 oplog 真实情况:拉取和回放属于不同的线程,相互不会阻塞 误解2:对参数 replBatchLimitBytes(默认100MB) 和 replBatchLimitOperations(默认5000) 存在误解,认为回放线程必须累积到这么多oplog后才会批量回放 真实情况:回放线程尽量累积大量数据才回放(批量并发执行效率高)。但是如果oplog比较少,会提前返回。但是极端情况下,可能会有最多阻塞1秒的情况(具体参考 sync_tail.cpp 中的 SyncTail::tryPopAndWaitForMore实现)。关于这一点,下一篇文章会结合代码和例子进行详细分析 误解3:从节点通过心跳返回同步进度,主节点根据心跳信息决定 writeConcern:majority 是否返回 真实情况:从节点通过 replSetUpdatePosition 及时上报同步情况。心跳周期太长,默认 2 秒一次,所以根据心跳信息显然是不合适的
性能调优建议
- 根据实际情况,调整回放线程的个数,默认 16 个。对应 replWriterThreadCount 参数,可在程序启动时指定。
- 根据实际情况,调整批量回放的最大 oplog 条数(默认 5000)和最大 oplog 大小(默认 100MB)。前者对应 replBatchLimitOperations 参数,可在程序启动时或者运行过程中指定;后者对应 replBatchLimitBytes 参数,在 官方文档中说明可以动态修改,但是实测发现并不成功,代码中也没有找到修改的接口。如果有变更需求,可以直接修改 sync_tail.h 中 replBatchLimitBytes 的初始化代码
主从延迟命令解析
MongoDB 管理员使用 printSlaveReplicationInfo 命令来观察主从延迟情况
printSlaveReplicationInfo 是 MongoShell 封装的 js 命令,可以在任意一个MongoShell客户端上直接执行db.printSlaveReplicationInfo 查看 js 源代码。如下所示:
function () {
var startOptimeDate = null; // 基准optime var primary = null; // 根据基准optime,打印节点的延迟情况,精确到秒 function getReplLag(st) { assert(startOptimeDate, "how could this be null (getReplLag startOptimeDate)"); print("\tsyncedTo: " + st.toString()); var ago = (startOptimeDate - st) / 1000; var hrs = Math.round(ago / 36) / 100; var suffix = ""; if (primary) { suffix = "primary "; } else { suffix = "freshest member (no primary available at the moment)"; } print("\t" + Math.round(ago) + " secs (" + hrs + " hrs) behind the " + suffix); } function getMaster(members) { for (i in members) { var row = members[i]; if (row.state === 1) { return row; } } return null; } function g(x) { assert(x, "how could this be null (printSlaveReplicationInfo gx)"); print("source: " + x.host); if (x.syncedTo) { var st = new Date(DB.tsToSeconds(x.syncedTo) * 1000); getReplLag(st); } else { print("\tdoing initial sync"); } } function r(x) { assert(x, "how could this be null (printSlaveReplicationInfo rx)"); if (x.state == 1 || x.state == 7) { // ignore primaries (1) and arbiters (7) return; } print("source: " + x.name); if (x.optime) { getReplLag(x.optimeDate); } else { print("\tno replication info, yet. State: " + x.stateStr); } } var L = this.getSiblingDB("local"); if (L.system.replset.count() != 0) {