標籤:back 插入 cto can thread ati date 閱讀 size
著作權聲明:本文由孔德雨原創文章,轉載請註明出處:
文章原文連結: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的非同步複製模式,這種模式主要有幾個技術點:
新節點加入,正常同步前的初始化
Primary節點掛掉後,剩餘的Secondary節點如何提供服務
- 如何保證主節點掛掉後資料不丟失/主節點掛掉後遺失資料的處理
MongoDB作為一個成熟的資料庫產品,較好的解決了上述問題,一個完整的複製集包含如下幾點功能:
資料同步
1.1 initial-sync
1.2steady-sync
1.3異常資料復原
MongoDB叢集心跳與選舉
一.資料同步initial_sync
當一個節點剛加入叢集時,它需要初始化資料使得 自身與叢集中其它節點的資料量差距盡量少,這個過程稱為initial-sync。
一個initial-sync 包括六步(閱讀rs_initialSync.cpp:_initialSync函數的邏輯)
- 刪除本地除local庫以外的所有db
- 選取一個源節點,將源節點中的所有db匯入到本地(注意,此處只匯入資料,不匯入索引)
- 將2)開始執行到執行結束中源產生的oplog 應用到本地
- 將3)開始執行到執行結束中源產生的oplog 應用到本地
- 從源將所有table的索引在本地重建(匯入索引)
- 將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的過程。該過程一般會出現這兩種問題:
- 複製源寫入過快(或者相對的,本地寫入速度過慢),複製源的oplog覆蓋了 本地用於同步源oplog而維持在源的遊標。
- 本節點在宕機之前是Primary,在重啟後本地oplog有和當前Primary不一致的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)的實踐,實際處理過程相當繁瑣。
- 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端,是否有並發提速機制呢?
首先,不相干的文檔之間無需保證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();584585 // For doc locking engines, include the _id of the document in the hash so we get586 // parallelism even if all writes are to a single collection. We can‘t do this for capped587 // collections because the order of inserts is a guaranteed property, unlike for normal588 // 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-time907 _networkQueue->consume(txn);908 } else {909 // This op must be processed alone, but we already had ops in the queue so we can‘t910 // include it in this batch. Since we didn‘t call consume(), we‘ll see this again next911 // time and process it alone.912 ops->pop_back();913 }
從庫和主庫的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 to518 // 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‘t520 // support document locking.521 if (!enoughToMultiThread ||522 !txn->getServiceContext()->getGlobalStorageEngine()->supportsDocLocking()) {523524 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),主節點降級的理由如下:
- 心跳檢測到有其他primary節點的優先順序高於當前主節點,則嘗試將主節點降級(stepDown) 為
Secondary, primary值的動態變更提供給了營運一個可以熱變更主節點的方式
- 本節點若是主節點,但是無法ping通叢集中超過半數的節點(majority原則),則將自身降級為Secondary
選舉主節點
Secondary節點檢測到當前叢集沒有存活的主節點,則嘗試將自身選舉為Primary。主節點選舉是一個二階段過程+多數派協議。
第一階段
以自身POV,檢測自身是否有被選舉的資格:
- 能ping通叢集的過半數節點
- priority必須大於0
- 不能是arbitor節點
如果檢測通過,向叢集中所有存活節點發送FreshnessCheck(詢問其他節點關於“我”是否有被選舉的資格)
同僚仲裁
選舉第一階段中,某節點收到其他節點的選舉請求後,會執行更嚴格的同僚仲裁
- 叢集中有其他節點的primary比發起者高
- 不能是arbitor節點
- primary必須大於0
- 以沖裁者的POV,發起者的oplog 必須是叢集存活節點中oplog最新的(可以有相等的情況,大家都是最新的)
第二階段
發起者向叢集中存活節點發送Elect請求,仲裁者收到請求的節點會執行一系列合法性檢查,如果檢查通過,則仲裁者給發起者投一票,並獲得30秒鐘“選舉鎖”,選舉鎖的作用是:在持有鎖的時間內不得給其他發起者投票。
發起者如果或者超過半數的投票,則選舉通過,自身成為Primary節點。獲得低於半數選票的原因,除了常見的網路問題外,相同優先順序的節點同時通過第一階段的同僚仲裁併進入第二階段也是一個原因。因此,當選票不足時,會sleep[0,1]秒內的隨機時間,之後再次嘗試選舉。
MongoDB複製集原理