ZAB Leader選舉實現
設定集羣中有3個節點,通過ZAB算法實現選主。節點之間的通信使用的是自我實現的Remoting組件,基於Netty開發,可以以同步,異步的方式發起通信。在後續《分佈式通信》系列的文章中,會向大家詳細分析Remoting組件。
分佈式選舉的項目名稱:justin-distribute-election
整體結構如圖:
主要Package說明:
callback:異步請求消息的回調處理器集合
client:數據存取接口
message:投票、心跳等消息的集合
processor:接收請求消息的處理器集合
節點相關的類設計:
節點狀態類:NodeStatus.class
public enum NodeStatus {
LOOKING,
LEADING,
FOLLOWING,
}
節點類:Node.class
// 投票箱,記錄集羣中節點的投票信息
private final ConcurrentMap<Integer, Vote> voteBox =
new ConcurrentHashMap<>();
// 記錄集羣中Follower節點的數據ID
private final ConcurrentMap<Integer, ZxId> zxIdMap =
new ConcurrentHashMap<>();
// 記錄集羣中Follower節點的數據ID
private final ConcurrentMap<Integer, Boolean> snapshotMap =
new ConcurrentHashMap<>();
// 節點初始爲Following狀態
private volatile NodeStatus status = NodeStatus.FOLLOWING;
private Vote myVote;
投票類:Vote.class
public class Vote implements Comparable<Vote>{
// 投票的節點ID
private int nodeId;
// 投票週期
private volatile long epoch;
// 被投票的節點ID
private volatile int voteId;
// 節點的數據ID
private volatile ZxId lastZxId;
// 先比較數據ID,如果相等再比較節點ID
@Override
public int compareTo(Vote o) {
if (this.lastZxId.compareTo(o.lastZxId) != 0) {
return this.lastZxId.compareTo(o.lastZxId);
}else if (this.nodeId < o.nodeId) {
return -1;
}else if (this.nodeId > o.nodeId) {
return 1;
}
return 0;
}
線程設計:
-
mgrServer端線程,用於接收管理集羣節點的消息;
-
server端線程,用於接收節點發送投票及數據同步的消息;
-
client端線程,用於向節點發送消息;
-
心跳線程,用於Leader節點向Follower節點發送心跳消息;
-
選舉線程,用於在集羣內選舉主節點;
-
節點發現線程,用於新節點加入集羣;
具體實現就不列出來了,很容易理解,大家看代碼吧。
選舉流程:
選舉線程調用的方法:election()
private void election() {
if (status == NodeStatus.LEADING) {
return;
}
if (!nodeConfig.resetElectionTick()) {
return;
}
status = NodeStatus.LOOKING;
// 選舉週期加1
epoch += 1;
// 清空集羣中節點的數據ID記錄
zxIdMap.clear();
// 獲取最新的數據ID,組裝投票信息
this.myVote = new Vote(nodeConfig.getNodeId(),
nodeConfig.getNodeId(), 0, getLastZxId());
this.myVote.setEpoch(epoch);
// 將本地節點的投票放入投票箱
this.voteBox.put(nodeConfig.getNodeId(), myVote);
VoteMessage voteMessage = VoteMessage.getInstance();
voteMessage.setVote(myVote);
// 單向發送投票消息,不需要回復
sendOneWayMsg(voteMessage.request());
}
投票消息處理類:VoteRequestProcessor.class
// 將其他節點的投票消息記入投票箱
node.getVoteBox().put(peerVote.getNodeId(), peerVote);
// 比較選舉週期
if (peerVote.getEpoch() > node.getMyVote().getEpoch()) {
node.getMyVote().setEpoch(peerVote.getEpoch());
node.getMyVote().setVoteId(peerVote.getNodeId());
node.setStatus(NodeStatus.LOOKING);
}else if (peerVote.getEpoch() == node.getMyVote().getEpoch()) {
// 週期相同,則比較數據ID和節點ID
if (peerVote.compareTo(node.getMyVote()) == 1) {
// 比較後,如果對端數據ID或節點ID大,則將投票ID記錄爲對端節點ID
node.getMyVote().setVoteId(peerVote.getNodeId());
node.setStatus(NodeStatus.LOOKING);
}
}
// 得票超過半數,則成爲Leader
if (node.isHalf()) {
logger.info("Node:{} become leader!", node.getNodeConfig().getNodeId());
node.becomeLeader();
}else if (node.getStatus() == NodeStatus.LOOKING){
// 向其他節點發送新的投票消息
VoteMessage voteMsg = VoteMessage.getInstance();
voteMsg.setVote(node.getMyVote());
node.sendOneWayMsg(voteMsg.request());
}
心跳線程調用的方法:heartbeat()
long index = -1;
// 獲取Follower節點應該同步的數據ID
if (zxIdMap.containsKey(entry.getKey())) {
index = zxIdMap.get(entry.getKey()).getCounter();
}else {
index = dataManager.getLastIndex();
}
// 獲取數據
Data data = dataManager.read(index);
if (data.getZxId().getEpoch() == 0) {
data.getZxId().setEpoch(epoch);
}
// 組裝數據消息
DataMessage dataMsg = DataMessage.getInstance();
dataMsg.setNodeId(nodeConfig.getNodeId());
dataMsg.setType(DataMessage.Type.SYNC);
dataMsg.setData(data);
executorService.submit(new Runnable() {
@Override
public void run() {
try {
// 發送數據消息
RemotingMessage response = client.invokeSync(
entry.getValue(), dataMsg.request(), 3*1000);
DataMessage res = DataMessage.getInstance()
.parseMessage(response);
// 將Follower節點應該同步的數據ID放入zxIdMap中
if (res.getSuccess()) {
int peerId = res.getNodeId();
ZxId peerZxId = res.getData().getZxId();
zxIdMap.put(peerId, peerZxId);
}
} catch (Exception e) {
logger.error(e);
}
}
});
心跳消息處理類:DataRequestProcessor.class
logger.info("Receive heartbeat message: " + dataMsg);
// 重置選舉計時器和心跳計時器
node.getNodeConfig().setPreElectionTime(System.currentTimeMillis());
node.getNodeConfig().setPreHeartbeatTime(System.currentTimeMillis());
// 節點切換爲Following狀態
node.setStatus(NodeStatus.FOLLOWING);
// 設置主節點ID
node.setLeaderId(dataMsg.getNodeId());
至此,ZAB算法的Leader選舉代碼實現完成。
接下來的文章我們來分析分佈式數據複製的原理,同時完善Raft算法數據複製部分的代碼。
代碼地址:https://github.com/Justin02180218?tab=repositories
更多【分佈式專輯】系列文章,請關注公衆號