關於若干選舉算法的解釋與實現

分佈式中有這麼一個疑難問題,客戶端向一個分佈式集羣的服務端發出一系列更新數據的消息,由於分佈式集羣中的各個服務端節點是互爲同步數據的,所以運行完客戶端這系列消息指令後各服務端節點的數據應該是一致的,但由於網絡或其他原因,各個服務端節點接收到消息的序列可能不一致,最後導致各節點的數據不一致。要確保數據一致,需要選舉算法的支撐,這就引申出了今天我們要討論的題目,關於選舉算法的原理解釋及實現,選舉包括對機器的選舉,也包括對消息的選舉。

選舉算法

最簡單的選舉算法

如果你需要開發一個分佈式集羣系統,一般來說你都需要去實現一個選舉算法,選舉出Master節點,其他節點是Slave節點,爲了解決Master節點的單點問題,一般我們也會選舉出一個Master-HA節點。

這類選舉算法的實現可以採用本文後面介紹的Paxos算法,或者使用ZooKeeper組件來幫助進行分佈式協調管理,當然也有很多應用程序採用自己設計的簡單的選舉算法。這類型簡單的選舉算法可以依賴很多計算機硬件因素作爲選舉因子,比如IP地址、CPU核數、內存大小、自定義序列號等等,比如採用自定義序列號,我們假設每臺服務器利用組播方式獲取局域網內所有集羣分析相關的服務器的自定義序列號,以自定義序列號作爲優先級,如果接收到的自定義序列號比本地自定義序列號大,則退出競爭,最終選擇一臺自定義序列號最大的服務器作爲Leader服務器,其他服務器則作爲普通服務器。這種簡單的選舉算法沒有考慮到選舉過程中的異常情況,選舉產生後不會再對選舉結果有異議,這樣可能會出現序列號較小的機器被選定爲Master節點(有機器臨時脫離集羣),實現僞代碼如清單1所示。

清單1簡單選舉算法實現僞代碼

拜占庭問題

原始問題起源於東羅馬帝國(拜占庭帝國)。拜占庭帝國國土遼闊,爲了防禦目的,每支軍隊都分隔很遠,將軍之間只能依靠信差傳信。在戰爭的時候,拜占庭軍隊內所有司令和將軍必需達成一致的共識,決定是否有贏的機會纔去攻打敵人的陣營。但是,在軍隊內有可能存有叛徒和敵軍的間諜,左右將軍們的決定又擾亂整體軍隊的秩序。因此表決的結果並不一定能代表大多數人的意見。這時候,在已知有成員謀反的情況下,其餘忠誠的將軍在不受叛徒的影響下如何達成一致的協議,拜占庭問題就此形成。

拜占庭將軍問題實則是一個協議問題。一個可信的計算機系統必須容忍一個或多個部件的失效,失效的部件可能送出相互矛盾的信息給系統的其他部件。這正是目前網絡安全要面對的情況,如銀行交易安全、存款安全等。美國911恐怖襲擊發生之後,大家普遍認識到銀行的異地備份非常重要。紐約的一家銀行可以在東京、巴黎、蘇黎世設置異地備份,當某些點受到攻擊甚至破壞以後,可以保證賬目仍然不錯,得以復原和恢復。從技術的角度講,這是一個很困難的問題,因爲被攻擊的系統不但可能不作爲,而且可能進行破壞。國家的安全就更不必說了,對付這類故障的問題被抽象地表達爲拜占庭將軍問題。

解決拜占庭將軍問題的算法必須保證

A.所有忠誠的將軍必須基於相同的行動計劃做出決策;

B.少數叛徒不能使忠誠的將軍做出錯誤的計劃。

拜占庭問題的解決可能性

(1)叛徒數大於或等於1/3,拜占庭問題不可解

如果有三位將軍,一人是叛徒。當司令發進攻命令時,將軍3可能告訴將軍2,他收到的是“撤退”的命令。這時將軍2收到一個“進攻”的命令,一個“撤退”的命令,而無所適從。

如果司令是叛徒,他告訴將軍2“進攻”,將軍3“撤退”。當將軍3告訴將軍2,他收到“撤退”命令時,將軍2由於收到了司令“進攻”的命令,而無法與將軍3保持一致。

正由於上述原因,在三模冗餘系統中,如果允許一機有拜占庭故障,即叛徒數等於1/3,因而,拜占庭問題不可解。也就是說,三模冗餘對付不了拜占庭故障。三模冗餘只能容故障-凍結(fail-frost)那類的故障。就是說元件故障後,它就凍結在某一個狀態不動了。對付這類故障,用三模冗餘比較有效。

