分佈式一致性協議三部曲-深入理解一致性協議Paxos

 在理解分析分佈式一致性協議前,我們必須先看下CAP理論

CAP

    CAP是指在一個分佈式系統中,一致性(Consistency)、可用性(Availability)、分區容錯性(Partition tolerance)這三個要素最多隻能同時實現兩點,不可能三者兼顧。

 

 

  • Consistency 一致性

一致性指“all nodes see the same data at the same time”,即更新操作成功並返回客戶端完成後,所有節點在同一時間的數據完全一致。等同於所有節點擁有數據的最新版本。

  • Availability 可用性

可用性指“Reads and writes always succeed”,即服務一直可用,而且是正常響應時間。

對於一個可用性的分佈式系統,每一個非故障的節點必須對每一個請求作出響應。如果不考慮一致性,這個是很好實現的,立即返回本地節點的數據即可,而不需要等到數據一致才返回。

  • Partition Tolerance 分區容忍性

        Tolerance也可以翻譯爲容錯,分區容忍性具體指“the system continues to operate despite arbitrary message loss or failure of part of the system”,即系統容忍網絡出現分區,分區之間網絡不可達的情況,分區容忍性和擴展性緊密相關,Partition Tolerance特指在遇到某節點或網絡分區故障的時候,仍然能夠對外提供滿足一致性和可用性的服務。

比如一個服務有3個節點,中國,美國,新加坡,如果數據只有一份,如果數據只在中國,那麼美國用戶訪問數據流程是“美國節點->中國節點->中國數據“來實現的。這時如果美國和中國之間的網絡斷/高延時了,即出現分區,美國用戶就沒法訪問到該數據。爲了解決這個問題,目前都是每個節點都有一份數據備份,即中國,美國,新加坡都有一份數據,這樣美國節點和中國節點哪怕臨時中斷,美國用戶還是可以訪問美國的數據。但是數據分處不同地區,需要同步,但同步有延時,數據可能不一致。要保障一致,那就必須等寫操作在所有節點完成纔讀,這個時間又是不確定的,又會帶來可用性問題,這就是C,A,P不可兼得即CAP理論。

  • 具體工程場景

    • 傳統數據庫都是假設不保證P的,因爲傳統數據庫都是單機或者很小的本地集羣,假設網絡不存在問題,出現問題手工修復。所以,損失分區容錯(P)只保證CA相當於就是一個單體應用,根本不是分佈式,其實在分佈式出現之前都是這麼搭系統,但是倘若這種系統的節點之一掛了,整個系統就直接宕掉了,不符合目前現實需求(高擴展容錯)。分佈式是要求單個節點故障(概率太高了)系統仍能完成運行。搭建分佈式就是間接要求必須保證P,即P是現實,那C和A就無法同時做到,需要在這兩者之間做平衡。

    • 像銀行系統,是通過損失可用性(A)來保障CP,銀行系統是內網,很少出現分區不可達故障狀態,一旦出現,不可達的節點對應的ATM就沒法使用,即變爲不可用。同時如果數據在各分區未達到一致,ATM也是Loading狀態即不可用。

    • 在互聯網實踐中,可用性又是極其重要的,因此大部分是通過損失一致性(C)來保障AP,當然也非完全犧牲一致性,使用弱一致性,即一定時間後一致的弱一致性,當數據還在同步時(WRITE之後),使用上一次的數據。

    下面我們就來分析討論今天的重點分佈式一致性算法, 在理解Paxos之前我們必須先了解2PC和3PC。

 

二階段提交協議(2PC)

 

協議詳情

 

 

 

改進

    相比一階段協議,分兩步可以讓事務先執行然後再提交,讓事務在提交前儘可能地完成所有能完成的工作,這樣,最後的提交階段將是耗時極短,耗時極短意味着操作失敗的可能性也就降低。

 

