版權聲明:本文由孔德雨原創文章,轉載請註明出處:
文章原文連接: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的異步複製模式,這種模式主要有幾個技術點:數據庫
新節點加入,正常同步前的初始化網絡
Primary節點掛掉後,剩餘的Secondary節點如何提供服務併發
MongoDB做爲一個成熟的數據庫產品,較好的解決了上述問題,一個完整的複製集包含以下幾點功能:app
數據同步
1.1 initial-sync
1.2steady-sync
1.3異常數據回滾運維
MongoDB集羣心跳與選舉異步
當一個節點剛加入集羣時,它須要初始化數據使得 自身與集羣中其它節點的數據量差距儘可能少,這個過程稱爲initial-sync。
一個initial-sync 包括六步(閱讀rs_initialSync.cpp:_initialSync
函數的邏輯)
第2)步要拷貝全部數據,所以通常第2)步消耗時間最長,第3)與第4)步是一個連續逼近的過程,MongoDB這裏作了兩次
是由於第2)步通常耗時太長,致使第3)步數據量變多,間接受到影響。然而這麼作並非必須的,rs_initialSync.cpp:384 開始的TODO建議使用SyncTail的方式將數據一次性讀回來(SyncTail以及TailableCursor的行爲與原理若是不熟悉請看官方文檔
當節點初始化完成後,會進入steady-sync狀態,顧名思義,正常狀況下,這是一個穩定靜默運行於後臺的,從複製源不斷同步新oplog的過程。該過程通常會出現這兩種問題:
這兩種狀況在bgsync.cpp:_produce
函數中,雖然這兩種狀況很不同,可是最終都會進入 bgsync.cpp:_rollback
函數處理,
對於第二種狀況,處理過程在rs_rollback.cpp
中,具體步驟爲:
維持本地與遠程的兩個反向遊標,以線性的時間複雜度找到LCA(最近公共祖先,上conflict.png中爲Record4)
該過程與經典的兩個有序鏈表找公共節點的過程相似,具體實如今roll_back_local_operations.cpp:syncRollBackLocalOperations中,讀者能夠自行思考這一過程如何以線性時間複雜度實現。
針對本地每一個衝突的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
針對2)中分析出的每條oplog的處理方式,執行處理,相關函數爲 rs_rollback.cpp:syncFixUp,此處操做主要是對步驟2)的實踐,實際處理過程至關繁瑣。
與steady-sync相關的代碼有 bgsync.cpp, sync_tail.cpp。上面咱們介紹過,steady-sync過程從複製源讀取新產生的oplog,並應用到本地,這樣的過程脫不離是一個producer-consumer模型。因爲oplog須要保證順序性,producer只能單線程實現。
對於consumer端,是否有併發提速機制呢?
首先,不相干的文檔之間無需保證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 }
其次對於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 }
從庫和主庫的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的主節點選舉由心跳觸發。一個複製集N個節點中的任意兩個節點維持心跳,每一個節點維護其餘N-1個節點的狀態(該狀態僅是該節點的POV,好比由於網絡分區,在同一時刻A觀察C處於down狀態,B觀察C處於seconary狀態)
以任意一個節點的POV,在每一次心跳後會企圖將主節點降級(step down primary)(topology_coordinator_impl.cpp:_updatePrimaryFromHBData),主節點降級的理由以下:
選舉主節點
Secondary節點檢測到當前集羣沒有存活的主節點,則嘗試將自身選舉爲Primary。主節點選舉是一個二階段過程+多數派協議。
以自身POV,檢測自身是否有被選舉的資格:
同僚仲裁
選舉第一階段中,某節點收到其餘節點的選舉請求後,會執行更嚴格的同僚仲裁
發起者向集羣中存活節點發送Elect請求,仲裁者收到請求的節點會執行一系列合法性檢查,若是檢查經過,則仲裁者給發起者投一票,並得到30秒鐘「選舉鎖」,選舉鎖的做用是:在持有鎖的時間內不得給其餘發起者投票。 發起者若是或者超過半數的投票,則選舉經過,自身成爲Primary節點。得到低於半數選票的緣由,除了常見的網絡問題外,相同優先級的節點同時經過第一階段的同僚仲裁併進入第二階段也是一個緣由。所以,當選票不足時,會sleep[0,1]秒內的隨機時間,以後再次嘗試選舉。