這篇文章以一種易於理解的方式來解釋 Multi-Paxos 的機制。算法
一種實現方式是用一組基礎 Paxos 實例,每條記錄都有一個獨立的 Paxos 實例,要想這麼作只須要爲每一個 Prepare 和 Accept 請求增長一個小標索引(index),用來選擇特定的記錄,全部的服務器爲日誌裏的每條記錄都保有獨立的狀態。安全
上圖展現了一個請求的完整週期。服務器
從客戶機開始,它向服務器發送所需執行的命令,它將命令發送至其中一臺服務器的 Paxos 模塊。網絡
這臺服務器運行 Paxos 協議,讓該條命令(shl)被選擇做爲日誌記錄裏的值。這須要它與其餘服務器之間進行通訊,讓全部的服務器都達成一致。併發
服務器等待全部以前的日誌被選定後,新的命令將被應用到狀態機。app
這時服務器將狀態機裏的結果返回給客戶端。性能
這是個基本機制,後面會做詳細介紹。3d
本頁介紹了 Multi-Paxos 所需解決的一些基本問題,讓 Multi-Paxos 在實際中得以正確運行。日誌
這裏須要注意的是,基礎 Paxos 已經有很是完整地描述,並且分析也證實它是正確的。很是易於理解。可是 Multi-Paxos 確不是這樣,它的描述很抽象,有不少選擇處理方式,沒有一個是很具體的描述。並且,Multi-Paxos 並無爲咱們詳細的描述它是如何解決這些問題的。code
這篇文章以一種易於理解的方式來解釋 Multi-Paxos 的機制。如今咱們尚未實現它,也尚未證實它的正確性,因此後續解釋可能會有 bug 。但但願這些內容能夠對解決問題、構建可用的 Multi-Paxos 協議有所幫助
第一個問題是當接收到客戶端請求時如何選擇日誌槽,咱們以上圖中的例子來闡述如何作到這點。假設咱們的集羣裏有三臺服務器,因此 「大多數」 指的是 2 。這裏展現了當客戶端發送命令(jmp)時,每臺服務器上日誌的狀態,客戶端但願這個請求值能在日誌中被記錄下來,並被狀態機執行。
當接收到請求時,服務器 S1 上的記錄可能處於不一樣的狀態,服務器知道有些記錄已經被選定(1-mov,2-add,6-ret),在後面我會介紹服務器是如何知道這些記錄已經被選定的。服務器上也有一些其餘的記錄(3-cmp),但此時它還不知道這條記錄已經被選定。在這個例子中,咱們能夠看到,實際上記錄(3-cmp)已經被選定了,由於在服務器 S3 上也有相同的記錄,只是 S1 和 S3 還不知道。還有空白記錄(4-,5-)沒有接受任何值,不過其餘服務器上相應的記錄可能已經有值了。
如今來看看發生些什麼:
當 jmp 請求到達 S1 後,它會找到第一個沒有被選定的記錄(3-cmp),而後它會試圖讓 jmp 做爲該記錄的選定值。爲了讓這個例子更具體一些,咱們假設服務器 S3 已經下線。因此 Paxos 協議在服務器 S1 和 S2 上運行,服務器 S1 會嘗試讓記錄 3 接受 jmp 值,它正好發現記錄 3 內已經有值(3-cmp),這時它會結束選擇並進行比較,S1 會向 S2 發送接受(3-cmp)的請求,S2 會接受 S1 上已經接受的 3-cmp 並完成選定。這時(3-cmp)變成粗體,不過還沒能完成客戶端的請求,因此咱們返回到第一步,從新進行選擇。找到當前尚未被選定的記錄(此次是記錄 4-),這時 S1 會發現 S2 相應記錄上已經存在接受值(4-sub),因此它會再次放棄 jmp ,並將 sub 值做爲它 4- 記錄的選定值。因此此時 S1 會再次返回到第一步,從新進行選擇,當前未被選定的記錄是 5- ,此次它會成功的選定 5-jmp ,由於 S1 沒有發現其餘服務器上有接受值。
當下一次接收到客戶端請求時,首先被查看的記錄會是 7- 。
在這種方式下,單個服務器能夠同時處理多個客戶端請求,也就是說前一個客戶端請求會找到記錄 3- ,下一個客戶端請求就會找到記錄 4- ,只要咱們爲不一樣的請求使用不一樣的記錄,它們都能以並行的方式獨立運行。不過,當進入到狀態機後,就必須以必定的順序來執行命令,命令必須與它們在日誌內的順序一致,也就是說只有當記錄 3- 完成執行後,才能執行記錄 4- 。
下一個須要解決的就是效率問題。在以前描述過的內容中存在兩個問題:
第一個問題就是當有多個 提議者(proposer) 同時工做時,仍然會有可能存在競爭衝突的狀況,有些請求會被要求從新開始,可能你們還會記得在 基礎 Paxos 裏介紹過的死鎖狀況。一樣的情況也能夠在這裏發生,當集羣壓力過大時,這個問題會很是明顯,若是有不少客戶端併發的請求集羣,全部的服務器都試圖在同一條記錄上進行值的選定,就可能會出現系統失效或系統超負荷的狀況。
第二個問題就是每次客戶端的請求都要求兩輪的遠程調用,第一輪是提議的準備(Prepare)請求階段,第二輪是提議的接受(Accept)請求階段。
爲了讓事情更有效率,這裏會作兩處調整。首先,咱們會安排單個服務器做爲活動的 提議者(proposer) ,全部的提議請求都會經過這個服務器來發起。咱們稱這個服務器爲 領導者(leader) 。其次,咱們有可能能夠消除幾乎全部的準備(Prepare)請求,爲了達到目的,咱們能夠爲 領導者(leader) 使用一輪提議準備(Prepare),可是準備的對象是完整的日誌,而不是單條記錄。一旦完成了準備(Prepare),它就能夠經過使用接受(Accept)請求,同時建立多條記錄。這樣就不須要屢次使用準備(Prepare)階段。這樣就能減小一半的 RPC 請求。
選舉領導者的方式有不少,這裏只介紹一種由 Leslie Lamport 建議的簡單方式。這個方式的想法是,由於服務器都有它們本身的 ID ,讓有最高 ID 的服務器做爲領導者。能夠經過每一個服務器按期(每 T ms)向其餘服務器發送心跳消息的方式來實現。這些消息包含發送服務器的 ID ,固然同時全部的服務器都會監控它們從其餘服務器處收到的心跳檢測,若是它們沒有能收到某一具備高 ID 的服務器的心跳消息,這個間隔(一般是 2T ms)須要設置的足夠長,讓消息有足夠的通訊傳遞時間。因此,若是這些服務器沒有能接收到高 ID 的服務器消息,而後它們會本身選舉成爲領導者。也就是說,首先它會從客戶端接受到請求,其次在 Paxos 協議中,它會同時扮演 提議者(proposer) 和 接受者(acceptor) 兩種角色。若是機器可以接收到來自高 ID 的服務器的心跳消息,它就不會做爲領導者,若是它接收到客戶端的請求,那麼它會拒絕這個請求,並告知客戶端與 領導者(leader) 進行通訊。另一件事是,非 領導者(leader) 服務器不會做爲 提議者(proposer) ,只會做爲 接受者(acceptor) 。這個機制的優點在於,它不太可能出現兩個 領導者(leader) 同時工做的狀況,即便這樣,若是出現了兩個 領導者(leader) ,Paxos 協議仍是能正常工做,只是否是那麼高效而已。
應該注意的是,實際上大多數系統都不會採用這種選舉方式,它們會採用基於租約的方式(lease based approach),這比上述介紹的機制要複雜的多,不過也有其優點。
另外一個提升效率的方式就是減小準備請求的 RPC 調用次數,咱們幾乎能夠擺脫全部的準備(Prepare)請求。爲了理解它的工做方式,讓咱們先來回憶一下爲何咱們須要準確請求(Prepare)。首先,咱們須要使用提議序號來阻止老的提議,其次,咱們使用準備階段來檢查已經被接受的值,這樣就可使用這些值來替代本來本身但願接受的值。
第一個問題是阻止全部的提議,咱們能夠經過改變提議序號的含義來解決這個問題,咱們將提議序號全局化,表明完整的日誌,而不是爲每一個日誌記錄都保留獨立的提議序號。這麼作要求咱們在完成一輪準備請求後,固然咱們知道這樣作會鎖住整個日誌,因此後續的日誌記錄不須要其餘的準備請求。
第二個問題有點討巧。由於在不一樣接受者的不一樣日誌記錄裏有不少接受值,爲了處理這些值,咱們擴展了準備請求的返回信息。和以前同樣,準備請求仍然返回 接受者(acceptor) 所接受的最高 ID 的提議,它只對當前記錄這麼作,不過除了這個, 接受者(acceptor) 會查看當前請求的後續日誌記錄,若是後續的日誌裏沒有接受值,它還會返回這些記錄的標誌位 noMoreAccepted 。
最終若是咱們使用了這種領導者選舉的機制,領導者會達到一個狀態,每一個 接受者(acceptor) 都返回 noMoreAccepted ,領導者知道全部它已接受的記錄。因此一旦達到這個狀態,對於單個 接受者(acceptor) 咱們不須要再向這些 接受者(acceptor) 發送準備請求,由於它們已經知道了日誌的狀態。
不只如此,一旦從集羣大多數 接受者(acceptor) 那得到 noMoreAccepted 返回值,咱們就不須要發送準備的 RPC 請求。也就是說, 領導者(leader) 能夠簡單地發送接受(Accept)請求,只須要一輪的 RPC 請求。這個過程會一直執行下去,惟一能改變的就是有其餘的服務器被選舉成了 領導者(leader) ,當前 領導者(leader) 的接受(Accept)請求會被拒絕,整個過程會從新開始。
這個問題的目的是讓全部的 接受者(acceptor) 都能徹底接受到日誌的最新信息。如今算法並無提供完整的信息。例如,日誌記錄可能沒有在全部的服務器上被完整複製,所選擇的值只是在大多數服務器上被接受。但咱們要保證的就是每條日誌記錄在每臺服務器上都被徹底複製。第二個問題是,如今只有 提議者(proposer) 知道某個已被選定的特定值,知道的方式是經過收到大多數 接受者(acceptor) 的響應,但其餘的服務器並不知道記錄是否已被選定。例如, 接受者(acceptor) 不知道它們存儲的記錄已被選定,因此咱們還想通知全部的服務器,讓它們知道已被選定的記錄。提供這種完整信息的一個緣由在於,它讓全部的服務器均可以將命令傳至它們的狀態機,而後經過這個狀態機執行這些命令。因此這些狀態機能夠和領導者服務器上的狀態機保持一致。若是我沒有這麼作,他們就沒有日誌記錄也不知道哪一個日誌記錄是被選定的,也就沒法在狀態機中執行這些命令。
下面會經過四步來解釋這個過程:
第一步,在咱們達成仲裁以前不會中止接受(Accept)請求的 RPC 。也就是說若是咱們知道大多數服務器已經選定了日誌記錄,那麼就能夠繼續在本地狀態機中執行命令,並返回給客戶端。可是在後臺會不斷重試這些 Accept RPC 直到得到全部服務器的應答,因爲這是後臺運行的,因此不會使系統變慢。這樣就能保證在本服務器上建立的記錄能同步到其餘服務器上,這樣也就提供了完整的複製。但這並無解決全部問題,由於也可能有其餘更早的日誌記錄在服務器崩潰前只有部分已複製,沒有被完整複製。
第二步,每臺服務器須要跟蹤每一個已知被選中的記錄,須要作到兩點:首先,若是服務器發現一條記錄被選定,它會爲這條記錄設置 acceptedProposal 值爲無窮大 ∞ 。這個標誌表示當前的提議已被選定,這個無窮大 ∞ 的意義在於,永遠不會再覆蓋掉這個已接受的提議,除非得到了另一個有更高 ID 的提議,因此使用無窮大 ∞ 能夠知道,這個提議再也不會被覆蓋掉。除此以外,每臺服務器還會保持一個 firstUnchosenIndex 值:這個值是表示未被標識選定的最小下標位置。這個也是已接受提議值不爲無窮大 ∞ 的最低日誌記錄
第三步, 提議者(proposer) 爲 接受者(acceptor) 提供已知被選定的記錄信息,它以捎帶的方式在接受請求中提供相關的信息。每條由 提議者(proposer) 發送給 接受者(acceptor) 的請求都包括首個未被選定值的下標索引位置 firstUnchosenIndex ,換句話說 接受者(acceptor) 如今知道全部記錄的提議序號低於這個值的都已經被選定,它能夠用這個來更新本身。爲了解釋這個問題,咱們用例子來進行說明,假設咱們有一個 接受者(acceptor) 裏的日誌如上圖所示。在它接收到接受請求以前,日誌的信息裏知道的提議序號爲 一、二、三、5 已經被標記爲了選定,記錄 四、6 有其餘提議序號,因此它們尚未被認定是已選定的。如今假設接收到接受請求
Accept(proposal=3.4, index=8, value=v, firstUnchosenIndex=7)
它的提議序號是 3.4 ,firstUnchosenIndex 的值爲 7 ,這也意味着在 提議者(proposer) 看來,全部 1 至 6 位的記錄都已經被選定, 接受者(acceptor) 使用這個信息來比較提議序號,以及日誌記錄裏全部已接受的提議序號,若是存在任意記錄具備相同的提議序號,那麼就會標記爲 接受者(acceptor) 。在這個例子中,日誌記錄 6 有匹配的提議序號 3.4 ,因此 接受者(acceptor) 會標記這條記錄爲已選定。之因此能這樣,是由於 接受者(acceptor) 知道相關信息。首先,由於 接受者(acceptor) 知道當前這個日誌記錄來自於發送接受消息的同一 提議者(proposer) ,咱們同時還知道記錄 6 已經被 提議者(proposer) 選定,並且咱們還知道, 提議者(proposer) 沒有比這個日誌裏更新的值,由於在日誌記錄裏已接受的提議序號值與 提議者(proposer) 發送的接受消息中的提議序號值相同,因此咱們知道這條記錄在選定範圍之內,它仍是咱們所能知道的, 提議者(proposer) 裏可能的最新值。因此它必定是一個選定的值。因此 接受者(acceptor) 能夠將這條記錄標記爲已選定的。由於同時咱們還接收到關於新記錄 8 的請求,因此在接收到接受消息以後,記錄 8 處提議序號值爲 3.4 。
這個機制沒法解決全部的問題。問題在於 接受者(acceptor) 可能會接收到來自於不一樣 提議者(proposer) 的某些日誌記錄,這裏記錄 4 可能來自於以前輪次的服務器 S5 ,不幸的是這種狀況下, 接受者(acceptor) 是沒法知道該記錄是否已被選定。它也多是一個已失效過期的值。咱們知道它已經被 提議者(proposer) 選定,可是它可能應該被另一個值所取代。因此還須要多作一步。
這一系列機制能夠保證最終,全部的服務器裏的日誌記錄均可以被選定,並且它們知道已被選定。在一般狀況下,是不會有額外開銷的,額外的開銷僅存在與領導者被切換的狀況,這個時間也很是短暫。
Multi-Paxos 第五個問題是客戶端如何與系統進行交互的。若是客戶端想要發送一條命令,它將命令發送給當前集羣的 領導者(leader) 。若是客戶端正好剛啓動,它並不知道哪一個服務器是做爲 領導者(leader) 的,這樣它會向任一服務器發送命令,若是服務器不是 領導者(leader) 它會返回一些信息,讓客戶端重試並向真正的 領導者(leader) 發送命令。一旦 領導者(leader) 收到消息, 領導者(leader) 會爲命令肯定選定值所處位置,在肯定以後,就會將這個命令傳遞給它本身的狀態機,一旦狀態機執行命令後,它就會將結果返回給客戶端。客戶端會一直向某一 領導者(leader) 發送命令,知道它沒法找到這個 領導者(leader) 爲止,例如, 領導者(leader) 可能會崩潰,此時客戶端的請求會發送超時,在此種狀況下,客戶端會隨便選擇任意隨機選取一臺服務器,並對命令進行重試,最終集羣會選擇一個新的 領導者(leader) 並重試請求,最終請求會成功獲得應答。
可是這個重試機制存在問題。
若是 領導者(leader) 已經成功執行了命令,在響應前的最後一秒崩潰了?這時客戶端會嘗試在新 領導者(leader) 下重試命令,這樣就可能會致使相同的命令被執行兩次,這是不容許發生的,咱們須要保證的是每一個命令都僅執行一次。爲了達到目的,客戶端須要爲每條命令提供一個惟一的 ID ,這個 ID 能夠是客戶端的 ID 以及一個序列號,這條記錄包含客戶端發送給服務器的信息,服務器會記錄這個 ID 以及命令的值,同時,當狀態機執行命令時,它會跟蹤最近的命令的信息,即最高 ID 序號,在它執行新命令以前,它會檢查命令是否已經被執行過,因此在 領導者(leader) 崩潰的狀況下,客戶端會以新的 領導者(leader) 來重試,新的 領導者(leader) 能夠看到全部已執行過的命令,包括舊 領導者(leader) 崩潰以前已執行的命令,這樣它就不會重複執行這條命令,只會返回首次執行的結果。
結果就是,只要客戶端不崩潰,就能得到 exactly once 的保證,每一個客戶端命令僅被集羣執行一次。若是客戶端出現崩潰,就處於 at most once 的狀況,也就是說客戶端的命令可能執行,也可能沒有執行。可是若是客戶端是活着的,這些命令只會執行一次。
最後的一個問題在配置變動的狀況。
這裏說的系統配置信息指的是參與共識性協議的服務器信息,一般也就是服務器的 ID ,服務器的網絡地址。這些配置的重要性在於,它決定了仲裁過程,當前仲裁的大多數表明什麼。若是咱們改變服務器數量,那麼結果也會發生變化。咱們有對配置提出變動的需求,例如若是服務器失敗了,咱們可能須要切換並替代這臺服務器,又或咱們須要改變仲裁的規模,咱們但願集羣更加可靠(好比從 5 臺服務器提高到 7 臺)。
這些變動須要很是當心,由於它會改變仲裁的規模。
另一點就是須要保證在任什麼時候候都不能出現兩個不重疊的多數派,這會致使同一日誌記錄選擇不一樣值的狀況。假設咱們將集羣內服務器從 3 臺提高到 5 臺時,在某些狀況下,有些服務器會相信舊的配置是有效的,有些服務器會認爲新配置是有效的。這可能會致使最上圖最左側的兩臺服務器還以舊的配置信息進行仲裁,選擇的值是(v1),而右側的三臺服務器認爲新配置是有效的,因此 3 臺服務器構成了大多數,這三臺服務器會選擇一個不一樣的值(v2)。這是咱們不但願發發生的。
Leslie Lamport 建議的 Paxos 配置變動方案是用日誌來管理這些變動,當前的配置做爲日誌的一條記錄存儲,並與其餘的日誌記錄一同被複制同步。因此上圖中 1-C一、3-C2 表示兩個配置信息,其餘的用來存儲普通的命令。這裏有趣的是,配置所使用每條記錄是由它的更早的記錄所決定的。這裏有一個系統參數 å 來決定這個更早是多早,假設 å 爲 3,咱們這裏有兩個配置相關的記錄,C1 存於記錄 1 中,C2 存於記錄 3 中,這也意味着 C1 在 3 條記錄內不會生效,也就是說,C1 從記錄 4 開始纔會生效。C2 從記錄 6 開始纔會生效。因此當咱們選擇記錄 一、二、3 時,生效的配置會是 C1 以前的那條配置,這裏咱們將其標記爲 C0 。這裏的 å 是在系統啓動時配置好的參數,這個參數能夠用來限制同時使用的配置信息,也就是說,咱們是沒法在 i+å 以前選擇使用記錄 i 中的配置的。由於咱們沒法知道哪些服務器使用哪些配置,也沒法知道大多數所表明的服務器數量。因此若是 å 的值很小時,整個過程是序列化的,每條記錄選擇的配置都是不一樣的,若是 å 爲 3 ,也就意味着同時有三條記錄可使用相同的配置,若是 å 大不少時,事情會變得更復雜,咱們須要長時間的等待,讓新的配置生效。也就是說若是 å 的值是 1000 時,咱們須要在 1000 個記錄以後才能等到這個配置生效。
參考來源:
2013 Paxos lecture, Diego Ongaro
Wiki: Byzantine fault tolerance