事務前沿研究丨確定性事務

作者:童牧

緒論

在基於 Percolator 提交協議的分佈式數據庫被提出的時期,學術研究上還出現了一種叫確定性數據庫的技術,在這項技術的發展過程中也出現了各種流派。本文將講解學術上不同的確定性事務和特點,並綜合說說他們的優點和麪臨的問題。

本文將按照提出的順序進行講解:

  • 確定性數據庫的定義;

  • 可擴展的確定性數據庫 Calvin;

  • 基於依賴分析的確定性數據庫 BOHM & PWV;

  • 重視實踐的確定性數據庫 Aria。

確定性數據庫的定義

確定性數據庫的確定性指的是執行結果的確定性,一言蔽之,給定一個事務輸入集合,數據庫執行後能有唯一的結果。

圖 1 - 不存在偏序關係時的不確定性

但是這一確定性是需要基於偏序關係的,偏序關係代表的是事務在數據庫系統中執行的先後順序。圖 1 中,兩個事務併發執行,但是還沒有被確認偏序關係,那麼這兩個事務的執行先後順序還沒有被確定,因此這兩個事務的執行順序也是自由的,而不同的執行順序則會帶來不同的結果。

圖 2 - 使用事務管理器爲排序輸入事務排序

圖 1 中的例子說明,爲了達到確定性的結果,在事務執行前,我們就需要對其進行排序,圖 2 中加入了一個事務管理器,在事務被執行前,會從事務管理器之中申請一個 id,全局的事務執行可以看作是按照 id 順序進行的,圖中 T2 讀到了 T1 的寫入結果,T3 讀到了 T2 的寫入結果,因此這三個事務必須按照 T1 -> T2 -> T3 的順序執行才能產生正確的結果。

圖 3 - 死鎖的產生

確定性數據庫的另一個優點在於能夠規避死鎖的產生,死鎖的產生原因是交互式的事務有中間過程,圖 3 是對產生過程的解釋,T1 和 T2 各寫入一個 Key,隨後在 T1 嘗試寫入 T2 的 Key,T2 嘗試寫入 T1 的 Key 時就產生了死鎖,需要 abort 其中的一個事務。設想一下如果事務的輸入是完整的,那麼數據庫在事務開始的時候就知道事務會做哪些操作,在上面的例子中,也就能夠在 T2 輸入時知道 T2 和 T1 會產生寫依賴的關係,需要等待 T1 執行完畢後再執行 T2,那麼就不會在執行過程中才能夠發現死鎖的情況了。發生死鎖時會 abort 哪個事務是沒有要求的,因此在一些數據庫中,這一點也可能產生不確定性的結果。

數據庫系統中的不確定性還可能來源於多個方面,比如網絡錯誤、io 錯誤等無法預料的情況,這些情況往往會表現爲某些事務執行失敗,在對確定性數據庫的解讀中,我們會討論如果避免因這些不可預料的情況而產生不確定的結果。

確定性是一個約束非常強的協議,一旦事務的先後順序被確定,結果就被確定了,基於這一特點,確定性數據庫能夠優化副本複製協議所帶來的開銷。因爲能保證寫入成功,在有些實現中還能夠預測讀的結果。但是確定性並不是銀彈,強大的協議也有着其對應的代價,本文會在具體案例中詳細分析其缺陷,以及確定性數據庫所面臨的困難。

可擴展的確定性數據庫 Calvin

Calvin 提出於 2012 年,和 Spanner 出現於同一時期,嘗試利用確定性數據庫的特點解決解決當時數據庫的擴展性問題,這一研究成果後續演變成爲了 FaunaDB,一個商業數據庫。

圖 4 - Calvin 的架構圖

圖 4 是 Calvin 數據庫的架構圖,雖然比較複雜,但是我們主要需要解決的問題有兩個:

  • 在一個 replica 當中,是如何保證確定性的?

  • 在 replica 之間,是如何保證一致性的?

首先看第一個問題,在一個 replica 中,節點是按照 partition 分佈的,每個節點內部可以分爲 sequencer,scheduler 和 storage 三個部分:

  • Sequencer 負責副本複製,並在每 10ms 打包所收到的事務,發送到相應的 scheduler 之上;

  • Scheduler 負責執行事務並且確保確定性的結果;

  • Storage 是一個單機的存儲數據庫,只需要支持 KV 的 CRUD 接口即可。

圖 5 - Calvin 執行過程一