(2)用口頭信息,如果叛徒數少於1/3,拜占庭問題可解

這裏是在四模冗餘基礎上解決。在四模中有一個叛徒,叛徒數是少於1/3的。

拜占庭問題可解是指所有忠誠的將軍遵循同一命令。若司令是忠誠的,則所有忠誠將軍遵循其命令。我們可以給出一個多項式複雜性的算法來解這一問題。算法的中心思想很簡單,就是司令把命令發給每一位將軍,各將軍又將收到的司令的命令轉告給其他將軍,遞歸下去,最後用多數表決。例如,司令送一個命令v給所有將軍。若將軍3是叛徒,當他轉告給將軍2時命令可能變成x。但將軍2收到{v, v, x},多數表決以後仍爲v,忠誠的將軍可達成一致。如果司令是叛徒,他發給將軍們的命令可能互不相同,爲x, y, z。當副官們互相轉告司令發來的信息時,他們會發現,他們收到的都是{x,y,z},因而也取得了一致。

(3)用書寫信息,如果至少有2/3的將軍是忠誠的,拜占庭問題可解

所謂書寫信息,是指帶簽名的信息,即可認證的信息。它是在口頭信息的基礎上,增加兩個條件:

①忠誠司令的簽名不能僞造,內容修改可被檢測。

②任何人都可以識別司令的簽名,叛徒可以僞造叛徒司令的簽名。

一種已經給出的算法是接收者收到信息後,簽上自己的名字後再發給別人。由於書寫信息的保密性,可以證明,用書寫信息,如果至少有2/3的將軍是忠誠的,拜占庭問題可解。

例如,如果司令是叛徒,他發送“進攻”命令給將軍1,並帶有他的簽名0,發送“撤退”命令給將軍2,也帶簽名0。將軍們轉送時也帶了簽名。於是將軍1收到{“進攻”:0,“撤退”:0,2},說明司令發給自己的命令是“進攻”,而發給將軍2的命令是“撤退”,司令對我們發出了不同的命令。對將軍2同解。

Paxos算法

算法起源

Paxos算法是LesileLamport於1990年提出的一種基於消息傳遞且具有高度容錯特性的一致性算法,是目前公認的解決分佈式一致性問題最有效的算法之一。

在常見的分佈式系統中,總會發生諸如機器宕機或網絡異常等情況。Paxos算法需要解決的問題就是如何在一個可能發生上述異常的分佈式系統中,快速且正確地在集羣內部對某個數據的值達成一致,並且保證不論發生以上任何異常,都不會破壞整個系統的一致性。

爲了更加清晰概念,當client1、client2、client3分別發出消息指令A、B、C時,Server1~4由於網絡問題,接收到的消息序列就可能各不相同,這樣就可能由於消息序列的不同導致Server1~4上的數據不一致。對於這麼一個問題,在分佈式環境中很難通過像單機裏處理同步問題那麼簡單,而Paxos算法就是一種處理類似於以上數據不一致問題的方案。

Paxos算法是要在一堆消息中通過選舉,使得消息的接收者或者執行者能達成一致,按照一致的消息順序來執行。其實,以最簡單的想法來看,爲了達到所有人執行相同序列的指令,完全可以通過串行來做,比如在分佈式環境前加上一個FIFO隊列來接收所有指令,然後所有服務節點按照隊列裏的順序來執行。這個方法當然可以解決一致性問題,但它不符合分佈式特性,如果這個隊列出現異常這麼辦?而Paxos的高明之處就在於允許各個client互不影響地向服務端發指令,大夥按照選舉的方式達成一致,這種方式具有分佈式特性,容錯性更好。

Paxos規定了四種角色(Proposer,Acceptor,Learner,以及Client)和兩個階段(Promise和Accept)。

實現原理

Paxos算法的主要交互過程在Proposer和Acceptor之間。Proposer與Acceptor之間的交互主要有4類消息通信。

這4類消息對應於paxos算法的兩個階段4個過程:

階段1:

  1. a) proposer向網絡內超過半數的acceptor發送prepare消息;
  2. b) acceptor正常情況下回復promise消息。

階段2:

  1. a) 在有足夠多acceptor回覆promise消息時,proposer發送accept消息;
  2. b) 正常情況下acceptor回覆accepted消息。

Paxos算法的最大優點在於它的限制比較少,它允許各個角色在各個階段的失敗和重複執行,這也是分佈式環境下常有的事情,只要大夥按照規矩辦事即可,算法的本身保障了在錯誤發生時仍然得到一致的結果。

