在分布式系統中,一個事務可能涉及到集群中的多個節點。單個節點很容易知道自己執行的事務成功還是失敗,但因為網絡不可靠難以了解其它節點的執行狀態(可能事務執行成功但網絡訪問超時)。
若部分節點事務執行失敗進行回滾,而其它節點完成事務提交,則事務會處於部分完成的不一致狀態。為了避免錯誤,分布式系統需要使用分布式一致性協議來保證分布式事務的執行。
2PC
兩階段提交(2-Phase Commit, 2PC)是一種比較簡單的分布式一致性協議。
2PC協議中,每個事務需要一個協調者來協調各個參與者。每個事務分為兩步執行。
- 階段一: 事務請求
- 協調者向所有參與者發送事務內容,詢問是否可以執行事務操作。
- 各參與者執行事務,寫事務日志但不進行提交。 各參與者鎖定事務相關的資源,保證事務可以正常提交。
- 各參與者向協調者返回響應,YES表示可以提交,NO表示不可以提交。若協調者收到所有參與者的YES回復,則准備進行事務提交。若有參與者回復NO或者超時,則准備回滾事務。
- 階段二: 提交事務
- 協調者向所有參與者發送提交請求
- 參與者正式提交事務,並在完成后釋放相關資源。
- 參與者協調者回復ACK,協調者收到所有參與者的ACK后認為事務提交成功。
- 回滾事務
- 在事務請求階段若有參與者回復NO或者超時,協調者向所有參與者發出回滾請求
- 各參與者執行事務回滾,並在完成后釋放相關資源。
- 參與者協調者回復ACK,協調者收到所有參與者的ACK后認為事務回滾成功。
2PC是一種簡單的一致性協議,它存在一些問題:
- 單點服務: 若協調者突然崩潰則事務流程無法繼續進行或者造成狀態不一致
- 無法保證一致性: 若協調者第二階段發送提交請求時崩潰,可能部分參與者受到COMMIT請求提交了事務,而另一部分參與者未受到請求而放棄事務造成不一致現象。
- 阻塞: 為了保證事務完成提交,各參與者在完成第一階段事務執行后必須鎖定相關資源直到正式提交,影響系統的吞吐量。
參與者在完成階段一的事務執行后等待協調者的下一個請求,若協調者超時則可以自行放棄事務。
這種方案仍然有無法保證一致性的缺點,但並不會出現某些資料所述一直鎖定資源,無法繼續的情況。
3PC
三階段提交協議(3-Phase Commit, 3PC)進一步將事務請求分為兩個階段,可以解決2PC協議阻塞的問題但無法解決單點服務和不一致的問題。
3PC協議下事務分三步提交:
- CanCommit
- 協調者向所有參與者發送CanCommit請求
- 各參與者判斷是否可以完成事務提交,但不執行事務也不鎖定資源
- 各參與者根據是否可以完成事務向協調者回復YES或NO
- PreCommit
- 協調者向所有參與者發送PreCommit請求,執行事務預提交
- 各參與者執行事務,寫事務日志但不進行提交。 各參與者鎖定事務相關的資源,保證事務可以正常提交。
- 各參與者向協調者返回響應。若協調者收到所有參與者的YES回復,則准備進行事務提交。若有參與者回復NO或者超時,則放棄事務。
- DoCommit
- 協調者向所有參與者發送提交請求
- 參與者正式提交事務,並在完成后釋放相關資源。
- 參與者協調者回復ACK,協調者收到所有參與者的ACK后認為事務提交成功。若有參與者回復NO或者超時,則回滾事務。
- 參與者進入 PreCommit 狀態后,若始終未收到協調者的 DoCommit 請求則會超時后自動執行提交。
三階段提交協議在CanCommit階段不鎖定資源,解決了阻塞降低吞吐量的問題。
若某個參與者進入 PreCommit 后始終未收到協調者的進一步指令則會自動提交,該策略一定程度上避免協調者單點服務問題。
但是 3PC 仍然無法解決數據不一致問題。
Paxos
Paxos 算法的目的在於使分布式系統對於某個值達成一致,比如 Master 選舉過程中保證最終所有節點對 Master 身份達成共識。
作者認為 Paxos 解決的分布式共識問題與分布式事務有着較大不同。
Paxos 認為信道可能丟失數據但是不會篡改數據(即不存在拜占庭將軍問題),實際上我們也很容易通過校驗檢查數據是否被篡改。
在介紹Paxos算法之前,我們先來分析2PC(3PC)協議在分布式共識問題上的不足。
2PC(3PC)協議要求收到所有參與者的 ACK 消息后才認為提交成功,而在Master選舉這類分布式共識問題上只需要過半參與者達成一致即可。
而最難以解決的問題在於協調者的單點服務問題,若協調者在過程中崩潰則集群很難繼續達成共識。
因此,關鍵在於設計在有多個協調者的情況下仍然可以達成共識的協議。
Basic Paxos
Paxos算法中有3個角色:
- Proposer: 負責發起提案,類似於2PC中的協調者
- Acceptor: 負責批准提案,類似於2PC中的參與者
- Learner: 不參與提案過程,只從其它Acceptor那里學習已通過的提案。
我們重點介紹 Proposer 和 Acceptor 參與的流程,暫時不介紹 Learner。
在集群中每個進程(節點)可能會扮演其中多個角色。
提案由編號N和值V組成記作(N, V), 每個提案都的編號N是唯一的。保證編號唯一非常簡單,若集群中有k個 Proposer, 那么第i個Proposer提出的第n個提案編號為 i + k * n。
我們希望集群最終可以選中一個V,且所有節點知道集群最終選定的V值。
算法做出幾個規定:
- 只要集群中有超過半數的Accpetor批准了提案,Proposer 就可以認為集群對接受了提案
- 在一輪投票中,Acceptor總是批准它收到的第一個提案
- 在一輪投票中,Acceptor可以批准多個提案,但是批准提案的值V必須相同
算法分為兩個階段:
- prepare 階段
- Proposer 選擇提案N,向半數以上Acceptor發送請求Prepare(N)
- Acceptor 保存自己受到過的最大請求的編號 maxN 和已接受的編號最大提案 (acceptedN, acceptedV)。
- 若 maxN > N, 那么 Acceptor 返回拒絕響應
- 若 maxN < N, 那么 Acceptor 返回已接受的編號最大提案(acceptN, acceptV),若尚未接受過提案則返回空的成功響應。同時,Acceptor 更新 maxN, 即不會在接受編號小於N的請求
- accept 階段
- 若 Proposer 收到過半 Acceptor 對 Prepare(N) 返回的ACK響應,那么它會從響應的提案中選出編號最大的一個(acceptN, acceptV), 若響應中不包含提案則由 Proposer 決定提案。決定提案后 Proposer 會向過半 Acceptor 發送 Accept(N, V)請求。
- Acceptor 收到 Accept(N, V) 請求后
- 若 maxN > N, 那么 Acceptor 返回拒絕響應
- 若 maxN < N, 那么 Acceptor 返回成功響應,並更新已接受的編號最大提案 (acceptedN, acceptedV)
- 若 Proposer 未收到過半 Acceptor 對 Accept(N, V) 請求的成功響應,則認為提案被拒絕。
若集群中存在兩個 Proposer 依次提出編號遞增的提案可能會使 Paxos 算法陷入死循環:
- Proposer1 提出提案 N1, 並收到過半Prepare(N1)響應
- Proposer2 提出提案 N2 (N2 > N1), 並收到過半Prepare(N2)響應
- Proposer1 進入第二階段, 過半Accept(N1)請求被拒絕 (過半Acceptor 的 maxN = N2)。 Proposer1 提出提案 N3 (N3 > N2) ...
這種情況稱為算法陷入活鎖,在工程實踐中我們通常選擇一個 Proposer 作為 leader。
Paxos 算法可以在數學上證明它的正確性深入淺出Paxos算法協議。
Paxos 算法實現難度和運行開銷非常大,因此開發出 Raft、ZAB等協議用於生產實踐。