缺陷

  • 同步阻塞問題

    執行過程中,所有參與節點都是事務阻塞型的。當參與者佔有公共資源時,其他第三方節點訪問公共資源不得不處於阻塞狀態。

  • 單點故障

    由於協調者的重要性,一旦協調者發生故障,參與者會一直阻塞下去。尤其在第二階段,協調者發生故障,那麼所有的 參與者 還都處於鎖定事務資源的狀態中,而無法繼續完成事務操。儘管協調者掛掉後可以重新選舉一個協調者,但是無法解決因爲協調者宕機導致的參與者處於阻塞狀態的問題。

  • 數據不一致

    在P2A中,當協調者向參與者發送commit請求之後,發生了局部網絡異常或者在發送commit請求過程中協調者發生了故障,這會導致只有一部分參與者接受到commit請求。而在這部分參與者接到commit請求之後就會執行commit操作。但是其他部分未接到commit請求的機器則無法執行事務提交,於是整個分佈式系統便出現了數據不一致性的現象。

三階段提交(3PC)

協議詳情

改進

三階段提交針對兩階段提交做了改進:

  • 在第一階段和第二階段中插入一個準備階段,第一階段canCommit並不執行事務,這樣當第一階段失敗或者timeout時,不佔用事務資源不需要回滾(提高效率減少事務阻塞)。

  • 引入超時機制。在2PC中,只有協調者擁有超時機制,3PC同時在協調者和參與者中都引入超時機制, 主要是避免了參與者在長時間無法與協調者節點通訊(協調者掛掉了)的情況下,無法釋放資源的問題,因爲參與者自身擁有超時機制會在超時後主動提交釋放資源,降低了阻塞時間。由於有第一階段canCommit的yes纔會進入第三階段,因而該階段極大概率是commit而不是rollback,因而當協調者掛掉後,默認執行commit是最接近正確的行爲。

缺陷

    和2CP一樣,3CP還是可能會存在不一致性問題,比如,因而適合本地(無網絡情況)或者網絡非常好的情況。

Paxos

    二階段算法存在一個致命缺點,就是每個階段協調者需要等到所有參與者的ack, 即只要有一個節點有故障,就會導致無效甚至整個系統出現問題, 這個概率還是很高。因而Paxos算法來了,Paxos有個很特別的就是協調者(proposer)只需等到超過1/2(多數派)的節點同意而不是全部節點,這樣只有當1/2的節點同時出現故障整個系統纔會有問題,加上同時這個限定條件後,這個系統的故障概率是極低極低的。由於只需超過1/2節點同意就執行操作,很明顯不是所有節點都保持一致了,因而需要額外的邏輯來保障一致性,這個就是paxos的鎖定,即一旦一個值被多數節點同意並提交,該值就被鎖定了,剩下的少於1/2節點沒法修改該值,並能從其他節點獲取到這個設定的值,這個過程也叫learning過程,這個過程的proposer是learner角色。從這個角度我們可以看出參與者也可以是協調者,如果是數據同步過程,這個過程中參與者從協調者角色切換成了學習者。

協議詳情

  • 第一階段A

Proposer選擇一個提議編號round_n,向所有的Acceptor廣播Prepare(round_n)請求。

  • 第一階段B

Acceptor接收到Prepare(round_n)請求,若提議編號round_n比之前接收的Prepare請求都要大,則承諾將不會接收提議編號比round_n小的提議,並且帶上之前Accept的提議中編號小於n的最大的提議,否則不予理會。

  • 第二階段A

Proposer得到了多數Acceptor的承諾後,如果沒有發現有一個Acceptor接受過一個值,那麼向所有的Acceptor發起自己的值和提議編號round_n,即(round_n, my_value),否則,從所有接受過的值中選擇對應的提議編號最大的,作爲提議的值,提議編號仍然爲round_n,即(round_n, old_accepted_value)。

  • 第二階段B

Acceptor接收到提議後,如果該提議編號round_n不違反自己做過的承諾,則接受該提議並Ack Proposer。不違背承諾有兩個部分。

  • round_n >= prepare/accepted的round

  • 如果已經有accepted value

proxs的核心

  • 所有節點都是誠實的,即都按照規則行事

    一個提議只需被1/2半數以上節點同意(Promise),Proposer即可進行下一步。

  • 當一個提議被多數派接受(Accepted)後,這個提議對應的值被Chosen(選定),一旦有一個值被Chosen,那麼只要按照協議的規則繼續交互,後續被Chosen的值都是同一個值,也就是這個Chosen值被鎖定,所有節點只能獲取到這個值。

  • 所以一個值在Acceptor看來有3種狀態:未賦值,Acceptor, Choosen狀態

