領導選舉是分散式系統中最棘手的事情之一。同時,理解 Leader 是如何選舉產生的以及leader的職責,是理解分散式系統的關鍵。
在分散式系統中, 通常一個服務由多個節點或例項組成服務叢集, 提供可擴充套件性、高可用的服務。
這些節點可以同時工作, 提升服務處理、計算能力,但是,如果這些節點同時操作共享資源時,那就必須要協調它們的操作,防止每個節點覆蓋其他節點所做的更改,從而產生資料錯亂的問題。
所以,我們需要在所有節點中選出一個領導者 Leader, 來管理、協調叢集的所有節點,這種也是最常見的 Master-Slave 架構。
在分散式環境中的多個節點中選取主節點 Leader,通常會使用以下幾種策略:
- 根據程式 Id 或者例項 Id,選擇最大值,或者最小值作為主節點。
- 實現一種常見的領導者選舉演算法,比如 Raft,Bully 等。
- 通過分散式互斥鎖,保證只有一段時間只有一個節點可以獲取到鎖,併成為主節點。
在本文中,我會介紹幾種常見的選舉演算法,包括 Raft、ZAB、Bully、Token Ring Election,當然上面的一些演算法中,領導選舉只是其中一部分的功能,像 Raft 其實是共識演算法, 所以,不要把他們的概念給搞混了。
Bully 演算法
Garcia-Monila 在 1982 年的一篇論文中發明了 Bully 演算法,這是分散式系統中很常見的選舉演算法,它的選舉原則是“長者”為大,也就是在所有存活的節點中,選取 ID 最大的節點作為主節點。
假如有一個叢集, 各個節點可以相互連線,並且每個節點都知道其他節點的資訊( Id 和節點地址),如下:
叢集初始化時,各個節點首先判斷自己是不是存活的節點中 ID 最大的,如果是,就向其他節點傳送 Victory 訊息,宣佈自己成為主節點,根據規則,此時,叢集中的 P6 節點成為 Master 主節點。
現在叢集中出現了一些故障,導致節點下線。如果下線的是從節點, 叢集還是一主多從,影響不大, 但是如果下線的是 P6 主節點,那就變成了一個群龍無首的場面。
現在我們需要重新選舉一個主節點!
我們的節點是可以相互連線,並且節點間定時進行心跳檢查, 此時 P3 節點檢測到 P6 主節點失敗,然後 P3 節點就發起了新的選舉。
首先 P3 會向比自己 ID 大的所有節點傳送 Election 訊息。
由於 P6 已經下線,請求無響應,而 P4,P5 可以接收到 Election 請求,並響應 Alive 訊息。P3 節點收到訊息後,停止選舉,因為現在有比自己 Id 大的節點存活,他們接管了選舉。
接下來,P4 節點向 P5 和 P6 節點傳送選舉訊息。
P5 節點響應 Alive 訊息,並接管選舉。
同樣,P5 節點向 P6 節點傳送選舉訊息。
此時,P6 節點沒有響應,而現在 P5 是存活節點中 ID 最大的節點,所以 P5 理所應當成為了新的主節點,並向其他節點傳送 Victory 訊息,宣佈自己成為 Leader !
一段時間後,故障恢復,P6 節點重新上線,因為自己是 ID 最大的節點, 所以直接向其他節點傳送 Victory 訊息,宣佈自己成為主節點,而 P5 收到比自己 ID 大的節點發起的選舉請求後,降級變成從節點。
Token Ring 選舉演算法
Token Ring Election 演算法和叢集節點的網路拓撲有較大關係,它的特點是,所有的節點組成一個環,而每個節點知道下游節點,並能與之通訊,如下
叢集初始化的時候,其中一個節點會向下一個節點先發起選舉訊息,其中包含了當前節點的 ID,下一個節點收到訊息後,會在訊息中附加上自己的 ID,然後繼續往下傳遞,最終形成閉環。
本次選舉從 P3 節點發起。
P3 節點收到 P4 的訊息後,發現訊息中包含自己的節點 ID,可以確定選舉訊息已經走了整個環,這時還是按照 “長者為大” 的原則,從訊息 "3,6,5,2,1,4" 中選取最大的 Id 為主節點,也就是選舉 P6 為 Leader。
接下來,P3 節點向下遊節點傳送訊息,宣佈 P6 是主節點,直到訊息走了整個環,回到 P3,至此,本次選舉完成。
現在叢集中出現了一些故障, 導致主節點 P6 下線,位於上游的 P3 節點首先發現了(通過心跳檢查),然後 P3 節點重新發起選舉,當下遊的 P3 節點無法連線時,會嘗試連線下游的下游節點 P5,傳送選舉訊息,並帶上自己的節點 Id,訊息逐步往下游傳遞。
直到選舉訊息重新回到 P3 節點,從 "3,5,2,1,4" 節點列表中選取最大的 ID,也就是現在 P5 成為主節點。
接下來,P3 節點向下遊節點傳送訊息,宣佈 P5 是主節點。
直到訊息走了整個環,回到 P3,至此,本次選舉完成。
Raft 共識演算法
Raft 是一個比較新的演算法, 它是史丹佛大學的 Diego Ongaro 和 John Ousterhout 開發的,並在 2014年發表論文, Raft 的設計就是為了讓大家能更好地理解並實現共識,因為它的前身,就是 Lesli Lamport 開發的大名鼎鼎的 Paxos 演算法,但是這個演算法非常難以理解和實現, 所以,Diego 的論文標題是 “尋找可理解的共識演算法”,更容易理解並且更容易實現,同時 Raft 也是分散式環境中使用最為廣泛的共識演算法。
Raft 的領導選舉屬於多數派投票選舉演算法,和其他選舉演算法的 "長者為大" 的原則不同, 它的原則是 "眾生平等",核心思想是 “少數服從多數”, 也就是說,Raft 演算法中,獲得投票最多的節點成為主。
在 Raft 的領導選舉中,定義了叢集中的節點有三個角色
- Leader 領導者,負責協調和管理其他節點
- Follower 領導的跟隨者
- Candidate 候選者,發起選舉投票,它是從 Follower 到 Leader 的過渡狀態
讓我們看看 Raft 中的領導選舉是怎樣執行的!
初始狀態下,叢集中有三個節點,Node A, B,C,它們現在都是 Follower
首先,每個節點隨機生成 150 毫秒到 300 毫秒之間的時間值, 也就是選舉超時(election timeout)設定, 並進行等待,超時後,節點會從 Follower 變成 Candidate , 下圖中 Node A 首先變成了 Candidate,給自己投了一票,並開始了新的任期 (Term)。
等等,什麼是任期 (Term)?在 Raft 中,它是一個數字編號,初始化為 0,當一個節點從 Follower 變成 Candidate 時,就把當前的任期編號加1,而從 Candidate 變成 Follower 時,任期編號不加也不減,可以理解它就是一個遞增計數器。
任期由每個節點在本地管理,同時,每次和其他節點通訊時會帶上它(任期號),如果接收到比自己大的任期號時,會更新自己的任期號到最新,如果接收到比自己小的任期號,就把自己的任期號返回,讓對方節點去更新。
任期有什麼用?
因為每次選舉任期都會累加,在時間上來說,一個小的任期一定是在一個大的任期之前產生的,所以也就可以通過任期的大小確定事件的發生順序,這樣在應對節點的選舉衝突時,就可以通過比較任期來推斷,兩次選舉發生的先後順序。
比如,由於網路分割槽問題,可能導致產生兩個 Leader,也就是我們常說的腦裂現象,如果兩個 Leader 同時處理資料,就會產生資料一致性問題,這種情況就可以比較任期號,比較小的就降級為 Follower 。
等等,不就是確定事件發生順序嗎?為什麼搞這麼麻煩?不應該用時間嗎?實際上,要保證分散式環境中多個節點的時間絕對一致可不是個簡單的事情,即使我們有 NTP 時間同步協議, 因為時間同步後就算只有幾毫米的誤差,也有可能會打亂事件的發生順序。
所以,Raft 中任期的機制就是解決分散式環境的事件順序問題,也就是 邏輯時鐘 ,或者在分散式中我們更普遍地稱為 epoch,在不使用物理時間的情況下,來確定事件的發生順序。
接下來,我們繼續看選舉過程,上面說到 節點 A 變成了 Candidate 候選者,給自己的任期 Term + 1 ,並投了自己一票,然後會向其他節點傳送投票訊息,如果接收節點在自己的任期內還沒有投過票,就會把票投給候選人 ,並重置自己的選舉超時 (election timeout), 注意,一個節點一個任期只能投一次,現在節點 A 有了 3票,符合 "過半原則", 現在節點 A 從Candidate 變成了 Leader,並向 Follower 節點定時傳送心跳,維持狀態,Follower 節點收到後,同樣也會重置自己的選舉超時 (election timeout)。
讓我們看看 Leader 節點下線會發生什麼?
節點 A 下線後,B 和 C 的選舉超時 (election timeout) 不會被重置,並且很快會超時,此時,節點 C 首先從 Follower 變成了 Candidate, 然後任期(Term)變成 2,投了自己一票,併傳送選舉訊息,節點 B 收到後,因為沒有投過票,就把票投了 節點C,現在節點 C 變成了 Leader。
我們上面說了,Raft 中的選舉超時是隨機的150毫秒到300毫秒,那就有一定的概率,兩個節點同時成為 Candidate,產生分裂選舉, 同時發起投票,並且獲得的同樣的票數,那怎麼辦呢?注意, 如果 Candidate 獲得的票沒有過半,就不會產生 Leader,那就重新選舉一次,直到票數過半,成為 Leader。
ZAB - ZooKeeper 的原子廣播協議
眾所周知,Apache ZooKeeper 是雲端計算的分散式框架, 它的核心是一個基於 Paxos 實現的原子廣播協議(ZooKeeper Atomic Broadcast),但實際上它既不是 Basic-Paxos 也不是 Multi-Paxos。
目前在 ZooKeeper 中有兩種領導選舉演算法:LeaderElection 和 FastLeaderElection(預設), 而 FastLeaderElection 選舉演算法是標準的 Fast-Paxos演算法實現。
下面我會介紹 ZAB 協議中的領導選舉的實現。
首先,我們有三個節點,S1,S2,S3 , 每個節點在本地都有一份資料和投票箱,資料包括myid, zxid 和 epoch。
- myid 每個節點初始化的時候需要配置自己的節點 Id,不重複的數字
- epoch 選舉的輪數,預設為0,選舉時做累加操作,和 Raft 中的任期是一樣的,也就是邏輯時鐘, epoch 的大小可以表示選舉的先後順序
- zxid ZooKeeper 的全域性事務Id, 64位不重複的數字,前 32 位是 epoch,後32位是 count 計數器, zxid 是怎麼做到全域性唯一的呢?實際上叢集選中 Leader 後,一個寫的操作,首先會統一在 Leader 節點遞增 zxid,然後同步到 Follower 節點,在一個節點上保證一個數字遞增並且不重複就簡單多了, zxid 的大小可以表示事件發生的先後順序。
現在開始投票,投票內容就是上面說的節點的本地資料,【myid,zxid,epoch】, 每個節點先給自己投一票,並放到自己的投票箱,然後把這張票廣播到其他節點。
一輪投票交換後,現在,每個節點的投票箱都有所有節點的投票。
根據投票箱裡的投票的節點資訊,進行競爭,規則如下:
首先會對比 zxid,zxid 最大的勝出(zxid越大,表示資料越新), 如果 zxid 相同,再比較 myid (也就是 節點的 serverId),myid 較大的則勝出, 然後更新自己的投票結果,再次向其他節點廣播自己更新後的投票 。
節點 S3: 根據競爭規則,勝出的票是 S3 自己,就無需更新本地投票和再次廣播。
節點 S1 和 S2: 根據競爭規則, 重新投票給 S3,覆蓋之前投給自己的票,再次把投票廣播出去。
注意,如果接收到同一個節點同一輪選舉的多次投票,那就用最後的投票覆蓋之前的投票。
此時,節點 S3 收到節點 S1和S2的重新投票,都是投給自己,符合 "過半原則",節點 S3 成為 Leader,而 S1 和 S2 變成 Follower, 同時 Leader 向 Follower 定時傳送心跳進行檢查。
總結
本文主要介紹了分散式系統中幾個經典的領導選舉演算法,Raft、ZAB、Bully、Token Ring Election, 選舉規則有的是 "長者為大",而有的是 "民主投票",少數服從多數, 大家可以對比他們的優勢和缺點,在實際應用中選擇合適的選舉演算法。
為什麼沒有介紹 Paxos 演算法呢?因為 Paxos 是共識演算法,而 Basic-Paxos 中,是不需要 Leader 節點即可達成共識,可謂 "眾生平等", 而在 Multi-Paxos 中提到的 Leader 概念,也僅僅是為了提高效率。當然 Paxos 是非常重要的,可以說它是分散式系統的根基。
下圖是 Paxos 演算法寫入資料時的模擬動畫
Reference
http://thesecretlivesofdata.com/raft/
https://www.cs.colostate.edu/~cs551/CourseNotes/Synchronization/BullyExample.html
Zab: High-performance broadcast for primary-backup systems
ZooKeeper’s atomic broadcast protocol: Theory and practice
全文完......