本文由筆者首發於
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 版本,不過在某些版本可能會存在一些細節的變更,可是大致上的邏輯仍是沒有改變。最後你們若是有什麼問題,也能夠與我聯繫。