ZooKeeper 的選舉機制,你瞭解多少?
阿新 • • 發佈:2021-03-11
![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310161133991-1371384793.png)
本文作者:HelloGitHub-老荀
Hi,這裡是 HelloGitHub 推出的 HelloZooKeeper 系列,**免費開源、有趣、入門級的 ZooKeeper 教程**,面向有程式設計基礎的新手。 > 專案地址:https://github.com/HelloGitHub-Team/HelloZooKeeper 今天開始我們將繼續深入 ZK 選舉相關的知識 ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160405477-990254216.png) ## 一、選舉的基本規則 ZKr~這次我決定一反常態,先不講故事了~先得聊聊在 ZK 選舉中非常重要的一些東西。 ### 1.1 zxid zxid 就是我們[之前](https://mp.weixin.qq.com/s?__biz=MzA5MzYyNzQ0MQ==&mid=2247499173&idx=1&sn=8d09888533c4a75d2915720c1242ee13&chksm=905848fba72fc1ed38659069914313885499d89bd3259fcc95ef0604c8bbc1851f2cc31e85f7&token=638365590&lang=zh_CN#rd)提到的事務編號,是一個 8 位元組的整型數字,但是 ZK 設計的時候把這一個數字拆成了兩部分使用,一魚兩吃! 8 個位元組的整數一共有 64 位長度,前 32 位用來記錄 epoch,後 32 位就是用來計數。你可能要問了? epoch?是啥? ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160110413-802054477.png) zxid 初始化是 0,也就是這樣 ``` 00000000000000000000000000000000 00000000000000000000000000000000 ``` 每一次寫請求都會增加後 32 位,假設現在進行了 10 次寫請求(無論該請求有沒有真的修改到資料),zxid 就會變成這樣 ``` 00000000000000000000000000000000 00000000000000000000000000001010 ``` 當進行一次選舉的時候,前 32 位就會增加 1,並且清零後 32 位 ``` 00000000000000000000000000000001 00000000000000000000000000000000 ``` 除了選舉以外,當後 32 位徹底用完(變成全 1,也就是 ZK 正常執行了 2^32 - 1 次寫請求都沒進行過一次選舉,牛逼!)也會讓前 32 位增加 1,相當於進位 ``` # 進位前 00000000000000000000000000000000 11111111111111111111111111111111 # 進位後 00000000000000000000000000000001 00000000000000000000000000000000 ``` 到這裡我就可以回答大家前面的問題了,epoch 就是 zxid 前 32 位的這個數字,epoch 本身的翻譯是“紀元,時代”的意思,意味著更新換代,而 zxid 的後 32 位數字僅僅是寫請求的計數罷了 ### 1.2 myid 在之前的小故事裡,我給 ZK 的叢集中的各個節點都起了一個好記的名字(神特麼好記!)。但是 ZK 官方自己是如何給每一個叢集中的節點起名字的呢?用的就是 myid! ZK 的啟動配置 `zoo.cfg` 中有一項 `dataDir` 指定了資料存放的路徑(預設是 `/tmp/zookeeper`),在此路徑下新建一個文字檔案,命名為 `myid`, 文字內容就是一個數字,這個數字就是當前節點的 myid ``` /tmp └── zookeeper ├── myid └── ... ``` 然後在 `zoo.cfg` 是這樣配置叢集資訊 ``` server.1=zoo1:2888:3888 server.2=zoo2:2888:3888 server.3=zoo3:2888:3888 ``` 這個 `server.` 之後的數字就是 myid,這個 myid 在整個叢集中,各個節點之間是不能重複的。我忘記之前在哪兒看到的了,說是 myid 只能是 1 到 255 的數字,我一直信以為真,直到這次,我本著嚴謹的態度去做了實踐,一切以事實為主,並且我的實驗覆蓋了 3.4、3.5、3.6 三大版本(都是三臺機器的簡單叢集),結論是:myid 只要是不等於 -1 就行(-1 是一個固定的值會導致當前節點啟動報錯),不能大於 `Long.MAX_VALUE` 或者小於 `Long.MIN_VALUE`,但是如果在當前的節點中配置了 `zookeeper.extendedTypesEnabled=true` 那當前節點的最大 myid 是 254(負數不影響,我也不知道這個 254 的用意,但是程式碼中的確有判斷) 是不是奇怪的知識又增加了呢~ 關於配置更多的資訊,之後單獨再整理,今天就點到為止 ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160123775-1543807655.png) ## 1.3 選舉規則 知道了上面這些有什麼用呢?非常重要!因為選舉 Leader 完全看的就是這幾個值 - epoch - 寫請求次數 - myid 優先順序從上到下逐級比較,誰大誰就更有資格成為 Leader,當前級一樣就比較下一級,直到分出勝負為止!因為 myid 是不能重複的,所以最終是一定能分出勝負的! 好了,現在大家知道了最基本的選舉規則了~讓我們進入下一節吧 ## 二、三馬之爭 **馬果果**一定想不到,這輩子自己可以和兩位鼎鼎大名的明星企業家相提並論,讓我們一起去看看發生了什麼吧~ ### 2.1 準備開工 之前**馬果果**規定了三個辦事處在對外開張前必須選出一個 Leader,在正式開始選之前,每一個辦事處也有一些準備工作需要做: - 每一個辦事處必須得知道一共有多少個辦事處 - 額外聘請一些專門負責和其他辦事處溝通的話務員 - 準備好一個票箱用來對投票統計和歸票 - 為每一個辦事處設定一個固定的 myid 所以現在辦公室的佈置變成了這樣(我省略了之前章節的其他要素): ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160130215-1281237977.png) 有了這些準備工作以後所有辦事處都可以進入選舉的階段了,並且村委會規定了幾種狀態用於表示當前辦事處正處在的階段: - LOOKING,正在尋找 Leader,處於此階段的辦事處不能對外提供服務 - LEADING,當前辦事處就是 Leader,可以對外提供服務 - FOLLOWING,當前辦事處正在跟隨 Leader,可以對外提供服務 很明顯剛剛準備好的各個辦事處現在都處於 LOOKING 狀態,下面讓我們正式進入選舉流程吧 ### 2.2 開始選舉 由於各個辦事處剛準備好,所以彼此之間還沒有通過信,又加上大家都是姓馬的,心裡面都是想當老大的,所以每一個辦事都會率先擬一張寫著自己的選票發給其他辦事處。主要有這些資訊: - sid:我是誰 - leader:我選誰 - state:我當前的狀態 - epoch:我當前的 epoch - zxid:我選擇的 leader 的最大的事務編號 以**馬果果**舉例: ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160136806-1425672867.png) **馬小云**和**馬小騰**也一樣,一開始都選了自己做 Leader 候選人,並且都把自己認為的候選人(當前場景下就是自己)的票分別傳送給了其他兩位(以及自己) ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160143697-229187556.png) #### 2.2.1 馬果果視角 每個辦事處各自也會收到來自其他辦事處的選票(也有可能是自己的),每拿到一張選票,都需要和當前自己認為的 Leader 候選人做比較,理論上自己投給自己的選票會先一步達到自己的票箱,因為不需要經過通訊減少了傳輸的路徑,自己的選票和自己的候選人是一致的所以不需要比較,只需要在票箱中記上一筆,我們還是以**馬果果**舉例: ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160148987-1117284590.png) `=》`的左邊是辦事處的名字,右邊是該辦事處選的 Leader。當前投票統計是指,當前節點所選的 Leader 獲得的選票統計。 假設他再收到了**馬小云**的選票: - **馬果果**首先看到的是**馬小云**也處在 LOOKING 狀態 - 接著就會比較自己候選人和**馬小云**的選票(左邊代表當前辦事處的候選人,右邊代表收到的選票資訊,下同) ``` e:0 == e:0 z:0 == z:0 l: 馬果果(69) > l: 馬小云(56) ``` 最終因為**馬果果**的 myid 69 要比**馬小云**的 myid 56 要大,所以**馬果果**最終勝出!雖然**馬小云**勝出了,但是當前投票統計是不能修改的,因為**馬小云**這一輪的選票就是選的**馬小云**,需要等待他重新改票後再投才能修改投票統計。 之後會往投票箱記錄: ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160154148-1085544838.png) 緊接著是**馬小騰**的投票: ``` e:0 == e:0 z:0 == z:0 l: 馬果果(69) > l: 馬小騰(49) ``` **馬果果**還是勝出! 記錄投票箱: ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160203263-1308361773.png) 每次收到投票的時候,**馬果果**都會依據當前的投票統計進行歸票,但是很遺憾選舉仍然無法結束,因為結束的規則必須有某一個辦事處獲得半數以上的選票,現在只有一個**馬果果**自己的選票,不滿足半數以上,所以馬果果只能再等等了。 而在**馬果果**這邊忙的熱火朝天的同時,**馬小云**和**馬小騰**也在進行著同樣的動作。 #### 2.2.2 馬小云視角 我們這省略描述**馬小云**記錄自己選票的過程,假設他這邊是先收到**馬果果**的選票,是怎麼處理的呢? ``` e:0 == e:0 z:0 == z:0 l: 馬小云(56) < l: 馬果果(69) ``` **馬小云**看到自己認為的 Leader 候選人被**馬果果**的選票擊敗了,所以將自己的候選人改為**馬果果**,並將新的選票重新廣播出去 ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160211600-2065607458.png) 然後在自己的投票箱中記錄: ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160220081-924288759.png) 為了敘述的完整性,我們還是把**馬小騰**的票也看完 ``` e:0 == e:0 z:0 == z:0 l: 馬果果(69) > l: 馬小騰(49) ``` **馬果果**還是勝出了,所以**馬小云**的投票箱最終變成這樣: ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160225889-290050296.png) 講道理接下來應該以**馬小騰**為主視角,再講一遍剛才的過程,但是可以認為幾乎和**馬小云**是一樣的,為了故事的順暢,我們需要回到**馬果果**的視角,因為**馬小云**輸給**馬果果**之後改票了,又發了一輪選票 #### 2.2.3 馬果果視角(再) **馬果果**又再一次收到了**馬小云**的選票(改票後),投票箱就會改成這樣: ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160232914-877158720.png) 收到這個投票後,當前投票統計就會增加**馬小云**的記錄,然後**馬果果**進行歸票就發現了這次自己的選票超過半數了,然後會進行二次確認,會等待一會看看還能不能收到更新的選票,這裡假設沒有收到更新的投票,就會進行判斷,當前過半數的候選人是不是自己?如果是的話,那自己就是 Leader,不是的話,自己就是 Follower。 很明顯,**馬果果**就是 Leader,然後會把自己的狀態修改為 LEADING。 與此同時,**馬小云**、**馬小騰**也進行歸票,歸票結果自己為 Follower,把自己狀態修改為 FOLLOWING,然後各自都會和 Leader 進行資料的同步,同步完成之後整個辦事處就都可以對外提供服務了。 ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160239765-954424457.png) ### 2.3 馬小騰停電啦 選舉本身涉及到叢集間的通訊、節點自身的狀態管理和狀態變更,本身就是一個比較複雜的過程,剛才只是舉例了一個最簡單的啟動選舉流程,下面會舉更多的例子幫助大家能理解整個選舉的邏輯。 現在假設辦事處安然無恙得對外提供了一段時間服務後,**馬小騰**的辦事處突然停電了,就不能和另外兩馬進行通訊了,而另外兩馬在一段時間內都沒有收到過**馬小騰**的資訊的時候就知道,出事了!但是各自盤點了下目前仍然還有兩個辦事處可以對外提供服務,是達到整個叢集總數的半數以上的,是可以繼續讓村民們來辦理業務的,所以現在整個叢集變成了這樣: ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160245921-1491115981.png) 沒過一會,因為電力公司的積極搶修,**馬小騰**的辦事處恢復供電了,重新開張了,但是每一個辦事處在開張前都是處在 LOOKING 狀態的,還是會優先投票給自己,並會通過覆盤本地的存檔來得到自己辦事處最新的資料,假設**馬小騰**停電前是這樣: ``` e:0 z:21 l: 馬小騰(49) LOOKING ``` 他和之前一樣會給另外兩個辦事處發自己的選票 ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160252818-1771820532.png) 但和之前的情況不同,無論是**馬果果**還是**馬小云**他們現在都處在工作的狀態,收到了**馬小騰**的選票後就會把當前的 Leader 也就是**馬果果**的選票資訊以及自己當前的狀態傳送給他。 **馬果果**傳送的選票資訊: ``` e:0 z:30 l: 馬果果(69) LEADING ``` **馬小云**傳送的選票資訊: ``` e:0 z:30 l: 馬果果(69) FOLLOWING ``` **馬小騰**收到兩位的選票資訊後,知道了當前的 Leader 是**馬果果**,並且**馬果果**本人也確認了是 LEADING 狀態,就馬上把自己的狀態修改為了 FOLLOWING 狀態,並且會和之前一樣與 Leader 進行資料的同步,關於具體怎麼同步的,我打算留到之後再進行講解~ 同步之後,**馬小騰**的狀態變成了和**馬小云**一樣的了。 --- 我再假設這裡有一個平行世界,回到**馬小騰**剛恢復完供電準備開張上線的時候,此時的**馬小騰**的狀態假設是這樣的: ``` e:1 z:7 l: 馬小騰(49) LOOKING ``` 哪怕 epoch 比目前的 Leader 還要大,其實照道理是更有資格當 Leader,但是由於當前叢集中的其他辦事處已經有了一個明確的 Leader,**馬小騰**也只能忍辱負重(誰讓你停電了呢)還是以 Follower 的身份加入到叢集中來,並且仍然以當前 Leader 的資訊來同步,你也可以理解為降級(把自己的 epoch 降級回 0 ) 職場就是這麼殘忍,你稍微請個長假再回來可能已經是物是人非了~ ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160328853-390450109.gif) ### 2.4 馬果果又病啦 **馬果果**畢竟年事已高,又又又生病了,辦事處只能含淚關門,但是和上一次**馬小騰**停電不同,這次是作為 Leader 的**馬果果**停止服務了,因為之前定下的規定,整個辦事處叢集必須得有一個 Leader。現在**馬小云**和**馬小騰**發現 Leader 聯絡不上了,說明 Leader 無法服務了,他們就知道必須選出一個新的 Leader。於是紛紛將自己的狀態都修改為 LOOKING 狀態,並且再次把候選人選為自己,重新向其他仍然可以提供服務的辦事處廣播自己的選票(當前這個場景就是互相發選票了)。 ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160334674-1247360058.png) 無論誰收到選票後經過比較後都會知道是**馬小騰**勝出 ``` e:1 == e:1 z:77 < z:80 l: 馬小云(56) l: 馬小騰(49) ``` **馬小云**會把自己的候選人修改為**馬小騰**之後重新再把自己的選票發出去,現在**馬小騰**就獲得了 2 票通過,同時也滿足大於整個辦事處叢集半數以上,所以**馬小騰**和**馬小云**各自修改狀態為 LEADING 和 FOLLOWING 後,並且會和之前說的一樣,把 epoch 加 1 同時清空計數部分,最後重新恢復對村民提供服務。 而**馬果果**這邊病好以後,會重新開張和之前的例子一樣也是先從 LOOKING 狀態開始,最後會從其他兩馬那裡得知目前的 Leader 是**馬小騰**之後,就會主動和**馬小騰**同步資料並以 Follower 的身份加入到辦事處叢集中對外提供服務。 ### 2.5 招商引資 辦事處的熱火朝天被村委會看在了眼裡,心想只有三個辦事處就能達到這樣的效果,如果有更多的辦事處呢?於是和三馬商量了下,決定對外招商引入社會資本,讓他們自己按照現有模式建立新的辦事處,這樣村委會不用出一分錢,村民還能獲得實在的好處,秒啊! ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160340129-233576821.png) 此舉一度引來社會資本的大量關注,但是商量過後,三馬又覺得如果過多的引入外部力量勢必會削弱自己手中的權力,所以又出了一個規定,三馬自封為 Participant 只有他們三個才有資格進行 Leader 的競選,而引入的社會資本所建立的辦事處只能作為 Observer 加入辦事處的叢集中對外提供只讀服務,沒有資格競爭 Leader,這樣就可以在不增加選舉複雜程度的同時,提升整個辦事處叢集對讀請求的吞吐量。 要聲明當前節點是 Observer,需要在 `zoo.cfg` 中先配置 `peerType=observer` 同時宣告的叢集資訊最後要多加一個 `:observer` 用來標識,這樣其他節點也會知道當前 myid 為 1 和 2 都是 Observer ``` server.69=maguoguo:2888:3888 server.56=maxiaoyun:2888:3888 server.49=maxiaoteng:2888:3888 server.1=dongdong:2888:3888:observer server.2=jitaimei:2888:3888:observer ``` 而在 LOOKING 狀態的 Observer 一開始的 Leader 候選人也會選自己,但是選票資訊被設定成了這樣,以**東東**舉例: ``` e:Long.MIN_VALUE z:Long.MIN_VALUE l: 東東(1) LOOKING ``` 因為 epoch 被設定成了最小值所以這個選票等同於形同虛設,可以被直接忽略,並且在三馬那裡會維護一個 Participant 的列表,如果他們收到了來自 Participant 以外的辦事處的選票會直接選擇忽略,所以可以說 Observer 的選票對選舉結果是完全沒有影響的。最終是等待 Participant 之間的選舉結果通知,Observer 自身修改狀態為 OBSERVING,開始和 Leader 進行同步資料,這點和 Follower 沒區別,之後 Observer 和 Follower 會統稱為 Learner ### 2.6 小結 > - 競選 Leader 看的是 epoch、寫請求運算元、myid 三個欄位,依次比較誰大誰就更有資格成為 Leader > - 獲選超過半數以上的辦事處正式成為 Leader,修改自己狀態為 LEADING > - 其他 Participant 修改為 FOLLOWING,Observer 則修改為 OBSERVING > - 如果叢集中已經存在一個 Leader,其他辦事處如果中途加入的話,直接跟隨該 Leader 即可 > - 還得提一句,如果當前可提供服務的節點已經不足半數以上了,那麼這個選舉就永遠無法選出結果,每個節點都會一直處在 LOOKING 狀態,整個辦事處叢集也就無法對外提供服務了 ## 三、猿話一下 扯蛋扯完了,現在用咱的行話對有一些概念再深入一下。 首先我必須要說的是,故事裡的三馬,為了一定的節目效果,我描述成了三個角色,但是實際中 ZK 服務端是不會做這樣的區分的,都是相同的程式碼,根據不同的配置啟動,才有了執行時期 Leader、Follower、Observer 的角色之分,所以更貼近於實際的應該類似於火影裡的影分身或者龍珠裡的殘像拳之類的(好像混入了什麼奇怪的東西)。 ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160349127-1221664463.png) 我畫了下選舉的簡單流程圖: ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160405477-990254216.png) 其他地方我基本上都講過了,這裡再講下紅色部分,因為可能一些網路因素,發出去的選票對方卻沒收到,這個發起重新廣播投票就是為了能讓對方再重新發一次剛剛的選票。 同監聽客戶端 2181 埠不同的是,服務端叢集之間相互通訊,直接使用的是原生的 Socket 並沒有使用 NIO 或者是 Netty,因為服務端節點一共就這麼幾個而且針對每一個其他節點都會啟動一個執行緒去監聽,所以直接採用了這種比較原始的並且是阻塞的方式通訊,更簡單直接,而且假設對方服務不可用了的話, Socket 會直接報錯退出。 收發選票也是採用了 ZK 中非常常見的生產者-消費者模式,分別維護了兩個阻塞佇列,一個對應傳送出去的選票,一個對應收到的選票,各自使用一個子執行緒去輪詢該阻塞佇列。 之前的 ZK 是擁有 3 種選舉策略的,雖然另外兩種之前都是被廢棄的狀態,不建議使用,但是通過配置檔案還是可以強行使用的。不過在最新的 3.6.2 中另兩種策略直接從原始碼中刪除了,現在只有一種選舉的策略,原始碼中對應 `FastLeaderElection`,另外兩個我也沒研究過,就不展開了。 關於服務端之間的心跳檢測: - 服務端之間的心跳檢測(PING)是由 Leader 發起的,發向所有叢集中的其他節點 - Follower 收到 PING 後會回一個PING 給 Leader 並帶上自己這邊的客戶端會話資料 - 而 Leader 收到 Follower 的 PING 後,就會對這些客戶端進行會話連線 關於會話相關的知識點留到之後再說~ ## 四、總結 今天我們介紹了選舉的規則,以及舉例了一些選舉的場景並加以說明。為了介紹 Follower 或者 Observer 是如何在選舉完成之後和 Leader 同步資料的,下一篇我們會先介紹 ZK 是如何進行持久化的,期待一下吧,ZKr~ ![](https://img2020.cnblogs.com/blog/759200/202103/759200-20210310160415198-1871787617.png) 老規矩,如果你有任何對文章中的疑問也可以是建議或者是對 ZK 原理部分的疑問,歡迎來倉庫中提 issue 給我們,或者來語雀話題討論。 > 地址:https://www.yuque.com/kaixin1002/yla8hz