ZooKeeper ZAB協議

基本概念

ZooKeeper並沒有完全採用Paxos算法,而是使用了一種稱爲ZooKeeper Atomic Broadcast(ZAB,ZooKeeper原子消息廣播協議)的協議作爲其數據一致性的核心算法。

ZAB協議是爲分佈式協調服務ZooKeeper專門設計的一種支持崩潰恢復的原子廣播協議。ZAB協議最初並沒有要求其具有很好的擴展性,最初只是爲雅虎公司內部那些高吞吐量、低延遲、健壯、簡單的分佈式系統場景設計的。在ZooKeeper的官方文檔中也指出,ZAB協議並不像Paxos算法那樣,是一種通用的分佈式一致性算法,它是一種特別爲ZooKeeper設計的崩潰可恢復的原子消息廣播算法。

ZooKeeper使用一個單一的主進程來接收並處理客戶端的所有事務請求,並採用ZAB的原子廣播協議,將服務器數據的狀態變更以事務Proposal的形式廣播到所有的副本進程上去。ZAB協議的這個主備模型架構保證了同一時刻集羣中只能夠有一個主進程來廣播服務器的狀態變更,因此能夠很好地處理客戶端大量的併發請求。另一方面,考慮到在分佈式環境中,順序執行的一些狀態變更其前後會存在一定的依賴關係,有些狀態變更必須依賴於比它早生成的那些狀態變更,例如變更C需要依賴變更A和變更B。這樣的依賴關係也對ZAB協議提出了一個要求,即ZAB協議需要保證如果一個狀態變更已經被處理了,那麼所有其依賴的狀態變更都應該已經被提前處理掉了。最後,考慮到主進程在任何時候都有可能出現奔潰退出或重啓現象,因此,ZAB協議還需要做到在當前主進程出現上述異常情況的時候,依舊能夠工作。

清單4所示是ZooKeeper集羣啓動時選舉過程所打印的日誌,從裏面可以看出初始階段是LOOKING狀態,該節點在極短時間內就被選舉爲Leader節點。

清單4ZooKeeper集羣選舉日誌輸出

ZAB協議實現原理

ZAB協議的核心是定義了對於那些會改變ZooKeeper服務器數據狀態的事務請求的處理方式,即所有事務請求必須由一個全局唯一的服務器來協調處理,這樣的服務器被稱爲Leader服務器,而餘下的服務器則稱爲Follower服務器,ZooKeeper後來又引入了Observer服務器,主要是爲了解決集羣過大時衆多Follower服務器的投票耗時時間較長問題,這裏不做過多討論。Leader服務器負責將一個客戶端事務請求轉換成一個事務Proposal(提議),並將該Proposal分發給集羣中所有的Follower服務器。之後Leader服務器需要等待所有Follower服務器的反饋信息,一旦超過半數的Follower服務器進行了正確的反饋後,那麼Leader就會再次向所有的Follower服務器分發Commit消息,要求其將前一個Proposal進行提交。

支持模式

ZAB協議包括兩種基本的模式,分別是崩潰恢復和消息廣播。

當整個服務框架在啓動的過程中,或是當Leader服務器出現網絡中斷、崩潰退出與重啓等異同步之後,ZAB協議就會退出恢復模式。其中,所謂的狀態同步是指數據同步,用來保證集羣中存在過半的惡機器能夠和Leader服務器的數據狀態保持一致。通常情況下,ZAB協議會進入恢復模式並選舉產生新的Leader服務器。當選舉產生了新的Leader服務器,同時集羣中已經有過半的機器與該Leader服務器完成了狀態。在清單4所示選舉的基礎上,我們把Leader節點的進程手動關閉(kill -9 pid),隨即進入崩潰恢復模式,重新選舉Leader的過程日誌輸出如清單5所示。

清單5ZooKeeper重新集羣選舉日誌輸出

當集羣中已經有過半的Follower服務器完成了和Leader服務器的狀態同步,那麼整個服務框架就可以進入消息廣播模式了。當一臺同樣遵守ZAB協議的服務器啓動後加入到集羣中時,如果此時集羣中已經存在一個Leader服務器在負責進行消息廣播,那麼新加入的服務器就會自覺地進入數據恢復模式:找到Leader所在的服務器,並與其進行數據同步,然後一起參與到消息廣播流程中去。ZooKeeper設計成只允許唯一的一個Leader服務器來進行事務請求的處理。Leader服務器在接收到客戶端的事務請求後,會生成對應的事務提案併發起一輪廣播協議;而如果集羣中的其他機器接收到客戶端的事務請求,那麼這些非Leader服務器會首先將這個事務請求轉發給Leader服務器。