如何確定編號

    上面提到的編號round是具體一個人的round, 它不是全局的,全局的就存在同步問題。因而proposer的真正編號是nodeid 【+】 round, 具體的方案有如下幾種。

實例推演分析

    接下來我們來具體分析各種case,假設總共有5個節點

  • 【1-P1A】 Node1發起proposer, prepare(1),  【1-P1B】然後收到2個Promised{1, {null, null}}, 加上自己總數3>5*1/2

  • 【1-P2A】因而Node1發起Accept{1, value},  這時又分兩種情況

    • 【1-P2B】收到2個Node的Accepted{1, value}, 加上自己總數3>5*1/2, 這個value就被Chosen, 後續不管發起多少propose, 該值都會被不會被改變。其實目前只有Node1知道該值被Chosen了,如果客戶端訪問這個值,Node1是可以直接返回該值的。但是其他節點Node2~Node5並不知道該值有>1/2的節點Accepted, 因而並不認爲該值被Chosen, 因而如果有client訪問Node2節點該值,Node2是不能立即返回該值的,但Node2可通過發起一次Propose即prepare(2)【2-P1A】,    這時就有幾種情況

      • 【2-P1B-1】從其他節點收到過Promised{2, {1, value}},  然後會接下來會發起【2-P2A】 Accept{2, value},如果收到超過1/2節點的 Accepted{2, value} 【2-P2B】,表明該值已經被Chosen是確定狀態了,然後Node2就可以返回給客戶端了,這個過程就是Learn過程(也稱重確認)。這個過程發起的Accept動作會讓更多節點Accepted(value),即讓更多人知道該值,既是Learn也是傳播。

      • 【2-P1B-2】從其他節點未收到過Promised{2, {1, value}} (【1-P2B】中所有Accepted的節點都offline/timeout纔會出現這種case),  只收到Promised{2, {null, null}}, 由前面的協議可知,Promised{2, {null, null}}的數量不可能超過1/2s數量,因而這次Propose會失效,Node2會重新發起Propose(3), 重複前面的流程【2-P1A】,這時前面Accepted(1, value)中的任意節點online了,就會進入到【2-P1B-1】分支,然後Node2就Chosen了value, 直接返回給客戶端,這個過程也說明一個值被Chosen後,其他節點可能會有短暫的異常非同步狀態,但是最終還是會Chosen該值,是收斂的,專業詞彙叫活性。

    • 【1-P2B-2】收到1個Node(假設Node5)的Accepted{1, value}, 加上自己總數2<5*1/2, 未超過多數,這時儘管該值未被Chosen,但是該值被部分節點Accepted, 在下次propose時被Chosen的概率也是很大的, 因爲下次節點(假如Node2)發起新的propose, prepare(2) 【2-P1A】。

      • 【2-P1B-1】只要Node5在線,那麼Node2就會收到Node5的Promised(2, {1, value}}, 進而接受Node5已經Accepted的值value, 然後繼續發起Accept(2, value), 這一次value被Accepted的節點數就增大了,進而可能過半,進而被Chosen,這個過程充分體現了儘可能達成一致的規則,即儘可能使用上次多數人promised(表決)的結果, 然後盡力廣播這個表決結果讓更多的節點Accepted(執行),達到被chosen(確定)的結果。

      • 【2-P1B-2】Node5未在線,且Node2就會收到2個的Promised(2, {null, null}}, 超過多數,因而會發起Accept(2, value2),然後又有兩種情況。

      • 收到超過半數的Accept(2, value2), 那麼value2就被chosen確定了。

    • 未收到超過半數的Accept(2, value2), 那麼說明系統被分爲兩個小組存在分歧了,一個組支持value, 一組支持value2。如果Node1和Node2交替重試propose,  這個衝突甚至可能會一直持續,這個就是paxos裏的死鎖現象,即當有多個proposer交錯提交時,會導致大量節點不停的提交新的propose覆蓋未被Chosen的老propose,進而持續導致沒有任何一個propose達到chosen(確定)狀態。這個很明顯是因爲Paxos允許任何參與者都可以是proposer並提出Propose。

