天天看點

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進行讀取。
  • 如何選擇更早的時間點,需要滿足下面的規則,
MongoDB 事務,複制和分片的關系
  • 符号定義
  • 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 規則。
MongoDB 事務,複制和分片的關系
MongoDB 事務,複制和分片的關系
MongoDB 事務,複制和分片的關系
  • 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系列的 從節點同步資料造成節點性能下降的問題。

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