我們以一系列的事務輸入來進行說明,假設有三個 sequencer,他們都接收到了一些事務,每 10ms 將事務打包成 batch。但是在第一個 10ms 中,可以看到 sequencer1 中的 T1 與 sequencer3 中的 T10 產生了衝突,根據確定性協議的要求,這兩個事務的執行順序需要是 T1 -> T10。這些事務 batch 會在被髮送到 scheduler 之前通過 Paxos 算法進行復制,關於副本複製的問題我們之後再說。

圖 6 - Calvin 執行過程二

圖 6 中,這些 batch 被髮送到對應的 scheduler 之上,因爲 T1 的 id 比 T10 更小,說明它應該被更早的執行。Calvin 會在 scheduler 上進行鎖的分配,一旦這個 batch 的鎖分配結束了,持有鎖的事務就可以執行,而在我們的例子中,鎖的分配可能有兩種情況,T1 嘗試獲取被 T10 佔有的 x 的鎖並搶佔,或是 T10 嘗試獲取被 T1 佔有的鎖並失敗,不論哪種情況,都會是 T1 先執行而 T10 後執行的結果。

圖 7 - Calvin 的不確定性問題

思考 Calvin 的執行過程,就會懷疑是否會發生圖 7 這樣的問題,如果 T1 在 T10 完全執行之後才被髮送到 scheduler 上,那 T1 和 T10 的執行順序還是會產生不確定性。爲了解決這個問題,Calvin 有一個全局的 coordinator 角色,負責協調所有節點的工作階段,在集羣中有節點還未完成發送 batch 到 scheduler 的階段時,所有節點不會進入下一階段的執行。

在 SQL 層面,有些 predicate 語句的讀寫集合在被執行前是沒有被確定的,這種情況下 Calvin 無法對事務進行分析,比如 scheduler 不知道要向哪些節點發送讀寫請求,也不知道如何進行上鎖。Calvin 通過 OLLP 的策略來解決這一問題,OLLP 下,Calvin 會在事務進入 sequencer 的階段發送一個試探性讀來確定讀寫集合,如果這個預先讀取到的讀寫集合在執行過程中發生了變化,則事務必須被重啓。

我們考慮 Calvin 的一個問題,如何在 replica 之間保證一致性。在確定性協議的下,只需要保證一致的輸入,就可以在多個副本之間保證執行結果的一致。其中一致的輸入包括了輸入的順序。

圖 8 - Calvin 的不一致性問題

圖 8 描述了 Calvin 中的不一致性問題,如果 T2 先於 T1 被同步到一個副本中,並且被執行了,那麼副本間的一致性就遭到了破壞。爲了解決這個問題,所有的副本同步都需要在一個 Paxos 組之內進行以保證全局的順序性,這可能成爲一個瓶頸,但是 Calvin 聲稱能達到每秒 500,000 個事務的同步效率。

綜合來看,Calvin 和 Spanner 出現於同一年代,Calvin 嘗試通過確定性協議來實現可擴展的分佈式數據庫並且也取得了不錯的成果。本文認爲 Calvin 中存在的問題有兩點:

  • 全局的共識算法可能成爲瓶頸或者單點;

  • 使用 Coordinator 來協調節點工作階段會因爲一個節點的問題影響全局。

基於依賴分析的確定性數據庫 BOHM & PWV

在開始說 BOHM 和 PWV 之前,我們先來回顧以下依賴分析。Adya 博士通過依賴分析(寫後寫、寫後讀和讀後寫)來定義事務間的先後關係,通過依賴圖中是否出現環來判斷事務的執行是否破壞隔離性。這一思路也可以反過來被數據庫內核所使用,只要在執行的過程中避免依賴圖中的環,那麼執行的過程就是滿足事先給定的隔離級別的要求的,從這個思路出發,可以讓原本無法併發執行的事務併發執行。

例 1 - 無法並行的併發事務

例 1 中給出了一個無法併發執行的併發事務的例子,其中 T1 和 T3 都對 x 有寫入,並且 T2 需要讀取到 T1 寫入的 x=1,在通常的數據庫系統中,這三個事務需要按照 T1 -> T2 -> T3 的順序執行,降低了併發度。

圖 9 - BOHM 的 MVCC

BOHM 通過對 MVCC 進行了一定的改造來解決這個問題,設置了每條數據的有效期和指向上一個版本的指針,圖中 T100 數據的有效期是 100 <= id < 200,而 T200 數據的有效期是 200 <= id。MVCC 爲寫衝突的事務併發提供了可能,加上確定性事務知道事務的完整狀態,BOHM 實現了寫事務的併發。

