zookeeper(四)領導者選舉
先上圖:
首先看到這個圖肯定就會很懵,確實比較多,體系結構也比較複雜,目前但是如果是根據原始碼除錯加上這篇部落格應該問題就不是很大。
我們首先從判定為叢集開始說起:即從org.apache.zookeeper.server.quorum.QuorumPeerMain#runFromConfig開始。這裡重點說一下quorumPeer.start();
方法。
@Override
public synchronized void start() {
//載入事務和快照。其實就是恢復的意思。主要就是把資料載入到記憶體,拿到紀元號之類的
loadDataBase();
//就是啟動thread執行緒,跟單機的那個執行緒做的一樣的事
cnxnFactory.start();
//領導者選舉。(重要)
//https://www.cnblogs.com/johnvwan/p/9546909.html 這篇部落格講得很好。
startLeaderElection();
//使用選舉演算法選出領導並開始同步。
super.start();
}
前面2個不說,就跟單機啟動一模一樣。重點是第三個和第四個。
跟蹤到這個方法中:org.apache.zookeeper.server.quorum.QuorumPeer#startLeaderElection
synchronized public void startLeaderElection() {
try {
//引數:1、myid。2.最新的zxid。3、當前的紀元號
//首先投自己一票。在投票箱中寫上自己的資訊。
currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
} catch(IOException e) {
RuntimeException re = new RuntimeException(e.getMessage());
re.setStackTrace(e.getStackTrace());
throw re;
}
//getView().values()就是獲取到配置檔案中的所有以server.開頭的資訊。包括observer
for (QuorumServer p : getView().values()) {
if (p.id == myid) {//如果id號是自己
//為什麼要有這個判斷,因為你不但要告訴其它伺服器投的誰,你還要告訴其它伺服器是你投的
myQuorumAddr = p.addr;
break;
}
}
if (myQuorumAddr == null) {
throw new RuntimeException("My id " + myid + " not in the peer list");
}
if (electionType == 0) {
try {
udpSocket = new DatagramSocket(myQuorumAddr.getPort());
responder = new ResponderThread();
responder.start();
} catch (SocketException e) {
throw new RuntimeException(e);
}
}
//建立選舉演算法,預設是3.electionAlg可以更改。
//此外還有0,1,2.但是現在一般都不推薦了。所以一般不會動
//步驟如下:
//1、初始化QuorumCnxManager
//2、初始化QuorumCnxManager.Listener
//3、執行QuorumCnxManager.Listener
//4、執行QuorumCnxManager
//5、返回FastLeaderElection物件
//流程如下:
//1.把自己的投票放入queueSendMap中:用於傳送自己的投票資訊。首先肯定是投自己。
//2.queueSendMap中傳送給其它伺服器,如果是自己這一臺就直接放到recvQueue中,如果是其它伺服器就通過socket傳送投票資訊。
//3.不斷的檢測recvQueue中的投票資訊,如果在某一時刻recvQueue中過半的伺服器(通過sid標識),那個被投票的那個人就設定為leader,其它就設定為follower。
//伺服器連線方式id大的去連線id小的,不允許小的連線大的。
this.electionAlg = createElectionAlgorithm(electionType);
}
這個方法的整體邏輯已經在方法註釋中體現出來了。最複雜的是哪個方法呢?看註釋就知道是createElectionAlgorithm這個方法了。
protected Election createElectionAlgorithm(int electionAlgorithm){
Election le=null;
//TODO: use a factory rather than a switch
switch (electionAlgorithm) {
case 0:
le = new LeaderElection(this);
break;
case 1:
le = new AuthFastLeaderElection(this);
break;
case 2:
le = new AuthFastLeaderElection(this, true);
break;
case 3:
//建立一個QuorumCnxManager,裡面存在幾個重要的屬性。
//ConcurrentHashMap<Long, ArrayBlockingQueue<ByteBuffer>> queueSendMap:
//ConcurrentHashMap<Long, SendWorker> senderWorkerMap:就是用來記錄其他伺服器id以及對應的SendWorker的
//ArrayBlockingQueue<Message> recvQueue:儲存選票
//QuorumCnxManager.Listener
qcm = createCnxnManager();
//姑且認為就是一個監聽器。因為是一個執行緒。
QuorumCnxManager.Listener listener = qcm.listener;
if(listener != null){
listener.start();//啟動監聽器
//這裡面比較複雜
le = new FastLeaderElection(this, qcm);
} else {
LOG.error("Null listener when initializing cnx manager");
}
break;
default:
assert false;
}
return le;
}
由於0 1 2都是不建議使用了,所以我們們就直接看3的情況。當然這裡面哪裡很複雜呢?當然是le = new FastLeaderElection(this, qcm);這個了。這個也是今天的重點,快速領導者選舉,會很複雜,我們一步一步來。
FastLeaderElection初始化
public FastLeaderElection(QuorumPeer self, QuorumCnxManager manager){
this.stop = false;
this.manager = manager;
starter(self, manager);
}
幹了什麼呢?就是初始化的賦值,然後進入starter方法。好像也沒什麼,我們看看這個方法。
starter(self, manager)
private void starter(QuorumPeer self, QuorumCnxManager manager) {
this.self = self;
proposedLeader = -1;
proposedZxid = -1;
sendqueue = new LinkedBlockingQueue<ToSend>();
recvqueue = new LinkedBlockingQueue<Notification>();
this.messenger = new Messenger(manager);
}
前面3行就是初始化的。重點是後面3行。看起來好像也沒有什麼,但是意義非常重大。你現在不需要記住什麼,就只需要記住一點,初始化了兩個queue。sendqueue 和recvqueue ,用處顧名思義。牢記,後面能用上。
this.messenger = new Messenger(manager)
Messenger(QuorumCnxManager manager) {
this.ws = new WorkerSender(manager);
Thread t = new Thread(this.ws,
"WorkerSender[myid=" + self.getId() + "]");
t.setDaemon(true);
t.start();
this.wr = new WorkerReceiver(manager);
t = new Thread(this.wr,
"WorkerReceiver[myid=" + self.getId() + "]");
t.setDaemon(true);
t.start();
}
看起也簡單。new WorkerSender和new WorkerReceiver而已,然後就開子執行緒啟動了。那麼現在開始涉及到多執行緒了。我們先看WorkerSender,再看WorkerReceiver。
WorkerSender
public void run() {
while (!stop) {
try {
//這裡取了是把所有的參與者都放進去了。什麼時候放進去的呢?
// 通過sendNotifications函式放進去的。
ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS);
if(m == null) continue;
//開始處理
process(m);
} catch (InterruptedException e) {
break;
}
}
LOG.info("WorkerSender is down");
}
sendqueue有印象沒有,前面初始化提到過哦。這個是取方法。那麼肯定就有放的方法。從哪裡放的呢?當然你看到了我的註釋。但是啥時候呼叫的這個方法?神奇不?
當然除錯就知道啦!
一直往前推就能追溯到這個方法:org.apache.zookeeper.server.quorum.QuorumPeer#run,這個方式是什麼時候啟動的呢?第一個程式碼段的super.start();方法啟動的。我們看這段程式碼:
public void run() {
setName("QuorumPeer" + "[myid=" + getId() + "]" +
cnxnFactory.getLocalAddress());
System.out.println("當前執行緒:" + Thread.currentThread().getName());
LOG.debug("Starting quorum peer");
//這個try的內容看不懂要幹啥?
try {
jmxQuorumBean = new QuorumBean(this);
MBeanRegistry.getInstance().register(jmxQuorumBean, null);
for(QuorumServer s: getView().values()){
ZKMBeanInfo p;
if (getId() == s.id) {
p = jmxLocalPeerBean = new LocalPeerBean(this);
try {
MBeanRegistry.getInstance().register(p, jmxQuorumBean);
} catch (Exception e) {
LOG.warn("Failed to register with JMX", e);
jmxLocalPeerBean = null;
}
} else {
p = new RemotePeerBean(s);
try {
MBeanRegistry.getInstance().register(p, jmxQuorumBean);
} catch (Exception e) {
LOG.warn("Failed to register with JMX", e);
}
}
}
} catch (Exception e) {
LOG.warn("Failed to register with JMX", e);
jmxQuorumBean = null;
}
try {
/*
* Main loop
*/
//這裡我有一個誤區,我以為break是直接跳出了while迴圈,但實際是隻是跳出了switch,還是基礎不夠紮實。break不但用於跳出迴圈,也用於跳出switch。
//測試檔案:com.xq.test.TestWhile
while (running) {
switch (getPeerState()) {
case LOOKING:
LOG.info("LOOKING");
if (Boolean.getBoolean("readonlymode.enabled")) {//如果開啟了只讀
LOG.info("Attempting to start ReadOnlyZooKeeperServer");
// Create read-only server but don't start it immediately
final ReadOnlyZooKeeperServer roZk = new ReadOnlyZooKeeperServer(
logFactory, this,
new ZooKeeperServer.BasicDataTreeBuilder(),
this.zkDb);
// Instead of starting roZk immediately, wait some grace
// period before we decide we're partitioned.
//
// Thread is used here because otherwise it would require
// changes in each of election strategy classes which is
// unnecessary code coupling.
Thread roZkMgr = new Thread() {
public void run() {
try {
// lower-bound grace period to 2 secs
sleep(Math.max(2000, tickTime));
if (ServerState.LOOKING.equals(getPeerState())) {
roZk.startup();
}
} catch (InterruptedException e) {
LOG.info("Interrupted while attempting to start ReadOnlyZooKeeperServer, not started");
} catch (Exception e) {
LOG.error("FAILED to start ReadOnlyZooKeeperServer", e);
}
}
};
try {
roZkMgr.start();
setBCVote(null);
setCurrentVote(makeLEStrategy().lookForLeader());
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
setPeerState(ServerState.LOOKING);
} finally {
// If the thread is in the the grace period, interrupt
// to come out of waiting.
roZkMgr.interrupt();
roZk.shutdown();
}
} else {//沒有開啟只讀
try {
setBCVote(null);
//不斷更新投票,直到選出leader
setCurrentVote(makeLEStrategy().lookForLeader());
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
setPeerState(ServerState.LOOKING);
}
}
break;
case OBSERVING:
try {
LOG.info("OBSERVING");
setObserver(makeObserver(logFactory));
observer.observeLeader();
} catch (Exception e) {
LOG.warn("Unexpected exception",e );
} finally {
observer.shutdown();
setObserver(null);
setPeerState(ServerState.LOOKING);
}
break;
case FOLLOWING:
try {
LOG.info("FOLLOWING");
//產出一個適合follower的類
setFollower(makeFollower(logFactory));
follower.followLeader();
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
} finally {
follower.shutdown();
setFollower(null);
setPeerState(ServerState.LOOKING);
}
break;
case LEADING:
LOG.info("LEADING");
try {
//產出一個適合leader的類
setLeader(makeLeader(logFactory));
//這個只是選擇過半能接受的epoch,假設有更新的zxid沒有啟動
//在這個確定epoch的時候它啟動了,那麼這個leader會直接丟失多的資料嗎?看程式碼好像是的
leader.lead();
setLeader(null);
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
} finally {
if (leader != null) {
leader.shutdown("Forcing shutdown");
setLeader(null);
}
setPeerState(ServerState.LOOKING);
}
break;
}
}
} finally {
LOG.warn("QuorumPeer main thread exited");
try {
MBeanRegistry.getInstance().unregisterAll();
} catch (Exception e) {
LOG.warn("Failed to unregister with JMX", e);
}
jmxQuorumBean = null;
jmxLocalPeerBean = null;
}
}
從長度和註釋就知道這是一塊非常核心的程式碼。因為它就是真正的選舉核心以及選舉完了之後的角色分配。這段程式碼實在是非常複雜,建議結合下面的圖理解。
為了便於理解再用文字補充一下:
我們所瞭解到的WorkerSender執行緒是從sendqueue取的資料。而我們現在就是在看是那裡放的資料?放的應該是什麼資料?這個應該理解一下就能想到是你投票的資料了。關於多執行緒這塊需要有一定的基礎才方便理解。
現在我們回到WorkerSender執行緒裡面,再來回顧一下這段程式碼:
public void run() {
while (!stop) {
try {
//這裡取了是把所有的參與者都放進去了。什麼時候放進去的呢?
// 通過sendNotifications函式放進去的。
ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS);
if(m == null) continue;
//開始處理
process(m);
} catch (InterruptedException e) {
break;
}
}
LOG.info("WorkerSender is down");
}
取出來之後重點就是process(m);處理了。
void process(ToSend m) {
ByteBuffer requestBuffer = buildMsg(m.state.ordinal(),
m.leader,
m.zxid,
m.electionEpoch,
m.peerEpoch);
//構造除byte的陣列傳送訊息
manager.toSend(m.sid, requestBuffer);
}
主要是看怎麼傳送的,定位到 manager.toSend(m.sid, requestBuffer)。
public void toSend(Long sid, ByteBuffer b) {
/*
* If sending message to myself, then simply enqueue it (loopback).
*/
if (this.mySid == sid) {//如果是本機的話
b.position(0);
//直接新增到recvQueue中
addToRecvQueue(new Message(b.duplicate(), sid));
/*
* Otherwise send to the corresponding thread to send.
*/
} else {//不是本機的話
/*
* Start a new connection if doesn't have one already.
*/
ArrayBlockingQueue<ByteBuffer> bq = new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY);
//putIfAbsent 如果傳入key對應的value已經存在,就返回存在的value,不進行替換。如果不存在,就新增key和value,返回null
ArrayBlockingQueue<ByteBuffer> bqExisting = queueSendMap.putIfAbsent(sid, bq);
if (bqExisting != null) {//如果不是null,說明已經存在了。證明已經投過一票了。就把b加入到bqExisting中
addToSendQueue(bqExisting, b);
} else {//如果為null,說明目前為止還沒有投過。就把b加入到bq中。為啥新增到不同的佇列中?
addToSendQueue(bq, b);
}
connectOne(sid);
}
}
這段程式碼有細節沒懂,不過不是很影響大局。
對照著圖下一個應該是SendWorker執行緒了。這個是啥時候被造出來的?
這個時候分為兩種情況。
1.當前就一臺啟動了,因為目前就一臺,所以不需要像其它伺服器發訊息,所以這個時候並不會造出這個執行緒,而如果第二臺起來了,它才會造這個執行緒。下面給個呼叫鏈。它是怎麼實現的?就是不斷的探測能否與其它伺服器通訊,只有能通訊才造執行緒。
org.apache.zookeeper.server.quorum.QuorumPeer#run—>org.apache.zookeeper.server.quorum.QuorumPeer#setCurrentVote–>org.apache.zookeeper.server.quorum.QuorumCnxManager#connectAll–>org.apache.zookeeper.server.quorum.QuorumCnxManager#connectOne–>org.apache.zookeeper.server.quorum.QuorumCnxManager#initiateConnection–>org.apache.zookeeper.server.quorum.QuorumCnxManager#startConnection
2.當前不是第一個啟動的,即直接就能與其它伺服器通訊。
這個又需要追溯到前面了。QuorumCnxManager.Listener listener = qcm.listener;這個在本頁面搜一下。在啟動listener之後。在這裡面就已經造好了SendWorker和RecvWorker執行緒。這個又是怎麼實現的?因為一啟動就開啟了埠,此時另外一臺在向這邊發資料,所以是被迫造的執行緒!
org.apache.zookeeper.server.quorum.QuorumCnxManager.Listener#run–>org.apache.zookeeper.server.quorum.QuorumCnxManager#receiveConnection–>org.apache.zookeeper.server.quorum.QuorumCnxManager#handleConnection
RecvWorker執行緒也是在這個時候造出來的。
WorkerReceiver也比較類似。雖然細節不一樣,但是大體流程也應該清楚了。
主要是要對投票的邏輯圖熟悉。在看這些稍微好點。
這篇長了點,先這樣吧。後面介紹是怎麼驗證過半的,以及選出後做了一些什麼。
相關文章
- 使用Spring Integration和Hazelcast進行叢集領導者選舉SpringAST
- ZooKeeper 工作、選舉 原理
- 分散式系統中的領導選舉分散式
- Zookeeper原始碼(啟動+選舉)原始碼
- Zookeeper原始碼分析-Zookeeper Leader選舉演算法原始碼演算法
- zookeeper原始碼(04)leader選舉流程原始碼
- 深入淺出Zookeeper(七):Leader選舉
- 演算法領頭羊丨分散式系統如何選舉領導?演算法分散式
- Zookeeper(4)---ZK叢集部署和選舉
- zookeeper的原理和使用(二)-leader選舉
- Zookeeper 的選舉機制也不過如此!
- 致同女性領導者當選IWIRC香港董事會和全球領導團隊成員
- 超細!細說Zookeeper選舉的一個案例(上)
- 超細!細說Zookeeper選舉的一個案例(下)
- 中國唯一入選 Forrester 領導者象限,阿里雲 Serverless 全球領先REST阿里Server
- 面試官:說一說Zookeeper中Leader選舉機制面試
- Zookeeper分散式過程協同技術 - 群首選舉分散式
- ZooKeeper系列(四)
- ZooKeeper-3.4.6叢集選舉Bug踩坑與恢復記錄
- 分散式協調元件Zookeeper之 選舉機制與ZAB協議分散式元件協議
- 全球領導人聯盟在巴黎舉行會議
- 領導者還是領先者,任先生是這樣說的
- Gartner:2022年領導力前瞻—軟體工程領導者(附下載)軟體工程
- 面試題:說說你對ZooKeeper叢集與Leader選舉的理解?面試題
- DDI:中國領導者十年領導力圖鑑(附下載)
- 埃森哲:尋找新型領導者
- 看完這篇文章你就可以告訴領導你精通Zookeeper了
- (四)選單導航及路由設定路由
- 好程式設計師大資料技術分享Zookeeper叢集管理與選舉程式設計師大資料
- 好程式設計師大資料技術分享:Zookeeper叢集管理與選舉程式設計師大資料
- zookeeper原始碼 — 四、session建立原始碼Session
- zab選舉
- Autodesk CAD2022:數字化設計領域的領導者
- AI晶片現狀:領導者很難被超越AI晶片
- 轉:領導者要做園丁,而不是做英雄
- Zookeeper 四字命令介紹
- zookeeper使用(四)--應用場景
- 什麼是Zookeeper?(動態的服務註冊和發現、Master選舉、分散式鎖)AST分散式