三個階段

整個ZAB協議主要包括消息廣播和崩潰恢復這兩個過程,進一步可以細分爲三個階段,分別是發現、同步和廣播階段。組成ZAB協議的每一個分佈式進程,會循環地執行這三個階段,我們將這樣一個循環稱爲一個主進程週期。

  • 階段一:發現

階段一主要就是Leader選舉過程,用於在多個分佈式進程中選舉出主進程,準Leader和Follower的工作流程分別如下。

1.Follower將自己最後接受的事務Proposal的epoch值發送給準Leader;

2.當接收到來自過半Follower的消息後,準Leader會生成消息給這些過半的Follower。關於這個epoch值e’,準Leader會從所有接收到的CEPOCH消息中選取出最大的epoch值,然後對其進行加1操作,即爲e’。

3.當Follower接收到來自準Leader的NEWEPOCH消息後,如果其檢測到當前的CEPOCH值小於e’,那麼就會將CEPOCH賦值爲e’,同時向這個準Leader反饋ACK消息。在這個反饋消息中,包含了當前該Follower的epoch CEPOCH(F p),以及該Follower的歷史事務Proposal集合:hf。

當Leader接收到來自過半Follower的確認消息ACK之後,Leader就會從這過半服務器中選取出一個Follower,並使用其作爲初始化事務集合Ie’。

ZooKeeper選舉算法執行流程圖如圖4所示。

圖4. ZooKeeper選舉算法流程圖

  • 階段二:同步

在完成發現流程之後,就進入了同步階段。在這一階段中,Leader和Follower的工作流程如下:

1.Leader會將e’和Ie’以NEWLEADER(e’,Ie’)消息的形式發送給所有Quorum中的Follower。

2.當Follower接收到來自Leader的NEWLEADER(e’,Ie’)消息後,如果Follower發現CEPOCH(F p)不等於e’,就直接進入下一輪循環,因爲此時Follower發現自己還在上一輪,或者更上輪,無法參與本輪的同步。

如果等於e’,那麼Follower就會執行事務應用操作。

最後,Follower會反饋給Leader,表明自己已經接受並處理了所有Ie’中的事務Proposal。

3.當Leader接收到來自過半Follower針對NEWLEADER(e’,Ie’)的反饋消息後,就會向所有的Follower發送commit消息。至此Leader完成階段二。

4.當Follower收到來自Leader的Commit消息後,就會依次處理並提交所有的Ie’中未處理的事務。至此Follower完成階段二。

新增的節點會從Leader節點同步最新的鏡像,日誌輸出如清單8所示。

清單6新增節點的同步信息日誌輸出

清單7新增節點時Leader節點的日誌輸出

  • 階段三:廣播

完成同步階段之後,ZAB協議就可以正式開始接收客戶端新的事務請求,並進行消息廣播流程。

1.Leader接收到客戶端新的事務請求後,會生成對應的事務Proposal,並根據ZXID的順序向所有Follower發送提案<e’,<v,z>>,其中epoch(z)=e’。

2.Follower根據消息接收到的先後次序來處理這些來自Leader的事務Proposal,並將他們追加到hf中去,之後再反饋給Leader。

3.當Leader接收到來自過半Follower針對事務Proposal<e’,<v,z>>的ACK消息後,就會發送Commit<e’,<v,z>>消息給所有的Follower,要求它們進行事務的提交。

4.當Follower接收到來自Leader的Commit<e’,<v,z>>消息後,就會開始提交事務Proposal<e’,<v,z>>。需要注意的是,此時該Follower必定已經提交了事務Proposal<v,z’>。

如清單6所示,新增一個節點,運行日誌輸出。

清單8新增一個Znode節點Leader節點日誌輸出

清單9新增一個Znode節點Follower節點日誌輸出

實現代碼

具體關於Leader節點的選舉程序代碼分析,請見本人的另一篇文章《Apache ZooKeeper服務啓動源碼解釋》。

  • 重新選舉Leader節點

如清單5所示,當手動關閉Leader節點後,原有的Follower節點會通過QuorumPeer對應的線程發現Leader節點出現異常,並開始重新選舉,線程代碼如清單10所示。

清單10QuorumPeer線程

所有的Follower節點都會進入到Follower類進行主節點的檢查,如清單11所示。

清單11Follower類