Paxos的落地

Multi-Poxis, Raft, Zab(中心化分佈式一致性協議)

    Paxos相對偏學術,是一種廣義抽象,理解和落地相對複雜,同時Accept和Learn階段存在不同程度的活鎖情況, 總的來說主要有如下缺點

  1. 由於有多個 Proposer,可能會出現活鎖

  2. Paxos只能確定一個值,無法用作數據的連續寫同步

  3. 提案的最終結果可能只有部分 Acceptor 知曉,不是每個節點都同步到Log被chosen確定這一信息。

因而工程落地上有不同的優化, 具體如下:

  • 活鎖優化

        因而在工程落地上有了針對具體場景添加約束來儘量降低活鎖概率的方案,比如Fast Paxos算法在此基礎上作了一些優化,通過選舉產生一個leader (領導者),只有leader才能提交proposer。選擇Leader的方法有很多,採用前面標準的Paxos協議也能選出Leader。但是在實際工程中,針對Leader這個可以優化,比如標準的Paxos協議prepare階段,優先級是按照round值的,由於在迭代的過程中round值會不停變化,會導致promise的返回值不停的變化,不利於收斂。如果我們把優先級規則從round變成節點序號node_id,那麼規則就趨向於收斂固定的。比如節點id越小的,優先級越高,那麼不論怎麼迭代,promise都是優先同意node_id=0的提議,也就是說能更快的進入到Accept階段。比如Multi-Paxos就是採用Basic-Paxios來選擇Leader。當然考慮到Leader選舉確定後(chosen後)需要最快速的同步到所有節點,且有更復雜的優先級規範,同時標準paxos協議偏複雜,其他工程實踐中很少使用這種優化的標準paxos協議來選取Leader。Leader選舉協議有如下Zookeeper的FastLeaderElecetion, Redis的哨兵模式哨兵leader選舉和集羣模式leader選舉, 我將單獨列一篇文章來詳細描述這些Leader選舉協議。

  • 多條數據記錄同步優化

        從上面的Paxos的介紹可以看出,Paxos只能寫入一條記錄,一旦被Chosen就不能被修改,當時我們現實情況是經常要連續不斷的寫入新數據的,Paxos如何解決呢?這個比較好解決,Paxos的一次提交只做一件事我們稱爲一個paxos實例,比如寫一條日誌Log1(delete id=1)。如果要寫多條日誌比如Log2(delete id=2),那就需新建一個paxos實例,實例和實例之間是完全獨立的,具體實現上,每個參與者可建立這樣的一個三維數據(instance_id, round, value),  每個instance_id的交互按照poxis協議的規範處理處理round和value值。由於Basic-Paxos並不假設一段時間內只有唯一的proposer,因此可能由集羣內的任意server發起redolog同步,因此不能由單一server維護instance_id,而是需要進行分佈式協商,爲不同的Log分配全局唯一且有序的instance_id(LogId)。同時Accept應答prepare消息和Accept消息前都要持久化本地Log,因而一次Log同步需要3次網絡交互加2次寫本地磁盤。並且在高併發的情況下,不同Log可能被分配到相同的LogId,這時最差可能會在accept階段纔會失敗重試。

        Multi-Paxos:在Paxos集羣中利用Basic-Paxos協議選舉唯一的leader,在leader有效期內所有的議案都只能由leader發起,這裏強化了協議的假設:即leader有效期內不會有其他server提出的議案。因此對於Log的同步過程,我們可以簡化掉產生LogId階段和prepare階段,而是由唯一的leader產生logID,然後直接執行accept,得到多數派確認即表示Log同步成功。事實上,Multi-Paxos協議並不假設全局必須只能有唯一的leader來生成日誌,它允許有多個“自認爲是leader的server”來併發生成日誌,這樣的場景即退化爲Basic-Paxos。

  • Learn優化(數據確認)

        如上一章節所知,讀取一個已經chosen的value也是需要執行一輪Paxos過程的(即Learn過程,也叫重確認),在實際工程中做數據恢復時,對每條日誌都執行一輪Paxos的代價過大,因此引入需要引入一種被成爲confirm的機制,即Leader(Proposer)持久化一條日誌,得到多數派的accept後,就再寫一條針對這條日誌的confirm日誌,表示這條日誌已經確認形成了多數派備份,在回放日誌時,判斷如果一條日誌有對應的confirm日誌,則可以直接讀取本地內容,而不需要再執行一輪Paxos。confirm日誌只要寫本地即可,不需要同步到備機(Follower),但是出於提示備機及時回放收到日誌的考慮(備機收到一條日誌後(日誌處於Accepted狀態)並不能立即回放,需要確認這條日誌已經形成多數派備份(Chosen狀態)才能回放)。當然,Leader也會批量的給備機同步confirm日誌。出於性能的考慮,confirm日誌往往是延遲的成批寫出去,因此仍然會出現部分日誌已經形成多數派備份,但是沒有對應的confirm日誌的情況,對於這些日誌,需要在恢復過程中進行重確認。

  • Leader模式"幽靈復現"問題

    Mutlti-Paxos下存在Leader切換情況,並且每個記錄(Paxos實例)是完全獨立的,因而是可能存在非順序確認的,當然同一個Leader下日誌是有順序的,這個有點像多線程併發執行,同一個線程裏的順序是確定,線程之間的執行順序誰知道呢?日誌非順序確認,就會出現客戶端開始訪問不到第7條記錄,但是能訪問到第20條記錄,然後後面7確認後,客戶端又能訪問到第7條日誌了。具體示例如下:

  • 第一輪中A被選爲 Leader,寫下了 1-10 號日誌,其中 1-5 號日誌形成了多數派,並且已給客戶端應答,而對於 6-10 號日誌,客戶端超時未能得到應答。

  • 第二輪,A 宕機,B 被選爲 Leader,由於 B 和 C 的最大的 LogID 都是 5,因此 B 不會去重確認 6 - 10 號日誌,而是從 6 開始寫新的日誌,此時如果客戶端來查詢的話,是查詢不到上一輪 6 - 10 號 日誌內容的,此後第二輪又寫入了 6 - 20 號日誌,但是隻有 6 號和 20 號日誌在多數派。

  • 第三輪,A 又被選爲 Leader,從多數派中可以得到最大 LogID 爲 20,因此要將 7 - 20 號日誌執行重確認,其中就包括了 A 上的 7-10 號日誌,之後客戶端再來查詢的話,會發現上次查詢不到的 7 - 10 號日誌又像幽靈一樣重新出現了。

     怎麼解決“幽靈復現”問題?新任 Leader 在完成日誌重確認,開始寫入新的 Log 之前,先寫出一條被稱爲 StartWorking 的日誌,這條日誌的內容中記錄了當前 Leader 的 EpochID( 可以使用 Proposalid 的值),並且 Leader 每寫一條日誌都在日誌內容中攜帶現任 Leader 的 EpochID。回放時,經過了一條 StartWorking 日誌之後,再遇到 EpochID 比它小的日誌,就直接忽略掉,比如按照上面例子畫出的這張圖,7 - 19 號日誌要在回放時被忽略掉。

    Multi-Paxos就是使用上述幾個優化的Paxos算法, ZAB和Raft都是衍生自Multi-Paxos,由於Multi-Paxos提出的Leader概念,ZAB和Raft就強化Leader視角,以Leader這個前提和現實需求來設計實現,本質上都是Paxos基礎理論來保障一致性的。這三個協議的不同主要體現在下面幾點

  • Leader選舉方式不同

    • Mutil-Paxos採用Base-Paxos選舉

    • Raft廣播式互相計數方式

    • raft是選自己,節點發起投票,大家根據規則選擇投與不投,看自己的票數是否成爲Leader

  • raft集成了成員管理,這個比較精妙

  • Raft, zab能保障日誌連續性

  • raft增加了committed狀態,這個在paxos裏沒有具體說

  • raft給你的基本是僞代碼的描述,方便實現

