分散式系統原理(9)Paxos 協議
Paxos 協議
簡介
Paxos 協議是少數在工程實踐中證實的強一致性、高可用的去中心化分散式協議
Paxos 協議的流程較為複雜,但其基本思想卻不難理解,類似於人類社會的投票過程。Paxos 協議中,有一組完全對等的參與節點(稱為 accpetor),這組節點各自就某一事件做出決議,如果某個決議獲得了超過半數節點的同意則生效。Paxos 協議中只要有超過一半的節點正常,就可以工作,能很好對抗宕機、網路分化等異常情況
介紹 Paxos 協議的資料很多,Lamport 的論文也寫得簡明有趣。與大多數材料不同的是,本文不首先介紹協議的推理和證明過程,而是從工程上的演算法流程描述起,感性的介紹協議過程。進而用一些複雜的例子演示協議的過程。最後,本文再介紹協議是如何推導設計出來的
協議描述
節點角色
Paxos 協議中,有三類節點:
- Proposer: 提案者。Proposer 可以有多個,Proposer 提出議案(value)。所謂 value,在工程中可以是任何操作,例如“修改某個變數的值為某個值”、“設定當前 primary 為某個節點”等等。Paxos協議中統一將這些操作抽象為 value。不同的 Proposer 可以提出不同的甚至矛盾的 value,例如某個Proposer 提議“將變數 X 設定為 1”,另一個 Proposer 提議“將變數 X 設定為 2”,但對同一輪 Paxos過程,最多隻有一個 value 被批准。
- Acceptor: 批准者。 Acceptor 有 N 個, Proposer 提出的 value 必須獲得超過半數(N/2+1)的Acceptor批准後才能通過。Acceptor 之間完全對等獨立。
- Learner: 學習者。Learner 學習被批准的 value。所謂學習就是通過讀取各個 Proposer 對 value的選擇結果,如果某個 value 被超過半數 Proposer 通過,則 Learner 學習到了這個 value。 回憶(2.4 )不難理解,這裡類似 Quorum 機制,某個 value 需要獲得 W=N/2 + 1 的 Acceptor 批准,從而學習者需要至少讀取 N/2+1 個 Accpetor,至多讀取 N 個 Acceptor 的結果後,能學習到一個通過的 value。上述三類角色只是邏輯上的劃分,實踐中一個節點可以同時充當這三類角色
流程描述
Paxos 協議一輪一輪的進行,每輪都有一個編號。每輪 Paxos 協議可能會批准一個 value,也可能無法批准一個 value。如果某一輪 Paxos 協議批准了某個 value,則以後各輪 Paxos 只能批准這個value。上述各輪協議流程組成了一個 Paxos 協議例項,即一次 Paxos 協議例項只能批准一個 value,這也是 Paxos 協議強一致性的重要體現。每輪 Paxos 協議分為階段,準備階段和批准階段,在這兩個階段 Proposer 和 Acceptor 有各自的
處理流程
Proposer 的流程:
(準備階段)
1. 向所有的 Acceptor 傳送訊息“Prepare(b)”;這裡 b 是 Paxos 的輪數,每輪遞增
2. 如果收到任何一個 Acceptor 傳送的訊息“Reject(B)”,則對於這個 Proposer 而言本輪 Paxos 失敗,將輪數 b 設定為 B+1 後重新步驟 1;
(批准階段,根據收到的 Acceptor 的訊息作出不同選擇)
3. 如果接收到的 Acceptor 的“Promise(b, v_i)”訊息達到 N/2+1 個(N 為 Acceptor 總數,除法取整,下同) ;v_i 表示 Acceptor 最近一次在 i 輪批准過 value v。
3.1 如果收到的“Promise(b, v)”訊息中, v 都為空, Proposer 選擇一個 value v,向所有 Acceptor 廣播 Accept(b, v);
3.2 否則,在所有收到的“Promise(b, v_i)”訊息中,選擇 i 最大的 value v,向所有 Acceptor 廣播訊息 Accept(b,v);
4. 如果收到 Nack(B),將輪數 b 設定為 B+1 後重新步驟 1;
Accpetor 流程
(準備階段)
1. 接受某個 Propeser 的訊息 Prepare(b)。
引數 B 是該 Acceptor 收到的最大 Paxos 輪數編號;V 是 Acceptor 批准的 value,可以為空
1.1 如果 b>B,回覆 Promise(b, V_B),設定 B=b; 表示保證不再接受編號小於 b 的提案。
1.2 否則,回覆 Reject(B)
(批准階段)
2. 接收 Accept(b, v)
2.1 如果 b < B, 回覆 Nack(B),暗示 proposer 有一個更大編號的提案被這個 Acceptor 接收了
2.2 否則設定 V=v。表示這個 Acceptor 批准的 Value 是 v。廣播 Accepted 訊息
例項
基本例子
基本例子裡有 5 個 Acceptor,1 個 Proposer,不存在任何網路、宕機異常。我們著重考察各個 Accpetor 上變數 B 和變數 V 的變化,及 Proposer 上變數 b 的變化
1.初始狀態
Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 | |
---|---|---|---|---|---|
B | 0 | 0 | 0 | 0 | 0 |
V | NULL | NULL | NULL | NULL | NULL |
Proposer 1 | |
---|---|
b | 1 |
2.Proposer 向所有 Accpetor 傳送“Prepare(1)”,所有 Acceptor 正確處理,並回復 Promise(1, NULL)
Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 | |
---|---|---|---|---|---|
B | 1 | 1 | 1 | 1 | 1 |
V | NULL | NULL | NULL | NULL | NULL |
Proposer 1 | |
---|---|
b | 1 |
3.Proposer 收到 5 個 Promise(1, NULL),滿足多餘半數的 Promise 的 value 為空,此時傳送Accept(1, v1),其中 v1 是 Proposer 選擇的 Value
Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 | |
---|---|---|---|---|---|
B | 1 | 1 | 1 | 1 | 1 |
V | v1 | v1 | v1 | v1 | v1 |
4.此時, v1 被超過半數的 Acceptor 批准, v1 即是本次 Paxos 協議例項批准的 Value。 如果 Learner學習 value,學到的只能是 v1
批准的 Value 無法改變
在同一個 Paxos 例項中,批准的 Value 是無法改變的,即使後續 Proposer 以更高的序號發起 Paxos協議也無法改變 value
1.例如,某次 Paxos 協議執行後,Acceptor 的狀態是:
Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 | |
---|---|---|---|---|---|
B | 3 | 3 | 3 | 2 | 2 |
V | v1 | v1 | v1 | NULL | NULL |
5 個 Acceptor 中,有 3 個已經在第三輪 Paxos 協議批准了 v1 作為 value。其他兩個 Acceptor 的 V為空,這可能是因為 Proposer 與這兩個 Acceptor 的網路中斷或者這兩個 Acceptor 宕機造成的。
2.此時,即使有新的 Proposer 發起協議,也無法改變結果。假設 Proposer 傳送“prepare(4)訊息”,由於 4 大於所有的 Accpetor 的 B 值,所有收到 prepare 訊息的 Acceptor 回覆 promise 訊息。但前三個 Acceptor 只能回覆 promise(4, v1_3),後兩個 Acceptor 回覆 promise(4, NULL)
Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 | |
---|---|---|---|---|---|
B | 4 | 4 | 4 | 4 | 4 |
V | v1 | v1 | v1 | NULL | NULL |
3.此時,Proposer 可能收到若干個 Acceptor 傳送的 promise 訊息,沒有收到的 promise 訊息可能是網路異常造成的。無論如何,Proposer 要收到至少 3 個 Acceptor 的 promise 訊息後才滿足協議中大於半數的約束,才能傳送 accpet 訊息。這 3 個 promise 訊息中,至少有 1 個訊息是 promise(4, v1_3),至多 3 個訊息都是 promise(4,v1_3)。另一方面,Proposer 始終不可能收到 3 個 promise(4, NULL)訊息,最多收到 2 個。綜上,按協議流程,Proposer 傳送的 accept 訊息只能是“accept(4, v1)”而不能自由選擇 value
無論這個 accept 訊息是否被各個 Acceptor 接收到,都無法改變 v1 是被批准的 value 這一事實。即從全域性看,有且只有 v1 是滿足超過多數 Acceptor 批准的 value。例如,假設 accept(4, v1)訊息被Acceptor 1、Acceptor2、Acceptor4 收到,那麼狀態變為:
Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 | |
---|---|---|---|---|---|
B | 4 | 4 | 4 | 4 | 4 |
V | v1 | v1 | v1 | v1 | NULL |
從這個例子我們可以看到一旦一個 value 被批准,此後永遠只能批准這個 value
一種不可能出現的狀態
Paxos 協議的核心就在與“批准的 value 無法改變”,這也是整個協議正確性的基礎,為了更好的理解後續對 Paxos 協議的證明。這裡再看一種看似可能,實際違反協議的狀態,這種狀態也是後續反證法證明協議時的一種錯誤狀態
Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 | |
---|---|---|---|---|---|
B | 1 | 1 | 1 | 2 | 2 |
V | v1 | v1 | v1 | v2 | v2 |
上述狀態中,3 個輪次為 1 的 Acceptor 的 value 為 v1,2 個輪次更高的 Acceptor 的 value 為 v1。此時被批准的 value 是 v1
假設此時發生新的一輪 b=3 的 Paxos 過程,Proposer 有可能收到 Acceptor 3、4、5 發出的 3 個promise 訊息分別為“promise(1, v1_1)”,“promise(2, v2_2)” “promise(2, v2_2)”。按協議,proposer選擇 value 編號最大的 promise 訊息,即 v2_2 的 promise 訊息,傳送訊息“Accept(3, v2)”,從而使得最終的批准的 value 成為 v2。就使得批准的 value 從 v1 變成了 v2
上述假設看似正確,其實不可能發生。這是因為本節中給出的初始狀態就是不可能出現的。這是因為,要到達成上述狀態,發起 prepare(2)訊息的 proposer 一定成功的向 Acceptor 4、Acceptor 5傳送了 accept(2, v2)。但傳送 accept(2, v2)的前提只能是 proposer 收到了 3 個“promise(2, NULL)”訊息。 然而,從狀態我們知道,在 b=1 的那輪 Paxos 協議裡,已經有 3 個 Acceptor 批准了 v1,這 3 個Acceptor 在 b=2 時發出的訊息只能是 promise(2, v1_1),從而造成 proposer 不可能收到 3 個“promise(2, NULL)”,至多隻能收到 2 個“promise(2, NULL)”。另外,只要 proposer 收到一個“promise(2, v1_1)”,其傳送的 accept 訊息只能是 accept(2, v1)
從這個例子我們看到 Prepare 流程中的第 3 步是協議中最為關鍵的一步,它的存在嚴格約束了“批准的 value 無法改變”這一事實。在後續協議推導中我們將看到這一步是如何被設計出來的
節點異常
這裡給一個較為複雜的異常狀態下 Paxos 執行例項。本例子中有 5 個 Acceptor 和 2 個 Proposer
1.Proposer 1 發起第一輪 Paxos 協議,然而由於異常,只有 2 個 Acceptor 收到了 prepare(1)訊息
Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 | |
---|---|---|---|---|---|
B | 1 | 1 | 0 | 0 | 0 |
V | NULL | NULL | NULL | NULL | NULL |
2.Proposer 1 只收到 2 個 promise 訊息,無法發起 accept 訊息;此時, Proposer 2 發起第二輪 Paxos協議,由於異常,只有 Acceptor 1、3、4 處理了 prepare 訊息,併發送 promise(2, NULL)訊息
Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 | |
---|---|---|---|---|---|
B | 2 | 1 | 2 | 2 | 0 |
V | NULL | NULL | NULL | NULL | NULL |
3.Proposer 2 收到了 Acceptor 1、3、4 的 promise(2, NULL) 訊息,滿足協議超過半數的要求,選擇了 value 為 v1,廣播了 accept(2, v1)的訊息。由於異常,只有 Accptor 3、4 處理了這個訊息
Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 | |
---|---|---|---|---|---|
B | 2 | 1 | 2 | 2 | 0 |
V | NULL | NULL | v1 | v1 | NULL |
4.Proposer 1 以 b=3 發起新一輪的 Paxos 協議, 由於異常,只有 Acceptor 1、 2、 3、 5 處理了 prepare(3)訊息
Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 | |
---|---|---|---|---|---|
B | 3 | 3 | 3 | 2 | 3 |
V | NULL | NULL | v1 | v1 | NULL |
5.由於異常,Proposer 1 只收到 Acceptor1、2、5 的 promise(3, NULL)的訊息,符合協議要求,Proposer 1 選擇 value 為 v2,廣播 accept(3, v2)訊息。由於異常,這個訊息只被 Acceptor 1、2 處理
Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 | |
---|---|---|---|---|---|
B | 3 | 3 | 3 | 2 | 3 |
V | v2 | v2 | v1 | v1 | NULL |
當目前為止,沒有任何 value 被超過半數的 Acceptor 批准,所以 Paxos 協議尚沒有批准任何 value。然而由於沒有 3 個 NULL 的 Acceptor,此時能被批准的 value 只能是 v1 或者 v2 其中之一
6.此時 Proposer 1 以 b=4 發起新的一輪 Paxos 協議,所有的 Acceptor 都處理了 prepare(4)訊息
Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 | |
---|---|---|---|---|---|
B | 4 | 4 | 4 | 4 | 4 |
V | v2 | v2 | v1 | v1 | NULL |
7.由於異常,Proposer 1 只收到了 Acceptor3 的 promise(4, v1_3)訊息、Acceptor4 的 promise(4, v1_2)、 Acceptor5 的 promise(4, NULL)訊息,按協議要求,只能廣播 accept(4, v1)訊息。假設 Acceptor2、3、4 收到了 accept(4, v1)訊息。由於批准 v1 的 Acceptor 超過半數,最終批准的 value 為 v1
Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 | |
---|---|---|---|---|---|
B | 4 | 4 | 4 | 4 | 4 |
V | v2 | v1 | v1 | v1 | NULL |
競爭及活鎖
從前面的例子不難看出,Paxos 協議的過程類似於“佔坑”,哪個 value 把超過半數的“坑”(Acceptor)佔住了,哪個 value 就得到批准了。這個過程也類似於單機系統並行系統的加鎖過程。假如有這麼單機系統:系統內有 5 個鎖,有多個執行緒執行,每個執行緒需要獲得 5 個鎖中的任意 3 個才能執行後續操作,操作完成後釋放佔用的鎖。我們知道,上述單機系統中一定會發生“死鎖”。例如, 3 個執行緒併發,第一個執行緒獲得 2 個鎖,第二個執行緒獲得 2 個鎖,第三個執行緒獲得 1 個鎖。此時任何一個執行緒都無法獲得 3 個鎖,也不會主動釋放自己佔用的鎖,從而造成系統死鎖。
但在 Paxos 協議過程中,雖然也存在著併發競爭,不會出現上述死鎖。這是因為,Paxos 協議引入了輪數的概念,高輪數的 paxos 提案可以搶佔低輪數的 paxos 提案。從而避免了死鎖的發生。然而這種設計卻引入了“活鎖”的可能,即 Proposer 相互不斷以更高的輪數提出議案,使得每輪 Paxos過程都無法最終完成,從而無法批准任何一個 value
1.Proposer 1 以 b=1 提起議案,傳送 prepare(1)訊息,各 Acceptor 都正確處理,迴應 promise(1, NULL)
Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 | |
---|---|---|---|---|---|
B | 1 | 1 | 1 | 1 | 1 |
V | NULL | NULL | NULL | NULL | NULL |
2.Proposer 2 以 b=2 提起議案,傳送 prepare(2)訊息,各 Acceptor 都正確處理,迴應 promise(2, NULL)
Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 | |
---|---|---|---|---|---|
B | 2 | 2 | 2 | 2 | 2 |
V | NULL | NULL | NULL | NULL | NULL |
3.Proposer 1 收到 5 個 promise(1, NULL)訊息,選擇 value 為 v1 傳送 accept(1, v1)訊息,然而這個訊息被所有的 Acceptor 拒絕,收到 5 個 Nack(2)訊息
4.Proposer 1 以 b=3 提起議案,傳送 prepare(3)訊息,各 Acceptor 都正確處理,迴應 promise(3, NULL)
Acceptor 1 | Acceptor 2 | Acceptor 3 | Acceptor 4 | Acceptor 5 | |
---|---|---|---|---|---|
B | 3 | 3 | 3 | 3 | 3 |
V | NULL | NULL | NULL | NULL | NULL |
5.Proposer 2 收到 5 個 promise(2, NULL)訊息,選擇 value 為 v2 傳送 accept(2, v2)訊息,然而這個訊息被所有的 Acceptor 拒絕,收到 5 個 Nack(3)訊息
上述過程交替進行,則永遠無法批准一個 value,從而形成 Paxos 協議活鎖。Paxos 協議活鎖問題也是這個協議的主要問題
工程投影
Chubby 中的 Paxos
Chubby 是最早基於 Paxos 的分散式系統之一。Chubby 的設計人員沒有直接提供一種 Paxos 的開發庫,而是利用 Paxos 實現一個高可用的分散式系統,再利用這個分散式系統對外提供高可用儲存、分散式鎖等服務,從而間接的提供了 Paxos 功能。
Chubby 中的節點完全是對等的,通過 Paxos 協議,這些節點選舉出一個 Master 節點(Primary),公開的資料中沒有解釋 Chubby 使用 Paxos 的細節,例如如何選擇 Paxos 的輪次號,如何避免 Paxos活鎖等。當選舉出 Primary 節點後,所有讀寫操作都由 Primary 節點控制,Chubby 系統從一個完全對等的去中心化狀態變為一個 Primary-Secondary 的中心化狀態。當 Primary 異常時, Chubby 節點將重新利用 paxos 協議發起新一輪的選舉以確定新的 primary 節點。新 primary 節點與原 primary 節點具有完全一樣的持久化資訊,新 primary 將代替原 primary 節點對外提供讀寫服務。
基於 Chubby 的服務,其他的分散式系統可以很容易的實現選擇 primary、儲存最核心元資料等功能。利用 Chubby 可以大大簡化分散式系統的設計:可以認為整個 Chubby 叢集邏輯上是一個 magic的高可用(幾乎不會停服務)的中心節點,其他分散式系統可以基於這個大中心節點可以實現中心化的副本控制協議。由於 Chubby 叢集本身是由多個節點組成的分散式系統, 基於 Chubby 的分散式系統無需直接實現 Paxos 協議,就可以利用 Paxos 協議實現全域性完全無單點
Zookeeper 中的 Paxos
Zookeeper 使用了一種修改後的 Paxos 協議。
首先, Zookeeper 的協議執行依賴 TCP 協議實現 FIFO, Zookeeper 通過 TCP 協議獲得兩點保障:1、資料總是嚴格按照 FIFO (first in first out)規則從一個節點傳遞到另一個節點的; 2、當某個 TCP連結關閉後,這個連結上不再有資料傳遞。由於 TCP 協議為傳輸的每一個位元組設定了序列號(sequence number)及確認(acknowledgment),上述兩點在 TCP 協議上是完全可以保證的。需要注意的是 Zookeeper 並不要求 TCP 協議可以可靠的將資料傳輸到對端節點,基於 TCP 協議實現真正意義上的可靠傳輸也是做不到的。Zookeeper 基於 TCP 的上述兩點保障,可以較大的簡化問題模型,忽略諸如網路訊息亂序、網路訊息重複等的異常,從而較大的簡化協議設計。
再者,在 Zookeeper 中,始終分為兩種場景:一、Leader activation,在這個場景裡,系統中缺乏 Leader(primary) ,通過一個類似 paxos 協議的過程完成 Leader 選舉。二、Active messaging,在這個場景裡,Leader 接收客戶端傳送的更新操作,以一種類似兩階段提交的過程在各個 follower(secondary)節點上進行更新操作。在 Leader activation 場景中完成 leader 選舉及資料同步後,系統轉入 Active messaging 場景,在 active messaging 中 leader 異常後,系統轉入 Leader activation 場景。
無論在那種場景,Zookeeper 依賴於一個全域性版本號:zxid。zxid 由(epoch, count)兩部分組成,高位的 epoch 部分是選舉編號,每次提議進行新的 leader 選舉時 epoch 都會增加,低位的 count 部分是 leader 為每個更新操作決定的序號。可以認為,一個 leader 對應一個唯一的 epoch,每個 leader任期內產生的更新操作對應一個唯一的有序的 count,從而從全域性的視野,一個 zxid 代表了一個更新操作的全域性序號(版本號)。每個 zookeeper 節點都有各自最後 commit 的 zxid,表示這個 zookeeper 節點上最近成功執行的更新操作,也代表了這個節點的資料版本。在 Leader activation 階段,每個 zookeeper 節點都以自己的 zxid 作為 Paxos 中的 b 引數發起 paxos 例項,設定自己作為 leader(此為 value)。
每個 zookeeper節點既是 proposer 又是 acceptor,所以,每個 zookeeper 節點只會 accpet 提案編號 b 大於自身 zxid的提案。不難理解,通過 paxos 協議過程,某個超過 quorum 半數的節點中持有最大的 zxid 的節點會成為新的 leader。值得注意的是,假如參與選舉的每個 zookeeper 節點的 zxid 都一樣,即所有的節點都以相同的 b=zxid 發提案,那麼就有可能傳送無法選舉出 leader 的情況。 zookeeper解決這個問題的辦法很簡單,zookeeper 要求為每個節點配置一個不同的的節點編號,記為 nodeid,paxos 過程中以 b=(zxid, nodeid)發起提議,從而當 zxid 相同時會優先選擇節點編號較大的節點成為leader。成為新 leader 的節點首先與 follower 完成資料同步後,再次說明,資料同步過程可能會涉及刪除 follower 上的最後一條髒資料。當與至少半數節點完成資料同步後, leader更新 epoch,在各個 follower 上以(epoch + 1, 0) 為 zxid 寫一條沒有資料的更新操作。這個更新操作稱為 NEW_LEADER 訊息,是為了在各個節點上更新 leader 資訊,當收到超過半數的 follower 對NEW_LEADER 的確認後, leader 發起對 NEW_LEADER 的 COMMIT 操作,並進入 active messaging狀態提供服務。
進入 active messaging 狀態的 leader 會接收從客戶端發來的更新操作,為每個更新操作生成遞增的 count,組成遞增的 zxid。 Leader 將更新操作以 zxid 的順序傳送給各個 follower (包括 leader 本身,一個 leader 同時也是 follower),當收到超過半數的 follower 的確認後,Leader 傳送針對該更新操作的 COMMIT 訊息給各個 follower。這個更新操作的過程很類似兩階段提交,只是 leader 永遠不會對更新操作做 abort 操作。
如果 leader 不能更新超過半數的 follower,也說明 leader 失去了 quorum,此時可以發起新的 leader選舉,最後一條更新操作處於“中間狀態”,其是否生效取決於選舉出的新 leader 是否有該條更新操作。從另一個角度,當 leader 失去 quorum 的 follower,也說明可能有一個超過半數的節點集合正在選舉新的 leader。
Zookeeper 通過 zxid 將兩個場景階段較好的結合起來,且能保證全域性的強一致性。由於同一時刻只有一個 zookeeper 節點能獲得超過半數的 follower,所以同一時刻最多隻存在唯一的 leader;每個 leader 利用 FIFO 以 zxid 順序更新各個 follower,只有成功完成前一個更新操作的才會進行下一個更新操作,在同一個 leader 任期內,資料在全域性滿足 quorum 約束的強一致,即讀超過半數的節點一定可以讀到最新已提交的資料;每個成功的更新操作都至少被超過半數的節點確認,使得新選舉的 leader 一定可以包括最新的已成功提交的資料
Megastore 中的 Paxos
Megastore 中的副本資料更新基於一個改良的 Paxos 協議進行。與 Chubby 和 Zookeeper 僅僅利用 Paxos 選出 primary 不同的是,Megastore 的每次資料更新都是基於一個 Paxos 協議的例項。從而使得 Megastore 具有一個去中心化的副本控制機制。另一方面,為了獲得較大的資料更新效能,Megastore 又引入了類似 Primary 的 leader 角色以在絕大部分的正常流程時優化原有 paxos 協議
基本的 Paxos 協議兩個特點使得其效能不會太高:
- 每個 Paxos 執行例項,至少需要經歷三輪網路互動:Proposer 傳送 prepare 訊息、Acceptor 傳送 promise 訊息、Proposer 再發送 accept 訊息;
- 讀取 Paxos 上的資料時,需要讀取超過半數的 Acceptor 上的結果才能獲得資料。
對於一個高吞吐、高併發的線上儲存系統,上述特性會制約系統的效能。為此,Megastore 使用了一些方式對 Paxos協議進行了改良。 Megastore 中的副本一般都是跨機房、跨地域部署,在通常狀態下,某個使用者只會
。為此,Megastore 在每個機房為每個副本部署一個特殊的稱為協調器(coordinator)的服務。Coordinator 服務相對 Megastore 的底層 Big table 系統而言顯得非常簡單,其主要功能就是維護副本直接一致性的資訊,外部節點(主要是 Megastore 的 client)可以通過訪問Coordinator 獲知當前本地副本是否與其他副本一致,即當前副本是否具有最新的已提交的資料。利用 Coordinator,如果判斷出當前本地副本已經是最新的資料,則只需讀取本地副本,而不需要讀取超過半數的節點就可以讀取到最新的資料
下面介紹 Megastore 中的資料讀取流程, Megastore 中的資料讀取流程除了讀取資料外還有兩個重要功能:
- 嘗試更新本地副本的 coordinator。
- 解決中間態資料問題。這裡需要說明的是,Megastore 的更新日誌與資料是分離的,每個 Megastore 副本收到更新操作後,都會立刻更新自己的日誌,但不會立刻把更新操作應用到對應的 Big Table 中。這是因為,在類似 Paxos 的 Prepare 階段就傳送到各個副本了, 此時更新操作會寫入日誌, 但只有收到 Accept 訊息後,副本才能確定這是一個已經成功提交的 Paxos 的資料,才可以將更新操作真正寫入 Big Table
Megastore 資料讀取流程:
1. 查詢本地副本對應 Coodinator 以獲知本地副本是否已經是最新的已提交的資料
2. 選擇一個要讀取的副本,使得該副本肯定包含最新的已提交的更新操作。
2.1 如果從 1 中發現本地副本已經包含最新的已提交的資料,則選擇本地副本。
2.2 如果 1 中檢查失敗,則讀取半數以上的副本,從中挑選版本號最大的副本。
3. 追趕資料。對於選擇的副本,如果不能確定副本上某次更新操作是已經提交的,則通過查詢其他副本確定。如果讀取所有副本都無法確定,則以 paxos 協議發起一次空的更新操作。則要麼空操作成為本次 paxos 的 value,要麼之前不能確定的更新操作成為 paxos 的 value。
4. 修正 Coordinator。如果在 2 中選擇了本地副本,且在 1 中 Coordinator 認為本地副本不包含最新已提交的資料,則向 Coordinator 傳送一個 Validation 訊息,告訴 Coorinator 本地副本以及與其他副本一致。
5. 向 2 中選擇的副本查詢資料,如果查詢失敗,重新發起本流程並選擇其他副本讀資料
這裡解釋追趕資料這一過程。假設有 3 個副本,正如在 2.4.4 中詳細分析的,如果僅僅讀取兩個副本,雖然已經滿足 Quorum,但在這兩個副本中選擇版本號最大的一個副本,卻不能知道該副本上最後一個版本的資料是不是最新的已提交的資料。例如,如果 3 副本的版本是(3, 2, 3),那麼讀取前兩個副本,3 已經是最新的已提交的版本,但如果 3 副本的版本是(3, 2, 2),那麼讀取前兩個副本,3 不是一個最新的已提交的版本。在 Megastore 中,系統利用 Paxos 協議更新,如果某個副本收到對於某個版本的 Accept 訊息,則說明該版本資料已經提交提交,對於沒有收到 Accept 訊息的資料,副本本身無法判斷該資料是否已經提交。為此 Megastore 在讀取資料增加了追趕資料的過程,就是為了在各個副本上確定每個 paxos 例項最終產生的 value
另一方面,如果某次更新對應的 paxos 例項不完整,那麼也無法確定該次更新產生的 value。例如,accept 訊息只在某一個副本上產生效果並生成對於的更新日誌,此時讀取所有的副本可以發現該日誌並非一個已經成功提交的更新,且對應的那個 paxos 例項也還產生 value,有可能那次更新操作已經失敗,也有可能那次更新操作正在進行。為此 Megastore 發起一次 Paxos 空操作,要麼空操作成為最後 paxos 的 value,要麼正在進行的更新操作成為 paxos 的 value
從上述流程不難發現,在沒有異常,各副本一致的情況下,查詢只會發生在本地副本,而無需讀取多個副本
再繼續討論 Megastore 的更新流程。 Megastore 每次成功的更新操作都會附帶指定下一次更新操作的 Leader 副本。通常,客戶端指定本地機房的副本作為 leader 副本。所謂 Leader 副本非常類似Primary-Secondary 中的 Primary,但 leader 副本不是必須的,只是一種效能優化,利用 leader 副本嘗試跳過 Paxos 的準備階段, 簡化了 Paxos 流程。但當 leader 副本失敗(類似於程式碼優化中的 fast path失敗),系統退化到普通的 paxos 過程
Megastore 中的資料更新流程:
1. 嘗試使用 Leader 直接提交資料。訪問 Leader 節點,請求 Leader 節點以 paxos 編號 0 直接向
各副本傳送 Accept 訊息。如果成功,轉 3.
2. 準備階段:通過更新操作在日誌中的位置,獲得當前 paxos 例項的編號。在本次 paxos 例項
中,選擇一個最大的輪次號 b 發起正常的 Paxos 準備流程,如果收到超過半數的 promise 消
息,則轉 3.
3. 批准階段:向所有的副本傳送 Accept 訊息,如果失敗,轉 2.
4. 修正 Coordinator。如果沒有收到某個副本的 Accepted 訊息,向該副本對應的 Coordinator
傳送一個 Invalid 訊息,告知該 Coordinator 對應副本已經不與其他副本同步。
5. 各個節點根據本次操作日誌更新對應的 Big Table 中的資料
上述流程中,步驟 1 嘗試使用 Leader 副本進行快速更新。如果該 leader 副本收到的是本次 paxos例項(對應於全域性更新操作的次序)第一個更新請求,則該流程可以生效。當 leader 節點失效,或者有併發的多個更新請求時,該優化失敗,轉為正常的 Paxos 過程
流程中的步驟 4 是不能失敗的,如果某個副本處於不一致的狀態,而又不能通知對應的Coordinator,則使用者就有可能在讀取流程中讀到該副本上的資料,從而打破系統的強一致性。Megastore 對此的辦法是:1. 相比於底層的 Big table 系統,Coordinator 是一個非常簡單的無狀態的輕量級服務,其穩定性本身較高。2. 每個 Coordinator 的狀態都會計入 Chubby,一旦 Coordinator失去 Chubby 中鎖,即失去 Chubby lease, Coordinator 會將對應副本的狀態標記為不一致。其他節點可以通過監控 Coordinator 再 Chubby 中對應的鎖而獲知 Coordinator 的狀態。從而,一旦一個Coordinator 異常失效,更新流程可能會阻塞在第 4 步直到這個 Coordinator 失去在 Chubby 中的鎖。在極端網路分化等異常下,可能有這樣的情況:更新流程的執行節點無法給 Coordinator 傳送 Invalid訊息,而 Coordinator 卻能始終佔有 Chubby 中對應的鎖。 Megastore 將這種極端情況通過 OP 手動殺Coordinator 解決
Megastore 通過改良的 Paxos 協議給出了一種跨機房、跨地域實現高可用系統的方案。與 PNUTS的跨機房方案相比, Megastore 的方案具有強一致性,且可以隨時在多個副本上讀取最新的已提交的資料。而 PNUTS 的雖然也具有讀最新已提交的資料的功能,但由於副本之間採用非同步同步的方式,通常只能在 Primary 副本上才能讀到最新的已提交的資料