本文由笔者首发于
InfoQ:《深刻浅出MongoDB复制》
MongoDB中文社区:《深刻浅出MongoDB复制》python
因为本身开了blog,因此将以前比较好的文章挪过来便于你们浏览。c++
笔者最近在生产环境中遇到许多复制相关问题,查阅网上资料发现官方文档虽然系统可是不够有深度,网上部分深度文章则直接以源码展现,不利于你们了解。因此本文则是结合前二者最终给读者以简单的方式展示MongoDB复制的整个架构。本文分为如下5个步骤:sql
本章节首先会给你们简单介绍一些MongoDB复制的一些基本概念,便于你们对后面内容的理解。mongodb
MongoDB有副本集及主从复制两种模式,今天给你们介绍的是副本集模式,由于主从模式在MongoDB 3.6也完全废弃不使用了。MongoDB副本集有Primary、Secondary、Arbiter三种角色。今天给你们介绍的是Primary与Secondary数据同步的内部原理。MongoDB副本集架构以下所示:数据库
MongoDB Oplog是MongoDB Primary和Secondary在复制创建期间和创建完成以后的复制介质,就是Primary中全部的写入操做都会记录到MongoDB Oplog中,而后从库会来主库一直拉取Oplog并应用到本身的数据库中。这里的Oplog是MongoDB local数据库的一个集合,它是Capped collection,通俗意思就是它是固定大小,循环使用的。以下图:网络
MongoDB Oplog中的内容及字段介绍:多线程
{ "ts" : Timestamp(1446011584, 2), "h" : NumberLong("1687359108795812092"), "v" : 2, "op" : "i", "ns" : "test.nosql", "o" : { "_id" : ObjectId("563062c0b085733f34ab4129"), "name" : "mongodb", "score" : "100" } } ts: 操做时间,当前timestamp + 计数器,计数器每秒都被重置 h:操做的全局惟一标识 v:oplog版本信息 op:操做类型 i:插入操做 u:更新操做 d:删除操做 c:执行命令(如createDatabase,dropDatabase) n:空操做,特殊用途 ns:操做针对的集合 o:操做内容,若是是更新操做 o2:操做查询条件,仅update操做包含该字段
MongoDB目前已经迭代了不少个版本,下图我汇总了目前市面上经常使用版本中MongoDB在复制的一些重要改进。架构
具体细节你们能够参考MongoDB官方Release Note:https://docs.mongodb.com/manu...app
MongoDB添加从库比较简单,在安装从库以后,直接在主库执行rs.add()或者replSetReconfig命令便可添加,这两个命令其实在最终都调用replSetReconfig命令执行。你们有兴趣能够去翻阅MongoDB客户端JS代码。nosql
而后咱们来看副本集加一个新从库的大体步骤,以下图,右边的Secondary是我新加的从库。
经过上图咱们能够看到一共有7个步骤,下面咱们看看每个步骤MongoDB都作了什么:
一、 主库收到添加从库命令
二、 主库更新副本集配置并与新从库创建心跳机制
三、 从库收到主库发送过来的心跳消息与主库创建心跳
四、 其余从库收到主库发来的新版本副本集配置信息并更新本身的配置
五、 其余从库与新从库创建心跳机制
六、 新从库收到其余从库心跳信息并跟其余从库创建心跳机制
七、 新加的节点将副本集配置信息更新到local.system.replset集合中,MongoDB会在一个循环中查询local.system.replset是否配置了replset 信息,一旦查到相关信息触发开启复制线程,而后判断是否须要全量复制,须要的话走全量复制,不须要走增量复制。
八、 最终同步创建完成
副本集全部节点以前都有相互的心跳机制,每2秒一次,在MongoDB 3.2版本之后咱们能够经过heartbeatIntervalMillis参数来控制心跳频率。
上述过程你们能够结合副本集节点状态来看(rs.status命令):
上面咱们知道添加一个从库的大体流程,那咱们如今来看主从数据同步的具体细节。当从库加入到副本集的时候,会判断本身是须要Initial Syc(全量同步)仍是增量同步。那是经过什么条件判断的呢?
以上三个条件有一个条件知足就须要作全量同步。
咱们能够得出在从库最开始加入到副本集的时候,只能先进行Initial Sync,下面咱们来看看Initial Sync的具体流程
这里先说明一点,MongoDB默认是采起级联复制的架构,就是默认不必定选择主库做为本身的同步源,若是不想让其进行级联复制,能够经过chainingAllowed参数来进行控制。在级联复制的状况下,你也能够经过replSetSyncFrom命令来指定你想复制的同步源。因此这里说的同步源其实相对于从库来讲就是它的主库。那么同步源的选取流程是怎样的呢?
MongoDB从库会在副本集其余节点经过如下条件筛选符合本身的同步源。
经过上述筛选最后过滤出来的节点做为新的同步源。
其实MongoDB同步源在除了在Initial Sync和增量复制 的时候选定以后呢,并非一直是稳定的,它可能在如下状况下进行变动同步源:
这里就到了Initial Sync的核心逻辑了,我下面以图和步骤的方式给你们展示MongoDB在作Initial Sync的具体流程。
注:本图是针对于MongoDB 3.4以前的版本
同步流程以下:
* 0. Add _initialSyncFlag to minValid collection to tell us to restart initial sync if we crash in the middle of this procedure * 1. Record start time.(记录当前主库最近一次oplog time) * 2. Clone. * 3. Set minValid1 to sync target's latest op time. * 4. Apply ops from start to minValid1, fetching missing docs as needed.(Apply Oplog 1) * 5. Set minValid2 to sync target's latest op time. * 6. Apply ops from minValid1 to minValid2.(Apply Oplog 2) * 7. Build indexes. * 8. Set minValid3 to sync target's latest op time. * 9. Apply ops from minValid2 to minValid3.(Apply Oplog 3) 10. Cleanup minValid collection: remove _initialSyncFlag field, set ts to minValid3 OpTime
注:以上步骤直接copy的MongoDB源码中的注释。
以上步骤在Mongo 3.4 Initial Sync 有以下改进:
上述4个新增特性提高了Initial Sync的效率而且提升了Initial Sync的可靠性,因此你们使用MongoDB最好使用最新版本MongoDB 3.4或者3.6,MongoDB 3.6 更是有一些使人兴奋的特性,这里就不在此叙述了。
全量同步完成以后,而后MongoDB会进入到增量同步的流程。
上面咱们介绍了Initial Sync,就是已经把同步源的存量数据拿过来了,那主库后续写入的数据怎么同步过来呢?下面仍是以图跟具体的步骤来给你们介绍:
注:这里不必定是Primary,刚刚提到了同步源也多是Secondary,这里采用Primary主要方便你们理解。
咱们能够看到上述有6个步骤,那每一个步骤具体作的事情以下:
一、 Sencondary 初始化同步完成以后,开始增量复制,经过produce线程在Primary oplog.rs集合上创建cursor,而且实时请求获取数据。
二、 Primary 返回oplog 数据给Secondary。
三、 Sencondary 读取到Primary 发送过来的oplog,将其写入到队列中。
四、 Sencondary 的同步线程会经过tryPopAndWaitForMore方法一直消费队列,当每次达到必定的条件以后,条件以下:
上述两个条件知足一个以后,就会将数据给prefetchOps方法处理,prefetchOps方法主要将数据以database级别切分,便于后面多线程写入到数据库中。若是采用的WiredTiger引擎,那这里是以Docment ID 进行切分。
五、 最终将划分好的数据以多线程的方式批量写入到数据库中(在从库批量写入数据的时候MongoDB会阻塞全部的读)。
六、 而后再将Queue中的Oplog数据写入到Sencondary中的oplog.rs集合中。
上面咱们介绍MongoDB复制的数据同步,咱们知道除了数据同步,复制还有一个重要的地方就是高可用,通常的数据库是须要咱们本身去定制方案或者采用第三方的开源方案。MongoDB则是本身在内部已经实现了高可用方案。下面我就给你们详细介绍一下MongoDB的高可用。
首先咱们看那些状况会触发MongoDB执行主从切换。
一、 新初始化一套副本集
二、 从库不能链接到主库(默认超过10s,可经过heartbeatTimeoutSecs参数控制),从库发起选举
三、 主库主动放弃primary 角色
修改如下配置的时候:
四、 移除从库的时候(在MongoDB 2.6会触发,MongoDB 3.4不会,其余版本待肯定)
经过上面触发切换的场景,咱们了解到MongoDB的心跳信息是MongoDB判断对方是否存活的重要条件,当达到必定的条件时,MongoDB主库或者从库就会触发切换。下面我给你们详细介绍一下心跳机制
咱们知道MongoDB副本集全部节点都是相互保持心跳的,而后心跳频率默认是2秒一次,也能够经过heartbeatIntervalMillis来进行控制。在新节点加入进来的时候,副本集中全部的节点须要与新节点创建心跳,那心跳信息具体是什么内容呢?
心跳信息内容:
BSONObjBuilder cmdBuilder; cmdBuilder.append("replSetHeartbeat", setName); cmdBuilder.append("v", myCfgVersion); cmdBuilder.append("pv", 1); cmdBuilder.append("checkEmpty", checkEmpty); cmdBuilder.append("from", from); if (me > -1) { cmdBuilder.append("fromId", me); }
注:上述代码摘抄MongoDB 源码中构建心跳信息片断。
具体在MongoDB日志中表现以下:
command admin.$cmd command: replSetHeartbeat { replSetHeartbeat: "shard1", v: 21, pv: 1, checkEmpty: false, from: "10.13.32.244:40011", fromId: 3 } ntoreturn:1 keyUpdates:0
那副本集全部节点默认都是每2秒给其余剩余的节点发送上述信息,在其余节点收到信息后会调用ReplSetCommand命令来处理心跳信息,处理完成会返回以下信息:
result.append("set", theReplSet->name()); MemberState currentState = theReplSet->state(); result.append("state", currentState.s); // 当前节点状态 if (currentState == MemberState::RS_PRIMARY) { result.appendDate("electionTime", theReplSet->getElectionTime().asDate()); } result.append("e", theReplSet->iAmElectable()); //是否能够参与选举 result.append("hbmsg", theReplSet->hbmsg()); result.append("time", (long long) time(0)); result.appendDate("opTime", theReplSet->lastOpTimeWritten.asDate()); const Member *syncTarget = replset::BackgroundSync::get()->getSyncTarget(); if (syncTarget) { result.append("syncingTo", syncTarget->fullName()); } int v = theReplSet->config().version; result.append("v", v); if( v > cmdObj["v"].Int() ) result << "config" <config().asBson();
注:以上信息是正常状况下返回的,还有一些不正常的处理场景,这里就不一一细说了。
前面咱们了解了触发切换的场景以及MongoDB副本集节点以前的心跳机制。下面咱们来看切换的具体流程:
一、从库没法链接到主库,或者主库放弃Primary角色。
二、从库会根据心跳消息获取当前该节点的角色并与以前进行对比
三、若是角色发生改变就开始执行msgCheckNewState方法
四、在msgCheckNewState 方法中最终调用electSelf 方法(会有一些判断来决定是否最终调用electSelf方法)
五、electSelf 方法最终向副本集其余节点发送replSetElect命令来请求投票。
命令以下:
BSONObj electCmd = BSON( "replSetElect" << 1 << "set" << rs.name() << "who" << me.fullName() << "whoid" << me.hbinfo().id() << "cfgver" <version << "round" << OID::gen() /* this is just for diagnostics */ );
具体日志表现以下:
2017-12-14T10:13:26.917+0800 [conn27669] run command admin.$cmd { replSetElect: 1, set: "shard1", who: "10.13.32.244:40015", whoid: 4, cfgver: 27, round: ObjectId('5a31de4601fbde95ae38b4d2') }
六、其余副本集收到replSetElect会对比cfgver信息,会确认发送该命令的节点是否在副本集中,确认该节点的优先级是不是该副本集全部节点中优先级最大的。最后知足条件才会给该节点发送投票信息。
七、发起投票的节点最后会统计所得票数大于副本集可参与投票数量的一半,则抢占成功,成为新的Primary。
八、其余从库若是发现本身的同步源角色发生变化,则会触发从新选取同步源。
咱们知道在发生切换的时候是有可能形成数据丢失的,主要是由于主库宕机,可是新写入的数据尚未来得及同步到从库中,这个时候就会发生数据丢失的状况。
那针对这种状况,MongoDB增长了回滚的机制。在主库恢复后从新加入到复制集中,这个时候老主库会与同步源对比oplog信息,这时候分为如下两种状况:
一、 在同步源中没有找到比老主库新的oplog信息。
二、 同步源最新一条oplog信息跟老主库的optime和oplog的hash内容不一样。
针对上述两种状况MongoDB会进行回滚,回滚的过程就是逆向对比oplog的信息,直到在老主库和同步源中找到对应的oplog,而后将这期间的oplog所有记录到rollback目录里的文件中,若是可是出现如下状况会终止回滚:
上述咱们已经知道了MongoDB的回滚原理,可是咱们在生产环境中怎么避免回滚操做呢,由于毕竟回滚操做很麻烦,并且针对有时序性的业务逻辑也是不可接受的。那MongoDB也提供了对应的方案,就是WriteConcern,这里就不细说了,有兴趣的朋友能够仔细了解。其实这也是在CAP中作出一个选择。
MongoDB复制内部原理已经给你们介绍完毕,以上其实还涉及不少细节没能一一列出。你们有兴趣能够本身去整理。这里还须要说明一点就是MongoDB版本迭代速度比较快,因此本文只针对于MongoDB 2.6 到MongoDB 3.4 版本,不过在某些版本可能会存在一些细节的变更,可是大致上的逻辑仍是没有改变。最后你们若是有什么问题,也能够与我联系。