PWV 是在 BOHM 之上進行的讀可見性優化,讓寫入事務能夠更早的(在完整提交之前)就被讀取到,爲了實現這一目標,PWV 對事務的可見性方式和 abort 原因進行了分析。

事務的可見性有兩種:

  • 提交可見性(Committed write visibility),BOHM 使用的策略,延遲高;

  • 投機可見性(Speculative write visibility),存在連鎖 abort 的風險。

圖 10 - 連鎖 abort

圖 10 是投機可見性的連鎖 abort 現象,T1 寫入的 x 被 T2 讀取到,T2 的寫入進一步的被 T3 讀取到,之後 T1 在 y 上的寫入發現違反了約束(value < 10),因此 T1 必須 abort。但是根據事務的原子性規則,T1 對 x 的寫入也需要回滾,因此讀取了 x 的 T2 需要跟着 abort,而讀取到 T2 的 T3 也需要跟着 T2 被 abort。

在數據庫系統中有兩種 abort 的原因:

  • 邏輯原因(Logic-induced abort),違反了約束;

  • 系統原因(System-induced abort),產生了死鎖、系統錯誤或寫衝突等情況。

但是非常幸運的,確定性數據庫能夠排除因系統原因產生的 abort,那麼只要確保邏輯原因的 abort 不發生,一個事務就一定能夠在確定性數據庫中成功提交。

圖 11 - 利用 piece 分割事務的實現

圖 11 是 PWV 對事務的分割,將事務分割成以 piece 的小單元,然後尋找其中的 Commit Point,在 Commit Point 之後則沒有可能發生邏輯原因 abort 的可能。圖中 T2 需要讀取 T1 的寫入結果,只需要等待 T1 執行到 Commit Point 之後在進行讀取,而不需要等待 T1 完全執行成功。

圖 12 - PWV 的性能

通過對事務執行過程的進一步細分,PWV 降低了讀操作的延遲,相比於 BOHM 進一步提升了併發度。圖 12 中 RC 是 BOHM 不提前讀取的策略,從性能測試結果能夠看出 PWV 在高併發下有着非常高的收益。

BOHM 和 PWV 通過對事務間依賴的分析來獲取衝突場景下的高性能,但是這一做法需要知道全局的事務信息,計算節點是一個無法擴展的單點。

重視實踐的確定性數據庫 Aria

最後我們來講 Aria,Aria 認爲現有的確定性數據庫存在着諸多問題。Calvin 的實現具有擴展性,但是基於依賴分析的 BOHM 和 PWV 在這方面的表現不好;而得益於依賴分析,BOHM 和 PWV 在衝突場景下防止性能回退的表現較好,但 Calvin 在這一情況下的表現不理想。

在分佈式系統中爲了併發執行而進行依賴分析是比較困難的,所以 Aria 使用了一個預約機制,完整的執行過程是:

  • 一個 sequence 層爲事務分配全局遞增的 id;

  • 將輸入的事務持久化;

  • 執行事務,將 mutation 存在執行節點的內存中;

  • 對持有這個 key 的節點進行 reservation;

  • 在 commit 階段進行衝突檢測,是否允許 commit,沒有發生衝突的事務則返回執行成功;

  • 異步的寫入數據。

圖 13 - Aria 的架構圖

圖 13 是 Aria 的架構圖,每個節點負責存儲一部分數據。Aria 的論文裏並沒有具體的規定複製協議在哪一層做,可以在 sequencer 層也可以在 storage 層實現,在 sequencer 層實現更能發揮優勢確定性數據庫的優勢,在 storage 層實現能簡化 sequencer 層的邏輯。

圖 14 - Aria 執行過程一

圖 14 中,輸入事務在經過 sequencer 層之後被分配了全局遞增的事務 id,此時執行結果就已經是確定性的了。經過 sequencer 層之後,事務被髮送到 node 上,T1 和 T2 在 node1 上,T3 和 T4 在 node2 上。

圖 15 - Aria 執行過程二

圖 15 中,假設 T1 和 T2 在 node1 上被打包成了一個 batch,T3 和 T4 在 node2 上被打包成了一個 batch。在執行時,batch 中的事務執行結果會放在所屬 node 的內存中,然後進行下一步。

圖 16 - Aria 執行過程三

圖 16 是 batch1 中的事務進行 reservation 的結果,需要注意的是執行事務的 node 不一定是擁有這個事務數據,但 reservation 的請求會發送到擁有數據的 node 上,所以 node 一定能知道和自身所存儲的 Key 相關的所有 reservation 信息。在 commit 階段,會發現在 node1 上 T2 的讀集合與 T1 的寫集合衝突了,因此 T2 需要被 abort 並且放到下一個 batch 中進行執行。對於沒有衝突的 T1,T3 和 T4,則會進入寫入的階段。因爲在 sequencer 層已經持久化了輸入結果,所以 Aria 會先向客戶端返回事務執行成功並且異步進行寫入。

