首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MongoDB主从复制介绍和常见问题说明

MongoDB主从复制介绍和常见问题说明

原创
作者头像
彭振翼
修改2020-03-01 16:41:13
3.5K1
修改2020-03-01 16:41:13
举报
文章被收录于专栏:MongoDB内核分析MongoDB内核分析

导语

腾讯云MongoDB的运营过程中,发现较多用户对副本集主从复制流程的理解还有些偏差。这些偏差在一定程度上影响了应用程序设计和平时的运营。

本文会聚焦下面几个问题:

  • 写大多数节点是如何完成的?
  • 从节点拉取oplog和回放oplog是否会有阻塞,如何调优?
  • Mongo Shell 上执行 printSlaveReplicationInfo 命令看主从延迟,系统压力不大时也在秒级,是否正常?
  • printSlaveReplicationInfo 为什么不能优化到毫秒级别?
  • 在从节点上执行 printSlaveReplicationInfo 命令,发现从节点的数据领先主节点,是否正常?
  • 什么是链式复制?哪些场景适合开启,哪些不适合?

主从复制架构分析

主从复制大致流程

MongoDB副本集模式下,用户向主节点写入数据,并记录oplog. 从节点通过oplog进行数据同步,最终保证副本集中的各个节点的数据一致性。

客户端可以指定写入请求的一致性级别(WriteConcern),比如对于数据一致性较高的场景,可以设置数据复制到“大多数”节点才返回成功。这样能够保证即使主节点重启后不会回滚掉之前写入的数据。

一个常见的误解:写大多数节点模型下,客户端需要将数据发到多个节点,是否会增加客户端的负担?

“写大多数”请求的流程如下,客户端只需要向主节点写入数据即可(不需也不能向从节点直接写数据);从节点进行oplog同步之后,会将自身已经同步的oplog时间点通知给主节点;主节点维护了副本集中各个从节点的oplog同步情况,如果确定数据已经到了大多数节点上(包括自己),则给客户端返回成功。如果数据同步发生了异常,或者同步太慢,则可能触发超时。

同理,ReadConcern Majority也不是客户端去读多个节点,这里不详细讨论

副本集数据同步示意图
副本集数据同步示意图

详细的主从同步流程如下图所示(以 1 Primary 1 Secondary 为例):

主从复制细节
主从复制细节

主要步骤如下:

  1. 主节点接受用户的写请求,更新用户表和oplog表。如果用户设置了 writeConcern:majority,此时由于不符合写入成功的返回条件,处理线程会阻塞
  2. 从节点上的 "rsBackgroundSync" 后台线程通过 find/getmore 命令到主节点上获取oplog,并放入到 OplogBuffer中;"replBatcher" 线程感知到OplogBuffer中的数据并消费,保存到OpQueue中;"OplogApplier" 线程感知OpQueue中的新数据,通过多个(默认16个)worker线程回放Oplog,并更新lastAppliedOpTime 和 lastDurableOpTime
  3. 从节点上的 "SyncSourceFeedback" 后台线程感知到有新数据写入成功,将自身最新的 lastAppliedOpTime和lastDurableOpTime 等信息通过 "replSetUpdatePosition" 内部命令返回给主节点
  4. 主节点接受到各个从节点 最新的 lastAppliedOpTime 和 lastDurableOpTime(writeConcernMajorityJournalDefault 配置项决定了具体以哪个时间为准),计算大多数节点(包括自己)当前的数据同步进展,并更新 lastCommittedOpTime, 然后唤醒正在等待的请求处理线程
  5. 主节点上的用户处理线程给用户返回处理结果

常见误解说明: 误解1:从节点拉取 oplog 回放完之后,才会拉取下一批 oplog 真实情况:拉取和回放属于不同的线程,相互不会阻塞 误解2:对参数 replBatchLimitBytes(默认100MB) 和 replBatchLimitOperations(默认5000) 存在误解,认为回放线程必须累积到这么多oplog后才会批量回放 真实情况:回放线程尽量累积大量数据才回放(批量并发执行效率高)。但是如果oplog比较少,会提前返回。但是极端情况下,可能会有最多阻塞1秒的情况(具体参考 sync_tail.cpp 中的 SyncTail::tryPopAndWaitForMore实现)。关于这一点,下一篇文章会结合代码和例子进行详细分析 误解3:从节点通过心跳返回同步进度,主节点根据心跳信息决定 writeConcern:majority 是否返回 真实情况:从节点通过 replSetUpdatePosition 及时上报同步情况。心跳周期太长,默认 2 秒一次,所以根据心跳信息显然是不合适的

