zookeeper叢集中,隻有一個節點是leader節點,其它節點都是follower節點(實際上還有observer節點,不參與選舉投票,在這裡我們先忽略,下同)。所有更新操作,必須經過leader節點,leader節點和follower節點之間保持着資料同步和心跳。
用戶端使用zookeeper時,可能會連到follower身份的server上,也可能會連到leader身份的server上。
三類角色分工如下:
leader:處理寫請求,單點
follower:處理用戶端請求,參與投票
observer:不參與leader選舉投票,隻處理用戶端請求
在一個zookeeper叢集裡,有多少個server是固定的,每個節點有一個唯一id,辨別它自己,另外,每個server還有用于選舉的ip和port,這些都在配置檔案中。一個具體的例子如下:
server.1= server1的ip位址:2888:3888
server.2= server2的ip位址:2888:3888
server.2= server3的ip位址:2888:3888
這裡有3個server,其id分别為1、2、3。2888為節點和leader交換資訊的端口,3888為選舉端口。這個節點的id,在投票時,使用者辨別參加競選的節點的身份。
<b>問題:這個leader節點是怎麼确定的?</b>
答案:zookeeper系統自己選舉出來的,所有server節點(observer除外),都參與這個選舉。這樣做的好處是:當現在leader挂掉了之後,系統可以重新選舉一個節點做leader。
zookeeper的選舉算法能保證:隻要超過半數節點還活着,就一定能選舉出唯一個一個節點作為leader。
當任何一個節點進入looking狀态時,選舉開始,進入looking狀态有如下原因:
1、節點剛啟動,使自己進入選舉狀态
2、發現leader節點挂掉了
zookeeper中的leader怎麼知道follower還活着?follower怎麼知道leader還活着?leader會定時向follower發ping消息;follower會定時向leader發ping消息。當發現無法ping通leader時,就會将自己的狀态改為looking,并發起新一輪選舉。處于選舉模式時,zookeeper服務不可用。
一個節點要成為leader,必須得到至少n/2+1(即半數以上節點)投票,實際上,在實作時,還可以考慮其它規則,比如節點權重。
為什麼要保證至少n/2+1的節點同意?因為這樣能保證本節點得到多數派的支援。因為每一個節點,隻能支援一個節點成為leader,是以,隻要一個節點獲得至少n/2+1選票,就一定會比其它任何節點得到的選票多。
這個規則意味着,如果超過半數以上的節點挂掉,zookeeper是選舉不出leader節點的,是以,zookeeper叢集最多允許n/2節點故障。
選舉算法目标是確定一定要選出一個唯一的leader節點。這有兩層含義:
1、一定要選出一個節點作為leader
2、這個leader一定要唯一
為此,要解決如下問題:
1、在一次選舉中,節點應該把票投給誰?
規則:每個節點有一個唯一id,在選舉中,節點總是把票投給id最大的那個節點,這樣,id大的節點更有可能成為leader,天生就是做上司的料。
2、在一次選舉過程中,有些節點由于沒有啟動而沒參加(有些人去國外了,沒有趕上這次大選,當他回國後,進入looking狀态,要發起選舉,怎麼辦?),後來這個節點啟動了,此時要求選舉,怎麼解決?
3、運作過程中,leader節點挂掉了,怎麼辦?
此時其它節點會發現leader挂了,會發起新一輪選舉,最後選出新leader。
1、直接指定一個節點做leader,例如,永遠都讓id最大節點當leader,這個想法最簡單。問題:這個節點挂了怎麼辦?這會出現單點問題。
2、每次選舉中,讓活着節點中,id最大節點當leader。問題:1、其它節點怎麼知道活着節點中,誰id最大?
選舉開始時,每個節點為自己生成一張投票,推薦自己成為leader,并把投票發送給其它節點,這相當于paxos算法中的proposer角色。接下來,節點啟動一個接收線程接收其它節點發送過來的投票,并對選票進行處理,這相當于paxos中的acceptor角色。簡單說,節點之間通過這種消息發送(投票),最終選舉出leader。
當收到其他它節點的選票之後,會和自己的投票比較,如果比自己的投票好(比如推薦的leader的id更大,選舉輪數更新),則更新自己的選票,接下來把收到的選票放在選票清單裡(該清單存儲了所有節點的投票,是一個key-value結構,key為節點的id,value為該節點的投票)。并再次把自己的投票發送給其它節點。
接下來節點會統計選票清單中每個節點獲得的票數,如果有一個節點獲得超過半數的選票,則認為該節點是leader。如果本節點就是,則将自身的狀态置為leading,表明自己是leader;否則将自己的狀态置為following,表明自己是follower。
通過若幹輪的消息交換,最終,會有一個節點獲得超過一半的選票而成為leader。這種方法的精髓在于,每個節點在不需要獲得所有節點的資訊(投票結果)的前提下,達成一緻意見,選出leader。
volatile long
logicalclock;
表示選舉輪數,在lookforleader開始的時候會加1,,另外,在收到其他節點的投票資訊時,如果其它節點的electionepoch比本值大,本值會被賦成electionepoch。也就是說,每次節點啟動時,該值為0?這個值隻在節點存活的時候有意義?即節點重新開機後,該值為0。
long proposedleader;
該值為本節點推薦的leader的id,初始時為自己,後面會更新,這個值不會從檔案中讀,也就是說,重新開機後會自動使用本節點id。getinitid源代碼如下:
public
long getid() {
return myid;
}
long proposedzxid;
本節點建議的zxid,在starter函數中,被初始化為-1;在updateproposal函數中,會更新該變量的值。
updateproposal(getinitid(),
getinitlastloggedzxid(), getpeerepoch());
private
long getinitlastloggedzxid(){
if(self.getlearnertype() == learnertype.participant)
return self.getlastloggedzxid();
else return long.min_value;
}
public long
getlastloggedzxid() {
if (!zkdb.isinitialized()) {
loaddatabase();
}
return
zkdb.getdatatreelastprocessedzxid();
long proposedepoch;
表示本節點推薦的選舉輪數,在updateproposal函數更新選票時,會更新該值。節點啟動初始化時候,第一次調用updateproposal,會把proposedepoch的值賦為getpeerepoch,而該函數又會調用getcurrentepoch,getcurrentepoch的代碼如下:
long getcurrentepoch() throws ioexception {
if
(currentepoch == -1) {
currentepoch
= readlongfromfile(current_epoch_filename);
}
return
currentepoch;
}
這表明,該值會從日志檔案中讀出來。也就是說,節點重新開機後,會使用上次活着的時候的值。
為什麼有了zxid還需要epoch?zxid是用來表示資料的新舊,而epoch是用來表示選舉的輪數。
假設zookeeper叢集中有3個節點,其id分别為1、2、3。整個叢集開始運作時,每個節點zxid都為1。
節點1、2、3啟動後,都進入looking狀态,開始leader選舉。每個節點的proposedleader即推薦的leader都是自己;logicalclock值都為1;建議的proposedzxid值都為1;建議的proposedepoch值都為1;投票清單為每個節點投自己的一票(1->1,2->2,3->3)。節點1首先向2、3發送自己的投票消息:
節點2、3收到節點1的投票消息,首先檢視1的狀态,發現1處于looking狀态。接下來,判斷1發來的electionepoch和本地邏輯時鐘logicalclock的大小,發現兩者相等(都為1)。接着判斷leader、zxid、peerepoch和本地proposedleader、proposedzxid、proposedepoch的大小,節點2發現節點1推薦的leader的id比自己小(1<2),節點3也發現節點1推薦的leader的id比自己的小(1<3),是以不用更新自己的投票。接下來,節點2、3把節點1的投票放入自己的投票清單中,這樣,節點2收到的投票的清單為:
1->1
2->2
節點3的為:
3->3
節點2、3再判斷此次投票是否可以結束,發現不能結束。如下圖所示:
節點2向節點1、3發送自己的投票資訊,節點3由于發送線程的故障原因,投票資訊一直沒有出去:
在2發出的投票資訊中,選擇的leader是它自己。
節點1、3收到節點2的投票消息。節點1比較自己的logcalclock和節點2發來的electionepoch的大小,二者相等,接下來比較leader、zxid、peerepoch和本地proposedleader、proposedzxid、proposedepoch的大小,發現節點2推薦的leader的id(2)比自己的proposedleader(1)大,于是更新自己的選票,将proposedleader改為2。然後,節點1将2的選票(2->2)放入自己收到的投票箱中,接着判斷投票是否可以結束(調用函數termpredicate),由于節點2被超過半數的節點選擇(1、2),是以選舉可以結束,由于自己不是leader,節點1将自己的狀态改為following。
節點3比較自己的logcalclock和節點2發來的electionepoch的大小,二者相等,接下來比較leader、zxid、peerepoch和本地proposedleader、proposedzxid、proposedepoch的大小,發現節點2推薦的leader的id(2)比自己的proposedleader(3)小,不用更新自己的選票。然後,節點3将2的選票(2->2)放入自己收到的投票箱中,接着判斷投票是否可以結束(調用函數termpredicate),由于沒有節點獲得超過半數的選票,是以選舉繼續。
節點1收到節點2的選票,更新選票後,再向節點1、3發送自己的投票資訊:此時,節點1選的leader已經變為2,而且節點1的狀态已經變成following。
節點2在收到節點1的選票資訊後,判斷節點1的狀态,發現為following,這表明,節點1已經認為leader選出來了,并且是2。節點2首先更新自己的收票箱,将1的投票改為2,接着,判斷選舉是否結束,發現确實可以結束,節點2就更新自己的狀态,由于發現自己是被半數以上人推薦的leader,是以把自己的狀态改為leading。同樣,節點3在收到節點1的投票資訊後,判斷節點1的狀态,發現為following,這表明,節點1已經認為leader選出來了,并且是2。節點3首先更新自己的收票箱,将1的投票改為2,接着,判斷選舉是否結束,發現确實可以結束,節點3就更新自己的狀态,由于發現自己不是被半數以上人推薦的leader,是以把自己的狀态改為following。至此,選舉結束,選出來的leader為2,1、3都為follower。