热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:MongoDB复制集原理

篇首语:本文由编程笔记#小编为大家整理,主要介绍了MongoDB复制集原理相关的知识,希望对你有一定的参考价值。版权声明:本文由孔德雨原创文章,转载请注明出处: 

篇首语:本文由编程笔记#小编为大家整理,主要介绍了MongoDB复制集原理相关的知识,希望对你有一定的参考价值。


版权声明:本文由孔德雨原创文章,转载请注明出处: 
文章原文链接:https://www.qcloud.com/community/article/136

来源:腾云阁 https://www.qcloud.com/community

 

MongoDB的单实例模式下,一个mongod进程为一个实例,一个实例中包含若干db,每个db包含若干张表。
MongoDB通过一张特殊的表local.oplog.rs存储oplog,该表的特点是:固定大小,满了会删除最旧记录插入新记录,而且只支持append操作,因此可以理解为一个持久化的ring-buffer。oplog是MongoDB复制集的核心功能点。
MongoDB复制集是指MongoDB实例通过复制并应用其他实例的oplog达到数据冗余的技术。

常用的复制集构成一般有下图两种方式 (注意,可以使用mongoshell 手工指定复制源,但mongdb不保证这个指定是持久的,下文会讲到在某些情况下,MongoDB会自动进行复制源切换)。
技术分享

MongoDB的复制集技术并不少见,很类似mysql的异步复制模式,这种模式主要有几个技术点:



  1. 新节点加入,正常同步前的初始化



  2. Primary节点挂掉后,剩余的Secondary节点如何提供服务



  3. 如何保证主节点挂掉后数据不丢失/主节点挂掉后丢失数据的处理

MongoDB作为一个成熟的数据库产品,较好的解决了上述问题,一个完整的复制集包含如下几点功能:



  1. 数据同步
    1.1 initial-sync
    1.2steady-sync
    1.3异常数据回滚



  2. MongoDB集群心跳与选举




一.数据同步


initial_sync

当一个节点刚加入集群时,它需要初始化数据使得 自身与集群中其它节点的数据量差距尽量少,这个过程称为initial-sync。
一个initial-sync 包括六步(阅读rs_initialSync.cpp:_initialSync函数的逻辑)



  1. 删除本地除local库以外的所有db

  2. 选取一个源节点,将源节点中的所有db导入到本地(注意,此处只导入数据,不导入索引)

  3. 将2)开始执行到执行结束中源产生的oplog 应用到本地

  4. 将3)开始执行到执行结束中源产生的oplog 应用到本地

  5. 从源将所有table的索引在本地重建(导入索引)

  6. 将5)开始执行到执行结束中源产生的oplog 应用到本地
    当第6)步结束后,源和本地的差距足够小,MongoDB进入Secondary(从节点)状态。

