兩段事務提交2PC的缺點和解決之道 - DBMS Musings
現在是時候拋棄2PC了,兩階段提交協議(2PC)已經在企業軟體系統中使用了三十多年 。它是一種非常有影響力的協議,用於確保訪問多個分割槽或分片中的資料的事務的原子性和永續性。它無處不在 - 無論是在舊的“古老的”分散式系統,資料庫系統和檔案系統(如Oracle,IBM DB2,PostgreSQL和Microsoft TxF(事務性NTFS))還是在較年輕的“千禧”系統(如MariaDB)中, TokuDB,VoltDB,Cloud Spanner,Apache Flink,Apache Kafka和Azure SQL資料庫。
如果您的系統支援跨分片/分割槽/資料庫的ACID事務,那麼它很可能會在表面下執行2PC(或其某些變體)。
在這篇文章中,我們將首先描述2PC:它是如何工作的以及它解決了什麼問題。然後,我們將展示2PC的一些主要問題以及現代系統如何試圖解決這些問題。不幸的是,這些嘗試的解決方案導致出現其他問題。最後,我將說明下一代分散式系統應該避免使用2PC,以及如何實現這一點。
2PC協議概述
2PC有很多變種,但基本協議的工作原理如下:
背景假設:事務所需的工作已經劃分為儲存該事務訪問的資料的所有分片/分割槽(banq注:JTA中是不同的XA資料來源,也就是不同資料庫為不同分片或分割槽)。我們將在每個分片中執行的工作稱為由該工作程式為該分片執行的工作(banq注:操作某個資料來源的程式稱為分片工作者)。每個分片工作者都能夠獨立於彼此開始處理指定事務的職責。2PC協議在事務處理結束時啟動,當事務準備好“提交”時,,它由某個協調器者(可能是參與該事務的工作者之一)啟動提交。
2PC的兩個階段:
- 階段1:協調者詢問每個分片工作者是否已成功完成對其事務的職責並準備提交。每個分片工作者都回答“是”或“否”。
- 階段2:協調者計算所有響應,如果每個分片工作者都回答“是”,那麼事務將提交。否則,它將中止。協調器向每個具有最終提交決策的分片工作者傳送訊息並接收確認。
此機制確保事務的原子性屬性:整個事務將要麼全部寫入系統的最終狀態中,或者全部不寫入到系統的最終狀態中。如果即使只有一個分片工作無法提交,那麼整個事務將被中止。換句話說:每個工作者對交易都有“否決權”。
它還確保了事務的永續性。每個工作者確保在第1階段響應“是”之前已將所有事務持久寫入到儲存。這使協調器可以自由地對事務做出最終決定,而無需關心工作者在投票'是'之後可能會失敗的事實。[在這篇文章中,當使用術語“持久寫入”時,我們有目的地含糊不清 - 這個術語可以指寫入本地非易失性儲存,或者將寫入複製到足夠的位置以便將其視為“耐用持久”。]
除了持久地寫入事務直接需要的寫入之外,協議本身還需要額外的寫入,在繼續寫入之前必須使其持久化。例如,一名工作者擁有否決權,直到在第一階段投票“是”為止這段時間都是這樣。在此之後,它不能改變其投票權。但如果它在投票'是'後立即崩潰怎麼辦?當它恢復時,它可能不知道它投了“是”,仍然認為它擁有否決權並繼續並中止交易。為了防止這種情況,它必須在將“是”投票發回協調器之前進行投票持久化。[除了這個例子,在標準的2PC中,還有另外兩個寫入在傳送作為協議一部分的訊息之前變得持久。]
2PC的問題
2PC存在兩個主要問題。第一個是眾所周知的,並在每個提出2PC的著名教科書中進行了討論。第二個不太知名,但仍然是一個主要問題。
第一個問題是眾所周知的,稱為“阻塞問題”。當每個工作者都投了'是'時會發生這種情況,但協調器在將最終決定的訊息傳送給至少一個工人之前自己發生了失敗(banq注:協調器本身自己崩潰了,是存在單點風險的)。
通過投票'是',每個工作者已經沒有了否決事務的權力。但是,協調器仍有絕對權力來決定交易的最終狀態,如果協調器在向至少一名工人傳送最終決定的訊息之前自己失敗了,那麼工作者們就無法再聚在一起做出決定 :他們不能中止,因為協調器可能會在失敗之前決定提交;並且他們也無法確認提交,因為協調器可能決定在失敗之前中止。從而,他們必須堵塞等待:等到協調器恢復(banq注:等領導身體恢復健康了),這樣才能讓它繼續實現最終的決定。與此同時,他們無法處理與停滯事務衝突的事務,因為該事務的寫入的最終結果尚未確定。
阻塞問題有兩類解決方法。第一類解決方法是修改核心協議以消除阻塞問題。不幸的是,這些修改降低了效能 - 通常通過新增額外的一輪通訊 - 因此很少在實踐中使用。第二類保持協議的合理性,但降低了協調器失敗型別的可能性,而不是導致阻塞程式 - 例如,通過在副本共識協議上執行2PC並確保協議的重要狀態被複制。不幸的是,這些解決方案再一次降低了效能,因為協議要求這些副本共識輪次順序發生,因此它們可能會給協議增加顯著的延遲。(banq注:TCC補償式事務是在兩個階段之間停止堵塞等待,第一個階段結束各個工作者就關閉鎖,如果在第二個階段提交確認再寫入,否則就進行補償性的回滾,這實際引入了對業務工作流程的依賴,通常比如傳送郵件等動作是無法補償)。
第二個問題是鮮為人知的問題,我稱之為“cloggage連鎖堵塞問題”。在處理事務之後發生2PC,因此必然將事務的等待時間增加等於執行協議所花費的時間。單獨的延遲增加對於許多應用程式來說已經是一個問題,但是一個潛在的更大問題是工作節點直到第二階段中途才知道事務的最終結果。在他們知道最終結果之前,他們必須為可能中止的可能性做好準備,因此他們通常會阻止衝突的交易在確定交易將提交之前取得進展。這些阻塞的事務反過來阻止其他事務執行,依此類推,直到2PC完成並且所有被阻止的事務都可以恢復。
總結我們上面討論的問題:2PC在四個方面使系統中毒:延遲 (協議的時間加上衝突事務的停頓時間),吞吐量 (因為它需要防止在協議期間執行其他衝突的事務,banq注:只能讓事務序列執行,區塊鏈其實是一個事務鏈),可擴充套件性 (更大)在系統中,事務變得多分割槽並且必須支付2PC的吞吐量和延遲成本以及可用性 (我們上面討論的阻塞問題)的可能性越大。
沒有人喜歡2PC,但幾十年來,人們都認為它是一種必要的邪惡。
是時候繼續前進了
三十多年來,我們一直堅持在分片系統中進行兩階段提交。人們已經意識到它引入的效能,可伸縮性和可用性問題,但仍然繼續,沒有明顯更好的替代方案。
事實是,如果我們只是以不同的方式構建我們的系統,那麼2PC的需求就會消失。已經有一些嘗試來實現這一目標 - 無論是在學術界(如SIGMOD 2016論文 )和行業。然而,這些嘗試通常通過首先避免多分片事務來工作,例如通過在事務之前重新分割槽資料使得它不再是多分片的。不幸的是,這種重新分割槽以其他方式降低了系統的效能。
我所要求的是我們構建分散式系統的方式的更深層次的變化。我堅持認為系統應該仍然能夠處理多分片事務 - 具有所有ACID保證和所需的內容 - 例如原子性和永續性 - 但是具有更簡單和更快的提交協議。
這一切都歸結為我們的系統中存在數十年的基本假設:交易可能隨時以任何理由中止。
即使我在相同的初始系統狀態下執行相同的事務...如果我在下午2:00執行它可能會提交,但在3:00它可能會中止。
大多數架構師認為我們需要這個假設的原因有幾個:首先,機器可能在任何時候都失敗 - 包括在事務過程中;在恢復時,通常不可能在故障之前重新建立易失性儲存器中的該事務的所有狀態。結果,在失敗之前從事務中斷的地方重新恢復似乎是不可能的。因此,系統將在發生故障時中止正在進行的所有事務。由於任何時候都可能發生故障,這意味著事務可能隨時中止。(banq注:中止其實對應CAP定理中的分割槽,發生故障意味著分割槽)
其次,大多數併發控制協議都需要能夠隨時中止事務。樂觀協議在處理事務後執行“驗證”階段。如果驗證失敗,則事務將中止。悲觀協議通常使用鎖來防止併發異常。這種鎖的使用可能導致死鎖,這可以通過中止(至少)一個死鎖事務來解決。由於可以隨時發現死鎖,因此事務需要保留隨時中止的能力。
如果仔細檢視兩階段提交協議,您將看到“中止事務”的這種任意可能性是協議中複雜性和延遲的主要來源。
工作者不能輕易地告訴對方他們是否會提交,因為他們可能在此之後(在事務提交之前)失敗並且需要在恢復期間中止此事務。因此,他們必須等到事務處理結束(當所有重要狀態變為持久時)並繼續進行必要的兩個階段:在第一階段,每個工作者公開放棄其對中止事務的控制,然後才能發生第二階段,作出最終決定並才能予以傳播。
在我看來,我們需要從工作者移除他們的否決權,重新架構這些系統,系統無法在執行期間隨時中止事務,只允許事務中的邏輯導致事務中止。如果理論上可以在給定資料庫的當前狀態的情況下提交事務,則無論發生何種型別的故障,該事務都必須提交。此外,相對於可能影響事務的最終提交/中止狀態的其他併發執行事務,不得存在競爭條件。
”消除事務的任意中止“靈活性聽起來很難。我們將很快討論如何實現這一目標。但首先讓我們觀察一下如果事務沒有中止退出事務的靈活性,提交協議會如何變化。
當事務不能隨意中止時,提交協議是什麼樣的
我們來看兩個例子:
在第一個示例中,假設儲存變數X的值的分片工作者被分配了一個事務中的某個任務:將X的值更改為42.假設(現在)沒有定義完整性約束或觸發器在X上(這可能會阻止系統將X設定為42)。在這種情況下,該工作者永遠不會被賦予能夠中止交易的權力。無論發生什麼,該工作者必須將X更改為42,如果該工作者失敗,則必須在器恢復後還是將X更改為42。由於它永遠沒有任何中止的能力,因此在提交協議期間無需對該工作者檢查它是否會提交?
在第二個示例中,假設儲存變數Y和Z的值的分片工作者被分配了兩個事務任務:從前一個Y值中減去1並將Z設定為Y的新值。此外,假設Y上存在完整性約束,表明Y永遠不會低於0(例如,如果它代表零售應用程式中專案的庫存)。因此,此工作者必須執行以下程式碼的等效程式碼:
IF(Y> 0) 從Y減去1 <b>else</b> 中止交易 Z = Y.
必須賦予此工作者中止事務的權力,因為應用程式的邏輯需要這樣做。但是,這種力量是有限的。只有當Y的初始值為0時,該工作者才能中止該事務。否則,它別無選擇,只能提交。因此,它不必等到它完成事務程式碼之後才知道它是否會提交。相反:只要它完成了事務中第一行程式碼的執行,它就已經知道了它的最終提交/中止決定。這意味著提交協議將能夠相對於2PC更早地啟動。
現在讓我們將這兩個例子組合成一個例子,其中一個事務由兩個工作者執行 - 其中一個正在完成第一個例子中描述的工作,另一個正在完成第二個例子中描述的工作。由於我們保證原子性,第一個工作者不能簡單地將X設定為42.相反,它自己的工作也必須依賴於Y的值。實際上,它的事務程式碼變為:
temp = Do_Remote_Read(Y) <b>if</b>(temp> 0) X = 42
請注意,如果第一個worker的程式碼是以這種方式編寫的,那麼另一個工作者的程式碼可以簡化為:
IF(Y> 0) 從Y減去1 Z = Y.
通過以這種方式編寫事務程式碼,我們從兩個工作者中刪除了顯式的中止邏輯,相反,兩個工作者都有if語句來檢查會導致原始事務中止的約束。如果原始事務中止,兩個工人最終都無所作為。否則,兩個工作者都會根據事務邏輯的需要更改其本地狀態的值。
此時需要注意的重要一點是,在上面的程式碼中完全消除了對提交協議的需求。由於應用程式程式碼在給定資料狀態下定義的條件邏輯以外的任何原因,系統不允許中止事務。並且所有工作者都在這個相同的條件邏輯上調整他們的寫入,這樣他們就可以獨立地決定在由於當前系統狀態而無法完成事務的情況下“什麼也不做”。因此,已經消除了事務中止的所有可能性,並且在事務處理結束時不需要任何型別的分散式協議來做出關於事務的組合的最終決定。2PC的所有問題都已消除。沒有阻塞問題,因為沒有協調器。並且沒有連鎖阻塞問題,因為所有必要的檢查都與事務處理重疊,而不是在完成之後。
此外,只要不允許系統因基於輸入資料狀態的條件應用程式邏輯之外的任何原因而中止事務,總是可以像上面那樣重寫任何事務以替換程式碼中的中止邏輯。 if語句有條件地檢查中止條件。此外,可以在不實際重寫應用程式程式碼的情況下實現此目的。[有關如何執行此操作的詳細資訊超出了本文的範圍,但總結為高級別:當分片完成任何可能導致中止的條件邏輯時,分片可以設定特殊的系統擁有的布林標誌,這些是從其他分片遠端讀取的布林標誌。]
(banq注:其實條件檢查也是第一段,檢查完成後提交也是第二段,兩段提交實際是在分散式系統中進行if檢查,然後在第二階段決定是否提交!)
實質上:在事務處理系統中有兩種型別的中止:(1)由資料狀態引起的中止和(2)由系統本身引起的中止(例如,故障或死鎖)。如上所述,類別(1)總是可以根據資料的條件邏輯來編寫。因此,如果您可以消除類別(2)中止,則可以消除了第二段的提交協議。
所以現在,我們所要做的就是解釋如何消除類別(2)中止。
消除系統引起的中止
我花了將近十年的時間來設計不允許系統導致中止的系統。此類系統的示例是Calvin ,CalvinFS ,Orthrus ,PVW 以及懶惰處理交易 的系統 。這一特性的推動力來自於這些專案中的第一個--- Calvin ---因為它是一個確定性的資料庫系統。確定性資料庫保證在給定一組定義的輸入請求的情況下,資料庫中只有一個可能的最終資料狀態。因此,可以將相同的輸入傳送到系統的兩個不同的副本,並確保副本將獨立地處理該輸入並最終處於相同的最終狀態,而沒有任何分歧的可能性。
系統引發的中止,例如系統故障或併發控制競爭條件,從根本上說是不確定性事件。
一個副本很可能會失敗或進入競爭狀態,而另一個副本則不會。如果允許這些非確定性事件導致事務中止,則一個副本可以中止事務而另一個事務將提交 - 這是對確定性保證的基本違反。
因此,我們必須以失敗和競爭條件不能導致交易中止的方式設計Calvin 。對於併發控制,Calvin使用了具有避免死鎖技術的悲觀鎖,該技術確保系統永遠不會陷入由於死鎖而必須中止事務的情況。面對系統故障,Calvin沒有完全從中斷的地方獲得事務(因為在失敗期間失去了易失性記憶體)。儘管如此,它還是能夠的使該事務的處理完成而不必中止。它通過從相同的原始輸入重新啟動事務來完成此操作。
這些解決方案 (死鎖避免或故障時重啟事務)都不限於在確定性資料庫系統中使用。[事務重啟在非確定性系統中變得有點棘手,如果與失敗期間丟失的事務相關聯的某些易失性狀態被其他未發生故障的計算機觀察到。但是有一些簡單的方法來解決這個問題,這個問題超出了這篇文章的範圍。]實際上,我上面連結的其他一些系統都是非確定性系統。一旦我們意識到消除系統級中止所帶來的強大功能,我們就將這個功能構建到我們在Calvin專案之後構建的每個系統中 - 甚至是非確定性系統。
結論
我認為系統架構師在向前發展的分散式分片系統中繼續使用2PC幾乎沒有什麼好處。我認為,消除系統引起的中止並重寫狀態引發的中止是更好的新方法。
確定性資料庫系統,如Calvin或FaunaDB 無論如何總是去除系統引起的中止,因此通常可以避免2PC,如上所述。但是將這種好處僅限於確定性資料庫是一個巨大的浪費。從非確定性系統中刪除系統引起的中止並不困難。最近的專案表明,甚至可以在使用除悲觀併發控制之外的併發控制技術的系統中消除系統引起的中止。例如,我們上面連結的PVW和惰性事務處理系統都使用多版本併發控制的變體。FaunaDB使用樂觀併發控制的變體。
在我看來,繼續“由關係統引起的系統中止需求"的過時假設幾乎沒有理由。在過去,當系統在單臺機器上執行時,這種假設是合理的。然而,在現代,許多系統需要擴充套件到可以彼此獨立地失敗的多臺機器,這些假設需要昂貴的協調和提交協議,例如2PC。
2PC的效能問題一直是非ACID相容系統興起的主要推動力,這些系統放棄了重要的保證,以實現更好的可擴充套件性,可用性和效能。2PC太慢了 - 它增加了所有事務的延遲 - 不僅僅是協議本身的長度,而且還阻止了訪問同一組資料的事務同時執行。2PC還限制了可伸縮性(通過降低併發性)和可用性(我們上面討論的阻塞問題)。前進的方向很明確:我們需要在設計我們的系統時重新考慮過時的假設,並對兩階段提交說“再見”!