RecvWorker線程會繼續拋出Leader連接不上的錯誤。

經過一系列的SHUTDOWN操作後,退出了之前集羣正常時的線程,重新開始新的選舉,有進入了LOOKING狀態,首先通過QuorumPeer類的loadDataBase方法獲取最新的鏡像,然後在FastLeaderElection類內部,傳入自己的ZXID和MYID,按照選舉機制對比ZXID和MYID的方式,選舉出Leader節點,這個過程和初始選舉方式是一樣的。

  • 集羣穩定後新加入節點

集羣穩定後ZooKeeper在收到新加節點請求後,不會再次選舉Leader節點,會直接給該新增節點賦予FOLLOWER角色。然後通過清單11的代碼找到Leader節點的IP地址,然後通過獲取到的最新的EpochZxid,即最新的事務ID,調用方法syncWithLeader查找最新的投票通過的鏡像(Snap),如清單12所示。

清單12Learner類

清單13保存最新Snap

  • 新提交一個事務

當新提交一個事務,例如清單6所示是新增一個ZNODE,這時候會按照ZooKeeperServer->PrepRequestProcessor->FinalRequestProcessor->ZooKeeperServer->DataTree這個方式新增這個節點,最終由ZooKeeperServer類的submitRequest方法提交Proposal並完成。在這個提交Proposal的過程中,FOLLOWER節點也需要進行投票,如清單14所示。

清單14處理投票

ZAB與Paxos的區別

ZAB協議並不是Paxos算法的一個典型實現,在講解ZAB和Paxos之間的區別之間,我們首先來看下兩者的聯繫。

  • 兩者都存在一個類似於Leader進程的角色,由其負責協調多個Follower進程運行。
  • Leader進程都會等待超過半數的Follower做出正確的反饋後,纔會將一個提案進行提交。
  • 在ZAB協議中,每個Proposal中都包含了一個epoch值,用來代表當前的Leader週期,在Paxos算法中,同樣存在這樣的一個標識,只是名字變成了Ballot。

在Paxos算法中,一個新選舉產生的主進程會進行兩個階段的工作。第一階段被稱爲讀階段,在這個階段中,這個新的主進程會通過和所有其他進程進行通信的方式來收集上一個主進程提出的提案,並將它們提交。第二階段被稱爲寫階段,在這個階段,當前主進程開始提出它自己的提案。在Paxos算法設計的基礎上,ZAB協議額外添加了一個同步階段。在同步階斷之前,ZAB協議也存在一個和Paxos算法中的讀階段非常類似的過程,稱爲發現階段。在同步階段中,新的Leader會確保存在過半的Follower已經提交了之前Leader週期中的所有事務Proposal。這一同步階段的引入,能夠有效地保證Leader在新的週期中提出事務Proposal之前,所有的進程都已經完成了對之前所有事務Proposal的提交。一旦完成同步階段後,那麼ZAB就會執行和Paxos算法類似的寫階段。

總的來講,Paxos算法和ZAB協議的本質區別在於,兩者的設計目標不一樣。ZAB協議主要用於構建一個高可用的分佈式數據主備系統,例如ZooKeeper,而Paxos算法則是用於構建一個分佈式的一致性狀態機系統。

結束語

通常在分佈式系統中,構成一個集羣的每一臺機器都有自己的角色,最典型的集羣模式就是Master/Slave模式(主備模式)。在這種模式中,我們把能夠處理所有寫操作的機器稱爲Master機器,把所有通過異步複製方式獲取最新數據,並提供讀服務的機器稱爲Slave機器。在Paxos算法內部,引入了Proposer、Acceptor和Learner三種角色。而在ZooKeeper中,這些概念也做了改變,它沒有沿用傳統的Master/Slave概念,而是引入了Leader、Follower和Observer三種角色。本文通過對Paxos和ZooKeeper ZAB協議進行講解,讓讀者有一些基本的分佈式選舉算法方面的認識。

參考資源 (resources)

  • 參考ZooKeeper文檔首頁,瞭解IBM開發者論壇已經收錄的ZooKeeper文章。
  • 參考 ZooKeeper原理 文章,瞭解ZooKeeper的原理性中文文章。
  • 參考書籍《ZooKeeper Essential》作者作爲ZooKeeper社區的主要推動者,對於ZooKeeper技術有深入的介紹。
  • 參考書籍《從Paxos到ZooKeeper》,作者的書是國內第一本ZooKeeper書籍。
發佈了428 篇原創文章 · 獲贊 197 · 訪問量 82萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章