PBFT(去中心分佈式一致性協議)

    中心化一致性協議有一個重要的前提是參與者不作惡,因爲參與者都是由一個實體管理,參與者都是不會違背協議的規範。而去中心化就不一樣,參與者(節點機器)分散在各地,參與者是有可能故意不遵守規範的,比如Paxos中,如果Proposer在沒有收到多數派的promise反饋仍發起Accept消息,就會導致該節點有無上權力,可以更新已經被Chosen的數據, 完全違背了規範。那是不是就不存在去中心化一致性協議呢?其實很簡單,去中心化的情況下節點作惡會造成影響的原因是參與者沒法驗證Proposer是否在遵守規則,如果每個節點的回覆都加上簽名就能解決這個問題。比如每個參與者在回覆promise時都通過自己的私鑰對promise回覆簽名,那麼一個Proposer如果沒有收到多數派的promise"同意",Proposer就沒法拿到多數派的"同意"簽名,哪怕它違背規範發起Accept,其他的參與者也會因爲它沒有多數派的簽名而不響應,也就是你可以作惡但是沒用。

    那在去中心化的分佈式系統中,如果Paxos要能容忍f個故障節點和f個作惡節點,多數派要爲多少?故障節點收到通信後不會返回結果,作惡節點收到通信後會返回錯誤的結果。

    假設節點總數是n,其中故障節點有f,那麼剩下的正確節點爲n- f,意味着只要收到n - f個消息就能做出決定,但是這n - f個消息有可能有f個是由作惡節點冒充的,那麼正確的消息就是n-f-f個,爲了多數一致,正確消息必須佔多數,也就是n - f - f > f 但是節點必須是整數個,所以n最少是3f+1個。所以在去中心化場景下,Paxos的多數派爲n-1/3f +1, 即收到2/3*n+1個節點的消息即可做出決定,可見這個節點數比中心化時的1/2*n + 1要嚴格不少。接下來我們規範化中心化paxos和去中心化下poxis的流程來對比下這種的區別。

  • 標準Paxos交互圖

 

    Basic-paxos, Proposer在接收到多數派的Accepted消息後知道該值已經被chosen(confirm不可變更), 此時proposer已經可以回覆Client, 但是其他節點不知道該值已經被confirm, 仍然不能reply客戶端,需要通過一次propose過來來確認值。

  • 帶confirm的paxios交互圖

        上面我們提到過,basic-poxis, 在proposer收到多數派的accepted後,也只有proposer知道該值被chosen(confirm),其他節點要確定這個值,需要通過一次propose過來來確認值, 因此raft等協議增加一個confirm交互來向其他節點廣告該值已經被confirm, 從而避免複雜的Propose交互,也能加快reply,  PBFT也是類似的。

 

  • PBFT

            從上面來看,一次提議需要5次交互,在中心化系統中,節點之間有專線連接,時延一般較低,可能還好。但在分佈式系統中,節點之間可能分佈在不同的個人設備上,時延往往較大,因而需要減少交互次數。PBFT實現上就是通過廣播來減少交互次數的,即Acceptor不僅僅和proposer交互還和其他Acceptor交互。Basic-Paxos下,Accepter接收到的Accept消息是Proposer統計promise消息發出來的,在PBFT下,Acceptor之間互相廣播promise消息,因而自己就可以統計自己收集到的promise消息,如果超過多數promise消息就可以直接accept併發出Accepted消息。

 

 

優化後的交互圖

 

    當然在PBFT裏,這些交互過程名稱有所變化。propose叫做pre-prepare, promise叫做prepare, accepted叫做commit, 當節點收到2/3*n+1各節點的commit即認爲數據被confirm了,可以直接reply。

 

 

    而區塊鏈TenderMint裏的PBFT協議裏各個階段的名字又不一樣,propose這個名字是一樣的,promise對應prevote, accepted對應precommit, 節點等到2/3*n+1個節點的precommit即認爲數據被chosen(confirm), 執行commit將數據寫入到區塊裏。

 

 

分佈式一致性協議算是一個比較複雜的系統,需要多思考討論,歡迎文章下面留言討論


系列文章:

分佈式一致性協議三部曲-深入理解一致性協議

分佈式一致性協議三部曲-從paxos幽靈復現看Raft實現原理

分佈式一致性協議三部曲-PBFT源碼分析

所有的碼客文章,都在這裏,歡迎長按關注

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