1. 程式人生 > 實用技巧 >MongoDB 事務,複製和分片的關係

MongoDB 事務,複製和分片的關係

摘要:本文嘗試對Mongo的複製和分散式事務的原理進行描述,在必要的地方,對實現的正確性進行論證,希望能為MongoDB核心愛好者提供一些參考。

1.前言

  • MongoDB基於wiredTiger提供的泛化SI的功能,重構了readHistory(readMajority)的能力
  • 基於wiredTiger提供的AllCommittedTimestamp API,重構了字首一致的主從複製(Prefix-Consistent-Replication)
  • 引入混合邏輯時鐘(HLC),每個節點(Mongos/Mongod)的邏輯時鐘維持在接近的值,基於此實現ChangeStream, 結合HLC與CLOCK-SI,實現分散式事務,HLC和泛化SI,CLOCK-SI兩篇Paper可以作為理解MongoDB的設計的理論參考(這裡並沒有說MongoDB是Paper的實現)。

本文嘗試對Mongo的複製和分散式事務的原理進行描述,在必要的地方,對實現的正確性進行論證,希望能為MongoDB核心愛好者提供一些參考。

2.MongoDB副本集事務介紹

  • MongoDB 副本集的事務
    • MongoDB副本集的複製是基於raft協議,相比於Paxos,raft協議實現簡單,但是raft協議只支援single-master,對應的,MongoDB的副本集是主從架構,而且只有主節點支援寫入操作。MongoDB副本集的事務管理,包括衝突檢測,事務提交等關鍵操作,都只在主節點上完成。也就是說副本集的事務在事務管理方面,跟單節點邏輯基本一致。
    • MongoDB的事務,仍然是實現了 ACID 四個特性, MongoDB使用 SI 作為事務的隔離級別。

3.SI的簡介

  • SI,即SnapshotIsolation,中文稱為快照隔離,是一種mvcc的實現機制,它在1995年的A Critique of ANSI SQL Isolation Levels中被正式提出。因快照時間點的選取上的不同,又分為Conventional Si 和 Generalized SI。

CSI(Convensional SI)

  • CSI 選取當前最新的系統快照作為事務的讀取快照
    • 就是在事務開始的時候,獲得當前db最新的snapshot,作為事務的讀取的snapshot,
    • snapshot(Ti) = start(Ti)
    • 可以減少寫事務衝突發生的概率,並且提供讀事務讀取最新資料的能力
    • 一般我們說一個數據庫支援SI隔離級別,其實預設是說支援CSI。比如RocksDB支援的SI就是CSI,WiredTiger在3.0版本之前支援的SI也是CSI。

GSI(Generalized SI)

  • GSI選擇歷史上的資料庫快照作為事務的讀取快照,因此CSI可以看作GSI的一個特例。
  • 在複製集的情況下,考慮 CSI, 對於主節點上的事務,每次事務的開始時間選取的系統最新的快照, 但是對於其他從節點來說, 並沒有 統一的 “最新的” 快照這個概念。泛化的快照實際上是基於快照觀測得到的,對於當前事務來說,我們通過選取合適的 更早時間的快照,可以讓 從節點上的事務正確且無延遲的執行。
  • 舉例如下:
    • 例如當前資料庫的狀態是, S={T1, T2, T3}, 現在要開始執行T4,
    • 如果我們知道T4要修改的值,在T3上沒有被修改, 那麼我們在執行T4的時候, 就可以按照 T2 commit後的snashot進行讀取。

  • 如何選擇更早的時間點,需要滿足下面的規則,
  • 符號定義
  • Ti: 事務i
  • Xi: 被事務i修改過的X變數
  • snapshot(Ti): 事務i的選取的快照時間
  • start(Ti): 事務i的開始時間
  • commit(Ti): 事務i的提交時間
  • abort(Ti): the time when Ti is aborted.
  • end(Ti): the time when Ti is committed or aborted.

公式解釋

讀規則

    • G1.1, 如果變數X被本事務修改了值且讀取到了新的值, 那麼 讀操作一定在寫操作後面;
    • G1.2, 如果事務i讀取了事務j更新的變數的X, 那麼一定不會有事務i更新X的操作,在事務i讀取了事務j更新的變數的X這個操作前面;
    • G1.3, 事務j的提交時間早於事務i的快照時間;
    • G1.4, 對於任意一個會更新變數X的事務k, 那麼這個事務k一定滿足, 要麼事務k的提交時間小於事務j, 要麼這個事務k的提交時間大於事務i。

寫規則

    • G2, 對於任意已經在提交歷史裡的兩個事務,Ci, Cj, 那麼一定可以保證當 事務j的commit時間戳在 事務i的觀測時間段內時(snapshot(Ti), commit(Ti)), 那麼他們更新的變數交集一定為空。

