MongoDB複製集原理

版權聲明:本文由孔德雨原創文章,轉載請註明出處: 
文章原文連接:https://www.qcloud.com/community/article/136mysql

來源:騰雲閣 https://www.qcloud.com/communitysql

 

MongoDB的單實例模式下,一個mongod進程爲一個實例,一個實例中包含若干db,每一個db包含若干張表。
MongoDB經過一張特殊的表local.oplog.rs存儲oplog,該表的特色是:固定大小,滿了會刪除最舊記錄插入新記錄,並且只支持append操做,所以能夠理解爲一個持久化的ring-buffer。oplog是MongoDB複製集的核心功能點。
MongoDB複製集是指MongoDB實例經過複製並應用其餘實例的oplog達到數據冗餘的技術。mongodb

經常使用的複製集構成通常有下圖兩種方式 (注意,可使用mongoshell 手工指定複製源,但mongdb不保證這個指定是持久的,下文會講到在某些狀況下,MongoDB會自動進行復制源切換)。
shell

MongoDB的複製集技術並很多見,很相似mysql的異步複製模式,這種模式主要有幾個技術點:數據庫

  1. 新節點加入,正常同步前的初始化網絡

  2. Primary節點掛掉後,剩餘的Secondary節點如何提供服務併發

  3. 如何保證主節點掛掉後數據不丟失/主節點掛掉後丟失數據的處理

MongoDB做爲一個成熟的數據庫產品,較好的解決了上述問題,一個完整的複製集包含以下幾點功能:app

  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<MultiApplier::OperationPtrs>* 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 順序必須徹底一致,所以無論一、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]秒內的隨機時間,以後再次嘗試選舉。

相關文章
相關標籤/搜索