第2)步要拷贝所有数据,因此一般第2)步消耗时间最长,第3)与第4)步是一个连续逼近的过程,MongoDB这里做了两次
是因为第2)步一般耗时太长,导致第3)步数据量变多,间接受到影响。然而这么做并不是必须的,rs_initialSync.cpp:384 开始的TODO建议使用SyncTail的方式将数据一次性读回来(SyncTail以及TailableCursor的行为与原理如果不熟悉请看官方文档

技术分享


steady-sync

当节点初始化完成后,会进入steady-sync状态,顾名思义,正常情况下,这是一个稳定静默运行于后台的,从复制源不断同步新oplog的过程。该过程一般会出现这两种问题:



  1. 复制源写入过快(或者相对的,本地写入速度过慢),复制源的oplog覆盖了 本地用于同步源oplog而维持在源的游标。

  2. 本节点在宕机之前是Primary,在重启后本地oplog有和当前Primary不一致的Oplog。
    这两种情况分别如下图所示:
    技术分享

技术分享

这两种情况在bgsync.cpp:_produce函数中,虽然这两种情况很不一样,但是最终都会进入 bgsync.cpp:_rollback函数处理,
对于第二种情况,处理过程在rs_rollback.cpp中,具体步骤为:



  1. 维持本地与远程的两个反向游标,以线性的时间复杂度找到LCA(最近公共祖先,上conflict.png中为Record4)
    该过程与经典的两个有序链表找公共节点的过程类似,具体实现在roll_back_local_operations.cpp:syncRollBackLocalOperations中,读者可以自行思考这一过程如何以线性时间复杂度实现。



  2. 针对本地每个冲突的oplog,枚举该oplog的类型,推断出回滚该oplog需要的逆操作并记录,如下:
    2.1: create_table -> drop_table
    2.2: drop_table -> 重新同步该表
    2.3: drop_index -> 重新同步并构建索引
    2.4: drop_db -> 放弃rollback,改由用户手工init_resync
    2.5: apply_ops -> 针对apply_ops 中的每一条子oplog,递归执行 2)这一过程
    2.6: create_index -> drop_index
    2.7: 普通文档的CUD操作 -> 从Primary重新读取真实值并替换。相关函数为:rs_rollback.cpp:refetch



  3. 针对2)中分析出的每条oplog的处理方式,执行处理,相关函数为 rs_rollback.cpp:syncFixUp,此处操作主要是对步骤2)的实践,实际处理过程相当繁琐。



  4. truncate掉本地冲突的oplog。
    上面我们说到,对于本地失速(stale)的情况,也是走_rollback 流程统一处理的,对于失速,走_rollback时会在找LCA这步失败,之后会尝试更换复制源,方法为:从当前存活的所有secondary和primary节点中找一个使自己“不处于失速”的节点。
    这里有必要解释一下,oplog是一个有限大小的ring-buffer, 失速的唯一判断条件为:本地维护在复制源的游标被复制源的写覆盖(想象一下你和同学同时开始绕着操场跑步,当你被同学超过一圈时,你和同学相遇了)。因此如果某些节点的oplog设置的比较大,绕完一圈的时间就更长,利用这样的节点作为复制源,失速的可能性会更小。
    对MongoDB的集群数据同步的描述暂告段落。我们利用一张流程图来做总结:
    技术分享


steady-sync的线程模型与Oplog指令乱序加速

