目錄
前置
myid:機器在集羣中的編號
SID:Server ID,是一個數字,和myid一致,標識集羣中一臺機器的唯一標識。
ZXID:事務的編號
一、不同場景下的選舉
當一臺機器加入到集羣,該機器會發起Leader選舉。如果集羣中已經存在Leader服務器,則該機器會被告知Leader信息。該機器會退出Leader選舉狀態,連接Leader並和同步狀態;如果此時集羣中不存在Leader,則會進行Leader選舉。
從3.4.0以後,ZooKeeper只支持FastLeaderElection選舉算法。
1、服務啓動時期的Leader選舉
主要步驟如下:
1、初始投票
初始情況,每個機器並不瞭解其他機器的信息,所以每個機器都會將自己作爲候選人發起投票。投票的內容爲:(myid, ZXID)。每個機器都會將自己的投票情況發送給集羣中的其他所有集羣。
2、接收投票
機器在接收到其他機器的投票後,會首先檢查該投票的有效性:輪次是否正確(是否是本輪的投票),是否來自LOOKING狀態的機器。
3、處理投票
如果接收到的投票是有效的,那麼機器會將該投票和自己的投票進行對比:
1)對比ZXID,選擇ZXID大的服務器作爲Leader;
2)如果ZXID相同,對比myid,選擇myid大的服務器作爲Leader。
因爲是剛剛啓動,所以此時ZXID都是相同的,實際上就是在比較myid。如果接收到的投票在對比中獲勝,就更新自己的投票爲該投票信息,並重新發送更新後的投票信息。
4、統計投票
每次投票後,每個服務器都會對集羣中所有的投票情況進行統計。如果統計到此時有過半的機器接收到相同的投票信息,就會認爲被過半機器選舉的服務器是Leader機器。
5、變更狀態
一旦確定了服務器狀態,每個服務器就會更新自己的狀態:Follower更新爲FOLLOWING,Leader更新爲LEADING。
2、服務器運行期間的選舉
如果Leader服務器因爲服務崩潰、關閉、重啓、網絡問題等原因,導致和集羣中過半機器失去聯繫,此時這個集羣將暫時對外停止寫服務,並重新進入Leader選舉狀態。
此時的Leader選舉的步驟:
1、變更狀態
非Observer機器都會將自己的狀態從FOLLOWING變更爲LOOKING,進入選舉流程。
2、初始投票
跟服務啓動時間的Leader選舉相同。此時每個機器都會將自己作爲候選人進行投票,生成投票信息(myid, ZXID)並通知其他機器。
3、接收投票
跟服務啓動時間的Leader選舉相同。接收並校驗投票的有效性。
4、處理投票
跟服務啓動時間的Leader選舉相同。對比接收到的投票和自己的投票,如果需要就更新並重新發松投票信息。
5、統計投票
跟服務啓動時間的Leader選舉相同。統計集羣中的所有集羣的投票信息,判斷是否已經選出Leader。
6、變更投票
跟服務啓動時間的Leader選舉相同。Follower更新爲FOLLOWING,Leader更新爲LEADING。
二、算法細節
FastLeaderElection算法並不複雜,但是在實現過程中還是有一些細節方面的問題。
1、投票信息的數據結構
org.apache.zookeeper.server.quorum.Vote類用來表示投票信息。主要成員屬性:
屬性 |
說明 |
id |
被推薦的Leader的SID |
zxid |
被推舉Leader的ZXID |
electionEpoch |
邏輯時鐘,用來判斷是不是在同一個投票輪次;發起新一輪投票時+1 |
peerEpoch |
被推舉的Leader的epoch,即zxid的高32位,每次Leader選舉完成後遞增 |
state |
當前服務器的狀態,枚舉:LOOKING, FOLLOWING, LEADING, OBSERVING |
2、網絡IO:QuorumCnxManager
QuorumCnxManager用來負責服務器之間底層Leader選舉過程中的通信。
2.1 消息隊列
QuorumCnxManager中維護了一系列的隊列,用來存儲消息的發送器、待發送的消息、最後一次發送的消息、以及接收到的消息。
隊列如下:
類型 |
名稱 |
說明 |
ConcurrentHashMap<Long, SendWorker> |
senderWorkerMap |
key:其他服務的SID;value:消息的發送器,Thread的從超類 |
ConcurrentHashMap<Long, ArrayBlockingQueue<ByteBuffer>> |
queueSendMap |
key:其他服務的SID;value:待發送消息的隊列 |
ConcurrentHashMap<Long, ByteBuffer> |
lastMessageSent |
key:其他服務的SID;value:最後一次發送的消息 |
ArrayBlockingQueue<Message> |
recvQueue |
接收到的消息的隊列 |
2.2 連接建立
1)連接管理
爲了保證能進行服務啓動時期的投票,服務在啓動的時候就會啓動QuorumCnxManager,在服務器兩兩之間建立連接。在QuorumCnxManager的啓動過程中創建一個ServerSocket來監聽Leader選舉端口。
2)處理邏輯
這個監聽是通過Listener來實現的,這是Thread的超類,在run方法中通過兩層while循環,創建ServerSock而並不斷的監聽端口,一旦接收到消息,就會交給receiveConnection方法進行處理。
3)避免重複
爲了避免兩臺機器重複建立連接,zk要求只能SID大的服務主動發起連接建立。在處理連接的方法receiveConnection中,會對比目標服務和自己的SID,判斷是否接受連接請求。
4)消息發送
連接建立後,會通過SID來管理消息和消息發送器,參考上面的消息隊列。
2.3 消息接收和發送
1)消息接收
消息接收通過RecvWorker來實現。zk會爲每個遠程服務器分配一個單獨的RecvWorker,每個RecvWorker只需要不斷的從這個TCP連接中讀取消息,並保存到RecvQueue中。
2)消息發送
消息發送通過SendWorker來實現。zk會爲每個遠程服務器分配一個單獨的SendWorker,這個SendWorker只需要不斷的根據SID從queueSendMap中取出消息併發送,同時將這條消息根據SID放入lastMessageSent中。
需要注意的是,如果待發送消息隊列空了,此時SendWorker會從lastMessageSent中取出最後一條消息,然後不斷重複發送。這是爲了避免連接關閉導致的最後一條消息沒有被正確處理的情況。當然,接收端也會正確處理重複消息。
發送端發送最近一條消息(SendWorker的run方法):
三、算法核心
1、基本概念
外部投票:其他服務器發來的投票
內部投票:當前服務器自身的投票
選舉輪次:邏輯時鐘,即本次Leader選舉中的投票輪次
PK:對比內部投票和外部投票,看是否要變更內部投票
2、選票管理
zk通過QuorumCnxManager管理服務器之間的投票發送和接收。FastLeaderElection中定義一些組件,用於調用QuorumCnxManager來實現投票的管理:
sendqueue:選票發送隊列,保存待發送的隊列;
recvqueue:選票接收隊列。保存接收到的外部投票;
WorkerSender:選票發送器。不斷的從sendqueue中取出投票信息,併發送給QuorumCnxManager,存儲到queueSendMap。
WorkerReceiver:選票接收器。不斷從QuorumCnxManager中取出外部投票,並轉換成選票的類型Message,保存到revcQueue。
這裏需要注意的是,對對於選票接收器,接收到的選票輪次低於自身的輪次,則忽略該投票, 並將內部投票發出去;如果自身並不在LOOKING狀態,就忽略該投票,並以投票的形式發送Leader的信息;如果該投票來自於Observer,則直接忽略該信息,併發送內部投票。
選票管理的過程:
3、選舉流程
選舉流程通過FastLeaderElection中的lookForLeader方法實現,這也是FastLeaderElection算法的核心。
1、自增選舉輪次
在FastLeaderElection實現中,有一個logicalclock屬性,用於標識當前Leader的選舉輪次,ZooKeeper規定了所有有效的投票都必須在同一輪次中。ZooKeeper在開始新一輪的投票時,會首先對logicalclock進行自增操作。
2、初始化選票
在開始進行新一輪的投票之前,每個服務器都會首先初始化自己的選票。在初始階段,每個服務器都會推薦自己成爲Leader。初始化投票如下:
屬性 |
值 |
id |
當前服務器自身的SID |
zxid |
當前服務器最新的ZXID |
electionEpoch |
當前服務器的選舉輪次 |
peerEpoch |
被推薦的服務器的選舉輪次(當前服務器) |
state |
LOOKING |
3、發送初始化選票
在完成選票的初始化後,服務器就會發起第一次投票。ZooKeeper會將剛剛初始化好的選票放入sendqueue隊列中,由發送器WorkerSender負責發送出去。
4、接收外部投票
每臺服務器都會不斷地從recvqueue隊列中獲取外部投票。如果服務器發現無法獲取到任何外部投票,就會立即確認自己是否和集羣中其他服務器保持着有效連接。如果發現沒有建立連接,那麼就會馬上建立連接;如果已經建立了連接,那麼就再次發送自己當前的內部投票。
5、判斷選舉輪次
當發送完初始化選票之後,接下來就要開始處理外部投票了。在處理外部投票的時候,會根據選舉輪次來進行不同的處理。
1)外部投票的選舉輪次大於內部投票
如果服務器發現自己的選舉輪次已經落後於該外部投票對應服務器的選舉輪次,那麼就會立即更新自己的選舉輪次(logicalclock),並且清空所有已經收到的投票,然後使用初始化的投票來進行PK以確定是否變更內部投票,最終再將內部投票發送出去。
2)外部投票的選舉輪次小於內部投票
如果接收到的選票的選舉輪次落後於服務器自身的,那麼ZooKeeper就會直接忽略該外部投票,不做任何處理,並返回步驟4。
3)外部投票的選舉輪次和內部投票一致
這是絕大多數下的情況。此時會開啓選票PK。
6、選票PK
選票PK的目的是爲了確定當前服務器是否需要變更投票,主要從選舉輪次、ZXID和SID三個因素來考慮,依次判斷,符合任意一個條件就需要進行投票變更。具體如下:
1)如果外部投票被推舉Leader的選舉輪次大於內部投票,則進行投票變更;
2)如果選舉輪次一致,就對比兩者的ZXID。如果外部投票的ZXID大於內部投票,則進行投票變更;
3)如果兩者的ZXID一致,就對比兩者的SID。如果外部投票的SID大於內部投票,則進行投票變更。
7、變更投票
通過選票PK後,如果確定了外部投票優於內部投票,那麼就進行投票變更,使用外部投票的選票信息來覆蓋內部投票。變更完成後,再次將這個變更後的內部投票發送出去。
8、選票歸檔
無論是否進行了投票變更,都會將剛剛收到的外部投票放入選票集合recvset中進行歸檔。recvset用於記錄當前服務器在本輪次的Leader選舉中收到的所有外部投票。
9、統計投票
完成選票歸檔後,就可以開始統計投票。統計投票是爲了統計集羣中是否已經有過半的服務器認可了當前的內部投票。如果是,則終止投票;否則返回步驟4。
需要注意的是,當確認過半服務器認可投票後,仍然會等待一段時間,以判斷是否有更優的投票。如果有,用更優投票替代當前統計結果。
10、更新服務器狀態
統計投票後,如果確定可以終止投票,那麼就開始更新服務器狀態。服務器會首先判斷當前被過半服務器認可的投票所對應的Leader服務器是否是自己,如果是自己的話,那麼就會將自己的服務器狀態更新爲LEADING。如果自己不是被選舉產生的Leader的話,那麼就會根據具體情況來確定自己是FOLLOWING或是OBSERVING。
上述過程中,4~9是放在while循環中的,會經過多次循環,直到Leader選舉產生。