參考資料
<<從PAXOS到ZOOKEEPER分佈式一致性原理與實踐>>
zookeeper-3.0.0
Zookeeper選舉模式
針對zookeeper-3.0.0版本,選舉模式可以分爲三種模式,主要分爲快速認證選舉,快速選舉和直接UDP選舉這三個算法。通過選舉來確定啓動服務的角色是leader還是follower。Zookeeper集羣規定至少是兩臺服務器,並且建議是以奇數組成服務器集羣,默認以三臺舉例,並且服務器myid依次爲1、2、3。
集羣啓動時選舉流程
- 每個Server啓動後發出一個投票。因爲是在集羣初始啓動階段,對於機器而言都是將自己作爲Leader服務器進行投票,每次投票包含的數據內容就是:所推舉的服務器的myid和zxid,zxid就是以自增的值去生成,例如剛剛開始生成的zxid爲0,如果還要繼續投票循環則生成爲1,此時的投票信息就是(1,0),(2,0),(3,0)。
- 接受來自各個服務器的投票。每個服務器都會接受到其他服務器的投票。集羣中的每個服務器在接受到投票後,首先會判斷該投票的有效性,包括檢查是否是本輪投票,是否來自LOOKING狀態的服務器等信息的檢查,檢查合法後纔會處理計算投票。
- 處理投票。在接受到來自其他服務器的投票後,針對每一個投票,服務器都需要將別人的投票和自己的投票進行對比,首先會檢查zxid,如果zxid較大則選擇zxid較大的服務器作爲leader,如果zxid相同,那就比較myid,myid較大的服務器作爲leader。由於是集羣啓動階段故zxid爲0 都相同,此時Server1和Server2會將當前的投票信息更新爲(3,0)然後將該選票發送出去。
- 統計投票。每次投票後,服務器都統計投票,判斷是否已經有過半的機器接收到相同的投票信息。此時統計出集羣中其他兩天機器已經接受了(3,0)這個投票信息,故達到了大於集羣數量一半的要求。
- 改變服務器狀態。此時Server3確定角色爲leader,Server1和Server2確定角色爲follower。
集羣運行過程中選舉流程
在集羣的運行過程中,一旦選出了一個leader,那麼所有服務器的集羣角色一般不會再發生改變,即leader服務器一直作爲集羣的leader,即使集羣中有非leader集羣掛了或是有新機器加入集羣也不會影響leader,如果leader服務器掛了,此時整個集羣就暫時無法對外提供服務,會進入新一輪的leader選舉。此時的選舉流程基本保持和集羣啓動時的流程保持一致,只不過在寫入投票階段過程中生成的zxid會根據運行的狀態去確定該內容值。
- 變更狀態。首先當leader掛了之後,剩餘的服務器都會將自己的服務器狀態變更爲looking狀態,然後進入選舉流程。
- 每個Server發送一個投票。在這個過程中,需要生成投票信息,因爲是運行期間,因此每個zxid可能會不同,如果Server1爲10,Server2爲11,Server3爲12,此時投票就會選擇Server3的12然後將投票發送出去。剩餘的流程就重複如上。
選舉算法分析
在上文集羣啓動中,最後會執行到QuorumPeer類中的run方法中去,其中就是當處於LOOKING狀態時,就進行選舉操作。
case LOOKING:
try {
LOG.info("LOOKING");
setCurrentVote(makeLEStrategy().lookForLeader()); // 設置投票並選擇leader
} catch (Exception e) {
LOG.warn("Unexpected exception",e); // 如果出錯則設置爲LOOKING狀態
setPeerState(ServerState.LOOKING);
}
break;
此時就執行了makeLEStrategy函數返回的類的lookForLeader方法。
protected Election makeLEStrategy(){
if(electionAlg==null)
return new LeaderElection(this);
return electionAlg;
}
在集羣模式啓動時,默認不選擇選舉算法時,此時的electionAlg就是null,此時就會新生成一個LeaderElection類實例,並調用該類的lookForLeader方法,在啓動模式的時候,會默認啓動ResponderThread類中的UDP選舉算法,該類就是監聽執行的端口,來獲取其他服務器的選票信息。並且該類處於選舉中的執行流程如下;
switch (getPeerState()) {
case LOOKING: // 如果是競選狀態
responseBuffer.putLong(current.id); // 壓入id 和 zxid
responseBuffer.putLong(current.zxid);
break;
就是將當前的id和zxid壓入返回數據中。此時我們繼續查看,LeaderElection的選舉方法。
private ElectionResult countVotes(HashMap<InetSocketAddress, Vote> votes, HashSet<Long> heardFrom) {
ElectionResult result = new ElectionResult(); // 初始化選舉結果實例
// Initialize with null vote
result.vote = new Vote(Long.MIN_VALUE, Long.MIN_VALUE); // 初始化一個默認的vote
result.winner = new Vote(Long.MIN_VALUE, Long.MIN_VALUE); // 初始化一個默認的winner
Collection<Vote> votesCast = votes.values(); // 獲取所有的值
// First make the views consistent. Sometimes peers will have
// different zxids for a server depending on timing.
for (Iterator<Vote> i = votesCast.iterator(); i.hasNext();) { // 迭代遍歷該值
Vote v = i.next();
if (!heardFrom.contains(v.id)) { // 如果不包括該值則移除
// Discard votes for machines that we didn't hear from
i.remove();
continue;
}
for (Vote w : votesCast) { // 遍歷votesCast
if (v.id == w.id) { // 如果id相同
if (v.zxid < w.zxid) { // 並且w的zxid大於當前的v的zxid
v.zxid = w.zxid; // 將v的zxid更新爲w的zxid
}
}
}
}
HashMap<Vote, Integer> countTable = new HashMap<Vote, Integer>(); // 統計選票hashmap
// Now do the tally
for (Vote v : votesCast) { // 遍歷所有vote
Integer count = countTable.get(v); // 首先獲取一下該v 如果爲NULL則統計數設置爲1
if (count == null) {
count = Integer.valueOf(0);
}
countTable.put(v, count + 1); // 將當前v壓入並計數加1
if (v.id == result.vote.id) { // 如果當前壓入的v的id 與投票獲取的id相同則加1
result.count++;
} else if (v.zxid > result.vote.zxid
|| (v.zxid == result.vote.zxid && v.id > result.vote.id)) { // 如果當前的zxid大於當前投票獲取的vote的zxid或者 當前的zxid與投票獲取的zxid相同當時當前id大於投票獲取的id
result.vote = v; // 設置當前的vote爲 v 此時就體現了獲取最大zxid 或者在zxid相同的情況下 選舉最大server的id的選舉思路
result.count = 1;
}
}
result.winningCount = 0; // 設置選舉統計數爲0
LOG.warn("Election tally: ");
for (Entry<Vote, Integer> entry : countTable.entrySet()) { // 遍歷所有的列表
if (entry.getValue() > result.winningCount) { // 如果獲取的統計數大於當前結果的選舉數則設置統計數與最終獲勝的選票
result.winningCount = entry.getValue();
result.winner = entry.getKey();
}
LOG.warn(entry.getKey().id + "\t-> " + entry.getValue());
}
return result; // 返回結果
}
public Vote lookForLeader() throws InterruptedException {
self.setCurrentVote(new Vote(self.getId(), self.getLastLoggedZxid())); // 設置當前選票 設置爲自己服務器的id 和自己的zxid
// We are going to look for a leader by casting a vote for ourself
byte requestBytes[] = new byte[4];
ByteBuffer requestBuffer = ByteBuffer.wrap(requestBytes);
byte responseBytes[] = new byte[28];
ByteBuffer responseBuffer = ByteBuffer.wrap(responseBytes);
/* The current vote for the leader. Initially me! */
DatagramSocket s = null;
try {
s = new DatagramSocket(); // 設置UDP通信
s.setSoTimeout(200); // 設置發送超時時間
} catch (SocketException e1) {
e1.printStackTrace(); // 如果報錯則退出
System.exit(4);
}
DatagramPacket requestPacket = new DatagramPacket(requestBytes,
requestBytes.length); // 設置發送數據包
DatagramPacket responsePacket = new DatagramPacket(responseBytes,
responseBytes.length); // 設置接收數據包
HashMap<InetSocketAddress, Vote> votes = new HashMap<InetSocketAddress, Vote>(
self.quorumPeers.size()); // 保存投票信息
int xid = new Random().nextInt(); // 獲取xid
while (self.running) { // 循環執行
votes.clear();
requestBuffer.clear();
requestBuffer.putInt(xid); // 壓入xid
requestPacket.setLength(4);
HashSet<Long> heardFrom = new HashSet<Long>();
for (QuorumServer server : self.quorumPeers.values()) { // 遍歷其他服務器列表
requestPacket.setSocketAddress(server.addr); // 設置發送的端口
LOG.warn("Server address: " + server.addr);
try {
s.send(requestPacket); // 發送數據
responsePacket.setLength(responseBytes.length);
s.receive(responsePacket); // 接收返回數據
if (responsePacket.getLength() != responseBytes.length) { // 檢驗返回數據是否合法
LOG.error("Got a short response: "
+ responsePacket.getLength());
continue;
}
responseBuffer.clear();
int recvedXid = responseBuffer.getInt(); // 獲取接收到的xid
if (recvedXid != xid) { // 如果不相同則繼續
LOG.error("Got bad xid: expected " + xid
+ " got " + recvedXid);
continue;
}
long peerId = responseBuffer.getLong(); // 獲取返回的id
heardFrom.add(peerId);
//if(server.id != peerId){
Vote vote = new Vote(responseBuffer.getLong(),
responseBuffer.getLong()); // 設置選票的信息
InetSocketAddress addr = (InetSocketAddress) responsePacket
.getSocketAddress();
votes.put(addr, vote); // 壓入hash表中統計選票
//}
} catch (IOException e) {
LOG.error("Error in looking for leader", e);
// Errors are okay, since hosts may be
// down
// ZooKeeperServer.logException(e);
}
}
ElectionResult result = countVotes(votes, heardFrom); // 統計選票
if (result.winner.id >= 0) { // 如果id大於等於0
self.setCurrentVote(result.vote); // 設置當前的選票
if (result.winningCount > (self.quorumPeers.size() / 2)) { // 如果獲勝的統計大於總數的一半
self.setCurrentVote(result.winner); // 設置當前的選票爲獲勝者
s.close();
Vote current = self.getCurrentVote(); // 獲取當前選票
self.setPeerState((current.id == self.getId())
? ServerState.LEADING: ServerState.FOLLOWING); // 如果當前id與當前選票id相同則是主否則爲從
if (self.getPeerState() == ServerState.FOLLOWING) { // 如果當前狀態爲FOLLOWING則休眠後返回當前選票
Thread.sleep(100);
}
return current;
}
}
Thread.sleep(1000); // 如果沒有找出則休眠繼續下一個循環
}
return null;
}
從尋找主的過程中可以看出,選舉的思路就是選取最大的zxid,如果zxid相同則選擇最大myid的Server最爲統計的獲勝者。
總結
本文主要就是概述了選舉算法的一個基本流程與實現的思路,選舉的流程實現較爲簡單,主要就是根據固定的規則,首先選擇最大的zxid,如果zxid相同則選擇最大的server的id的思路,並且通過UDP來訪問各個服務器之間的數據的交互,從而完成選舉過程。由於本人才疏學淺,如有錯誤請批評指正。