与steady-sync相关的代码有 bgsync.cpp, sync_tail.cpp。上面我们介绍过,steady-sync过程从复制源读取新产生的oplog,并应用到本地,这样的过程脱不离是一个producer-consumer模型。由于oplog需要保证顺序性,producer只能单线程实现。
对于consumer端,是否有并发提速机制呢?



  1. 首先,不相干的文档之间无需保证oplog apply的顺序,因此可以对oplog 按照objid 哈希分组。每一组内必须保证严格的写入顺序性。

    572 void fillWriterVectors(OperationContext* txn,
    573 MultiApplier::Operations* ops,
    574 std::vector* writerVectors) {
    581 for (auto&& op : *ops) {
    582 StringMapTraits::HashedKey hashedNs(op.ns);
    583 uint32_t hash = hashedNs.hash();
    584
    585 // For doc locking engines, include the _id of the document in the hash so we get
    586 // parallelism even if all writes are to a single collection. We can‘t do this for capped
    587 // collections because the order of inserts is a guaranteed property, unlike for normal
    588 // collections.
    589 if (supportsDocLocking && op.isCrudOpType() && !isCapped(txn, hashedNs)) {
    590 BSONElement id = op.getIdElement();
    591 const size_t idHash = BSONElement::Hasher()(id);
    592 MurmurHash3_x86_32(&idHash, sizeof(idHash), hash, &hash);
    593 }
    601 auto& writer = (*writerVectors)[hash % numWriters];
    602 if (writer.empty())
    603 writer.reserve(8); // skip a few growth rounds.
    604 writer.push_back(&op);
    605 }
    606 }


  2. 其次对于command命令,会对表或者库有全局性的影响,因此command命令必须在当前的consumer完成工作之后单独处理,而且在处理command oplog时,不能有其他命令同时执行。这里可以类比SMP体系结构下的
    cpu-memory-barrior

    899 // Check for ops that must be processed one at a time.
    900 if (entry.raw.isEmpty() || // sentinel that network queue is drained.
    901 (entry.opType[0] == ‘c‘) || // commands.
    902 // Index builds are achieved through the use of an insert op, not a command op.
    903 // The following line is the same as what the insert code uses to detect an index build.
    904 (!entry.ns.empty() && nsToCollectionSubstring(entry.ns) == "system.indexes")) {
    905 if (ops->getCount() == 1) {
    906 // apply commands one-at-a-time
    907 _networkQueue->consume(txn);
    908 } else {
    909 // This op must be processed alone, but we already had ops in the queue so we can‘t
    910 // include it in this batch. Since we didn‘t call consume(), we‘ll see this again next
    911 // time and process it alone.
    912 ops->pop_back();
    913 }


  3. 从库和主库的oplog 顺序必须完全一致,因此不管1、2步写入用户数据的顺序如何,oplog的必须保证顺序性。对于mmap引擎的capped-collection,只能以顺序插入来保证,因此对oplog的插入是单线程进行的。对于wiredtiger引擎的capped-collection,可以在ts(时间戳字段)上加上索引,从而保证读取的顺序与插入的顺序无关。

    517 // Only doc-locking engines support parallel writes to the oplog because they are required to
    518 // ensure that oplog entries are ordered correctly, even if inserted out-of-order. Additionally,
    519 // there would be no way to take advantage of multiple threads if a storage engine doesn‘t
    520 // support document locking.
    521 if (!enoughToMultiThread ||
    522 !txn->getServiceContext()->getGlobalStorageEngine()->supportsDocLocking()) {
    523
    524 threadPool->schedule(makeOplogWriterForRange(0, ops.size()));
    525 return false;
    526 }


steady-sync 的类依赖与线程模型总结如下图:
技术分享


二.MongoDB心跳与选举机制

MongoDB的主节点选举由心跳触发。一个复制集N个节点中的任意两个节点维持心跳,每个节点维护其他N-1个节点的状态(该状态仅是该节点的POV,比如因为网络分区,在同一时刻A观察C处于down状态,B观察C处于seconary状态)

以任意一个节点的POV,在每一次心跳后会企图将主节点降级(step down primary)(topology_coordinator_impl.cpp:_updatePrimaryFromHBData),主节点降级的理由如下:



  1. 心跳检测到有其他primary节点的优先级高于当前主节点,则尝试将主节点降级(stepDown) 为
    Secondary, primary值的动态变更提供给了运维一个可以热变更主节点的方式

  2. 本节点若是主节点,但是无法ping通集群中超过半数的节点(majority原则),则将自身降级为Secondary

选举主节点

Secondary节点检测到当前集群没有存活的主节点,则尝试将自身选举为Primary。主节点选举是一个二阶段过程+多数派协议。


第一阶段

以自身POV,检测自身是否有被选举的资格:



  1. 能ping通集群的过半数节点

  2. priority必须大于0

  3. 不能是arbitor节点
    如果检测通过,向集群中所有存活节点发送FreshnessCheck(询问其他节点关于“我”是否有被选举的资格)

同僚仲裁

选举第一阶段中,某节点收到其他节点的选举请求后,会执行更严格的同僚仲裁



  1. 集群中有其他节点的primary比发起者高

  2. 不能是arbitor节点

  3. primary必须大于0

  4. 以冲裁者的POV,发起者的oplog 必须是集群存活节点中oplog最新的(可以有相等的情况,大家都是最新的)


第二阶段

发起者向集群中存活节点发送Elect请求,仲裁者收到请求的节点会执行一系列合法性检查,如果检查通过,则仲裁者给发起者投一票,并获得30秒钟“选举锁”,选举锁的作用是:在持有锁的时间内不得给其他发起者投票。
发起者如果或者超过半数的投票,则选举通过,自身成为Primary节点。获得低于半数选票的原因,除了常见的网络问题外,相同优先级的节点同时通过第一阶段的同僚仲裁并进入第二阶段也是一个原因。因此,当选票不足时,会sleep[0,1]秒内的随机时间,之后再次尝试选举。

 

































推荐阅读
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • 电话号码的字母组合解题思路和代码示例
    本文介绍了力扣题目《电话号码的字母组合》的解题思路和代码示例。通过使用哈希表和递归求解的方法,可以将给定的电话号码转换为对应的字母组合。详细的解题思路和代码示例可以帮助读者更好地理解和实现该题目。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • IhaveconfiguredanactionforaremotenotificationwhenitarrivestomyiOsapp.Iwanttwodiff ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • t-io 2.0.0发布-法网天眼第一版的回顾和更新说明
    本文回顾了t-io 1.x版本的工程结构和性能数据,并介绍了t-io在码云上的成绩和用户反馈。同时,还提到了@openSeLi同学发布的t-io 30W长连接并发压力测试报告。最后,详细介绍了t-io 2.0.0版本的更新内容,包括更简洁的使用方式和内置的httpsession功能。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
author-avatar
神奇伟哥
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有