性能调优建议

  1. 根据实际情况,调整回放线程的个数,默认 16 个。对应 replWriterThreadCount 参数,可在程序启动时指定。
  2. 根据实际情况,调整批量回放的最大 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) {
            var status = this.adminCommand({'replSetGetStatus': 1}); // replSetGetStatus命令的结果,作为本次计算的数据源
            primary = getMaster(status.members);
            if (primary) {
                startOptimeDate = primary.optimeDate;  //如果主节点存在,则选择主节点的 optime 为基准 optime
            }
            // no primary, find the most recent op among all members
            else {
                startOptimeDate = new Date(0, 0);
                for (i in status.members) { // 如果主节点不存在,则选择最新的 optime 为基准 optime
                    if (status.members[i].optimeDate > startOptimeDate) {
                        startOptimeDate = status.members[i].optimeDate;
                    }
                }
            }

            for (i in status.members) { //对除 primary 和 arbiter 的节点,打印延迟情况
                r(status.members[i]);
            }
        }
    }

总体来说,根据replSetGetStatus命令中每个 member 的 optimeDate 来计算延迟。分析内核代码发现, replSetGetStatus 命令通过心跳信息来获取其他节点的 optimeDate。

可以得出,printSlaveReplicationInfo 命令的结果依赖于心跳信息。

MongoDB副本集心跳示意图
MongoDB副本集心跳示意图

默认配置下,节点之间的心跳间隔是 2 秒,也就是说printSlaveReplicationInfo展示的可能是“过期”信息,存在一定的误差。

比如有一个持续被写入的副本集,主节点在 t1 时刻维护的还是 t0 时刻的心跳信息,则 printSlaveReplicationInfo 命令会显示从节点比主节点落后 1 秒,在主节点很快接受到从节点更新的信息之后,主从延迟又会马上变为 0 秒。出现主从延迟“抖动”的情况。

同理,按照从节点的视角来看,在 t1 时刻已经从主节点同步到了最新的数据,但是维护的主节点心跳还是 t0 时刻的“过期”数据。此时会认为主节点的 optimeDate 还是 t0,所以在从节点上执行 printSlaveReplicationInfo 命令,会看到从节点“领先”主节点 1 秒的奇怪现象。

特殊说明 心跳信息维护在 TopologyCoordinator::memberData 中 内核对主节点维护的 memberData 进行了优化:除了正常的心跳请求会更新之外,从节点发送过来的 replSetUpdatePosition 也会更新 memberData 中的数据。所以在主节点上执行 printSlaveReplicationInfo 命令 相对来说 已经尽量做到准确了。

总结:心跳信息带来的不确定性,会导致 printSlaveReplicationInfo 的结果存在误差

延迟命令的精度问题

MongoDB 使用了 BSON 格式的 TimeStamp,是一个 64 bit 的值:

  • 高 32 bit 存放 UNIX 秒级时间
  • 低 32 bit 存放 一个递增的计数器,来区分这一秒内的多条oplog

因此,TimeStamp 能够表示的精度只有秒级。

因此,printSlaveReplicationInfo 命令看到的秒级延迟不能说明主从延迟真的是秒级。除了前文说到的心跳原因,TimeStamp 的精度问题也会给观测带来误差。

链式复制

什么是链式复制

在MongoDB副本集模式中,从节点除了可以到主节点同步数据外,还可以到数据较新的另外一个从节点同步数据。

如下图所示,第2个从节点可以切换同步源到第1个从节点,这样副本集的同步关系变成了Primary1-->Secondary1-->Secondary2 链式结构

image.png
image.png

如何开启

  1. 通过 rs.reconfig() 命令将 settings.chainingAllowed设置为true(默认已经开启)
  2. 根据具体的使用场景,可以在从节点上执行 rs.syncFrom 命令指定同步源。如果不手动指定,则MongoDB后台线程会根据各个节点的 oplog 时间进行选择和切换。

适合开启链式复制的场景

链式复制带来的好处是:不用所有从节点都到主节点同步数据,可以有效减少主节点的压力。

对于写完主节点即返回,并读主节点的业务来说,开启链式复制能在一定程度上提升性能。

适合关闭链式复制的场景

链式复制带来的缺陷是:

  1. 数据复制的链路变长。对于 WriteConcern 设置比较大的请求,处理时长会变长。
  2. 读oplog的压力从主节点转移到了部分从节点上,会一定程度上影响从节点的性能。 因此,对于 {WriteConcern:majority} 的业务场景,建议关闭链式复制;对于写主读从的业务场景,可以根据实际的请求量,考虑是否关闭链式复制。

参考文档

GitHub: replication-internals

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 导语
  • 主从复制架构分析
    • 主从复制大致流程
      • 性能调优建议
      • 主从延迟命令解析
        • 延迟命令的精度问题
        • 链式复制
          • 什么是链式复制
            • 如何开启
              • 适合开启链式复制的场景
                • 适合关闭链式复制的场景
                • 参考文档
                相关产品与服务
                云数据库 MongoDB
                腾讯云数据库 MongoDB(TencentDB for MongoDB)是腾讯云基于全球广受欢迎的 MongoDB 打造的高性能 NoSQL 数据库,100%完全兼容 MongoDB 协议,支持跨文档事务,提供稳定丰富的监控管理,弹性可扩展、自动容灾,适用于文档型数据库场景,您无需自建灾备体系及控制管理系统。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档