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

麥克周_杭州發表於2016-08-22

已經出版的《大話Java效能優化》請大家多多支援,《深入學習JVM&G1 GC》、《動手學習Apache ZooKeeper》2016年下半年出版。

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

選舉演算法

最簡單的選舉演算法

如果你需要開發一個分散式叢集系統,一般來說你都需要去實現一個選舉演算法,選舉出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書籍。

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

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

相關文章