圖 17 - Aria 執行過程四

圖 17 是 T2 被推遲執行的結果,T2 加入到了 batch2 之中。但是在 batch2 中,T2 享有最高的執行優先級(在 batch 中的 id 最小),不會無限的因爲衝突而被推遲執行,而且這一策略是能夠保證唯一結果的。

圖 18 - Aria 的不確定性問題

那麼容易想到的是 Aria 也可能會有如 Calvin 一樣的不確定性問題。圖 18 中,T1 和 T2 是存在衝突的,應該先執行 T1 在執行 T2,如果 T2 在 T1 尚未開始 reservation 之前就嘗試提交,那麼就不能夠發現自己與 T1 存在衝突,執行順序變爲了 T2->T1,破壞了確定性的要求。爲了解決這個問題,Aria 和 Calvin 一樣存在 coordinator 的角色,利用 coordinator 來保證所有的節點處在相同的階段,圖 18 中,在 T1 所在的 node1 完成 reservation 之前,node2 不能夠進入 commit 階段。

圖 19 - Aria 的重排序

確定性數據庫的優勢之一就是能夠根據輸入事務進行重排序,Aria 也考慮了這一機制,首先 Aria 認爲 WAR 依賴(寫後讀)是能夠安全的並行的,進一步在 commit 階段對 reservation 的結果進行衝突檢測時,可以將讀後寫依賴轉化爲寫後讀依賴。在圖 19 上方的圖中,如果按照 T1 -> T2 -> T3 的順序執行,那麼這三個事務需要串行執行。但是在經過重排後並行執行的結果中,T2 和 T3 的所讀取到的值都是 batch 開始之前的,換言之,執行順序變爲了 T3 -> T2 -> T1。而在圖 19 的下方,即使將 RAW 依賴轉化爲了 WAR 依賴,因爲依賴出現了環,依舊需要有一個事務被 abort。

相比於 Calvin,Aria 設計的優點在於執行和 reservation 的策略擁有更高的並行度,並且不需要額外的 OLLP 策略進行試探性讀,並且 Aria 能夠提供一個後備策略,在高衝突的場景下開其一個額外的衝突事務處理階段,本文不再詳細描述,感興趣的同學可以看 Luyi 老師在知乎上寫的文章

圖 20 - Aria 的 barrier 限制

圖 20 是 Aria 的 barrier 限制,具體表現爲,如果一個 batch 中存在一個事務的執行過程很慢,例如大事務,那麼這個事務會拖慢整個 batch,這是我們相當不願意看到的,尤其是在大規模的分佈式數據庫中,很容易成爲穩定性的破壞因素。 總結 確定性是一個很強的協議,但是它需要全局的事務信息來實現,對上述的確定性數據庫的總結如下。

基於依賴分析的 BOHM 和 PWV:

  • ↑ 充分利用 MVCC 的併發性能

  • ↑ 能夠防止衝突帶來的性能回退

  • ↓ 單節點擴展困難,不適合大規模數據庫

分佈式設計的 Calvin 和 Aria:

  • ↑ 單版本,存儲的數據簡單

  • ↓ 長事務、大事務可能拖慢整個集羣

  • ↓ barrier 機制需要 coordinator 進行實現,存在 overhead

  • ↓ 如果一個節點出現故障,整個集羣都將進入等待狀態

相比之下,基於 Percolator 提交協議的分佈式數據庫,只需要單調遞增的時鐘就能夠實現分佈式事務,對事務的解耦做的更加好。

圖 21 - 共識算法層次的對比

共識算法的層次是確定性數據庫一個很重要的性能提升點,圖 31 下方是我們經常接觸的在存儲引擎層面進行共識算法的做法,雖然系統對上層而言變得簡單了,但是存在着寫放大的問題。確定性協議能夠保證一樣的輸入得到唯一的輸出,因此使得共識算法能夠存在於 sequencer 層,如圖 21 上方所示,極大提升了一個副本內部的運行效率。

總結,確定性數據庫目前主要面臨的問題一是在 Calvin 和 Aria 中存在的 coordinator 角色對全局的影響,另一點是存儲過程的使用方式不夠友好;而優點則在於確定性協議是一個兩階段提交的替代方案,並且能夠使用單版本的數據來提升性能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章