PCSI(PREFIX-CONSISTENT SNAPSHOT ISOLATION SI)

    • GSI 只是定義了一個範圍的range,都可以作為SI使用,並沒有定義具體應該選擇哪個SI。
    • PCSI 是為了複製集而設計的。對於一個事務Ti 要開S節點開始執行, 那麼 S節點將必須包含這個事務所需要的所有前置事務都必須執行且提交。
    • 相比較於GSI, PCSI的讀規則,額外增加了 P1.5 規則。
  • SI的提交時間戳設定,依據 A Critique of ANSI SQL Isolation Levels 中的描述, 提交時間戳的設定應該是單調遞增的。新設定的時間戳,應該大於系統中已經存在的開始時間戳和提交時間戳。
  • SI 讀取時間戳的設定,必須保證比當前系統中正在執行的事務的最小的提交時間戳還要小, 因為一旦大於當前系統中正在執行事務的最小的提交時間戳,那麼這個讀事務讀取到的資料就是未定義的, 取決於讀事務啟動的時間,而不是snapshot的時間,這違背了 一致性的要求。舉例如下
    • 當前已經完成的事務是T1,正在執行的事務是T2, 將要執行的讀事務是T3, 如果 T3的讀時間戳大於T2事務提交時間戳, 並且T2事務正在執行,等到T2事務執行完後。我們觀察這個 database,就會發現 他違背了GSI,

事務執行順序如下所示是:

T1 commited and commitTs(1) -> T2 start -> T2 set commitTs(2) -> T3 start -> T3 set snapshotTs(3) -> T3 commit -> pointA -> T2 commit -> pointB

那麼可知, T3事務實際讀取的值是 T1事務的值。但根據 pointB 點來看 GSI的讀規則 1.4 的要求,會發現, 如果T3讀到T1的事務的修改,那麼必然要求, T3和T1之間沒有空洞。但實際上 T2 是落在了 T3和T1之間的, 也就是說, 違反了 GSI 1.4的讀規則。

    • 所以我們必須規定,SI 讀取時間戳的設定,必須保證比當前系統中正在執行的事務的最小的提交時間戳還要小。

4.MongoDB副本集時間戳應用

MongoDB 4.0的複製也是利用時間戳特性解決了3.x系列MongoDB從節點複製造成從節點效能下降的關鍵方案。

  • MongoDB oplog 亂序問題
    • MongoDB主備節點的資料同步並不基於WiredTiger的wal日誌來做的。相反,mongodb會將每次操作的資料變更寫入到一個叫做oplog的集合裡。
    • oplog這個集合,雖然名字帶有log,但實際上,它是一個MongoDB的表, 對oplog的寫入,並不是 append的方式修改的, 而是呈現出一種尾部亂序的方式。
    • 對於oplog來說, oplog的讀取順序是按照TS欄位來排序的, 跟上層的提交順序無關。所以存在後開始的事務,在oplog先讀取的場景。

  • oplog 空洞
    • 因為出現了亂序,所以從節點在讀取oplog的時候,就會在某些時間點出現空洞。舉例如下:
    • 時間點1: oplog 順序為: Ta -> Tb, 此時系統中還有一個事務Tc在執行
    • 時間點2: oplog 順序為: Ta -> Tc -> Tb, 當Tc執行結束後, 因為ts的順序, 看起來是將Tc插入到了Ta和Tb之間。
    • 那麼當 從節點 在時間點1 reply 到 Tb的時候, 實際上是漏了 Tc的,這個就是oplog的空洞, 他產生的原因是因為,從節點如果每次讀取oplog最新的資料,就有可能會得到一個不連續的資料, 例如 時間點1上 Ta-> Tb. 這就是oplog空洞。
    • 在具體複製邏輯中,我們必須想辦法來從節點讀取到含有空洞的oplog資料。這也是GSI的要求, snapshot的選取不能含有空洞。
    • 因為 oplog的Ts是mongo上層給的,我們很容易知道哪些事務有哪些ts, 我們再將這個ts 作為事務的commitTs 放到 oplog儲存的事務裡, 這樣我們讀取 oplog的順序事務的可見性順序相一致了,在這種情況下,我們就可以 根據 活躍事務列表, 就可以將oplog 分為兩個部分,
    • 假設活躍commitTs列表的事務是 {T10, T11, T12}, 活躍事務列表是 {T10, T11, T12, T13, T14}, 那麼意味著, 目前有 T10, T11, T12, T13, T14 再執行,並且 T10, T11, T12 已經設定了 commitTs, 又因為 上面討論的 commitTs 是單調遞增的, 那麼我們可知, T13, T14 的commitTs 一定大於 maxCommitTs(T10, T11, T12), 而且我們還可知, minCommitTs(T10,T11,T12) 就是全域性最小的 commitTs, 而小於這些的 commitTs的事務,因為不在 活躍事務列表裡了, 表示已經提交了, 那麼我們可以知道, oplog ts 在 全域性最小的 commitTs 之前的, 就是都提交了的, oplog 按照 commitTs 排序後,如下所示

… Tx | minCommitTs(T10,T11,T12) | …

我們可以知道 T9, 或者說小於 minCommitTs(T10,T11,T12) 都是無空洞,因為系統不會再提交小於 minCommitTs(T10,T11,T12) 的事務到oplog裡了, 所以從節點可以直接恢復這裡的資料。

    • 上面說的oplog minCommitTs(T10,T11,T12) 在 mongodb裡,就是特殊的timestamp, 這個後文會講。
    • 通過上面的方案,我們可以解決空洞的問題。這個時候,從節點每次恢復資料的時候,將讀取的snapshot,設定為上一次恢復的Ts(同樣也是無空洞的Ts), 這樣的話, 從節點的恢復資料和讀取資料也就做到了互不衝突。從而解決了 3.x系列的 從節點同步資料造成節點效能下降的問題。

點選關注,第一時間瞭解華為雲新鮮技術~