簡談zookeeper的選主過程
////在講zk的選主前,先普及幾個概念,即分散式的邏輯時鐘,paxos演算法的簡單普及
分散式系統中,由於是分布在不同的機器上,完全無法使用物理上的時鐘來表示,僅能依賴於網路的通訊,而由於通訊之間存在某種因果關係。在分散式系統中的事件存在潛在地因果關係,一個事件C是另外一個事件E的因,或者稱C發生在e之前(c happened before e),具體情況可能是: 當且僅當兩個事件都在同一個節點伺服器生,C首先執行,或者兩個事件在不同節點上發生,e通過接受消息也知道了c發生。如果事件之間彼此不認識,那麼稱它們為同時發生。那麼就可以認為c——>e,那麼就可以依靠這些因果關係建立一些形如(1,0,1)的向量時鐘等。
大家看到網上很多的文章基本都是在說paxos怎麼難理解,說什麼是分散式系統唯一可靠的演算法,但是看著那個拜占庭問題很難下手實現。其實還是由於不過理解以及作者闡述時沒有個特定告訴我們怎麼實現或者怎麼證明說這個演算法是正確的。paxos描述的是一個依賴於多數即可運行高可用、高一致的實現,那麼為什麼是正確的呢?這是需要保留的一個疑問,我們可以通過收斂的方法來看它為什麼可以保證一致。這裡不再多說,直接上個引用如何淺顯易懂地解說 Paxos 的演算法?大神論證得很不錯,我概述下,目的為了保證只能接受一個,從簡單的3個節點的集群情況,分幾種情況下採用拒絕策略可以保證先接收到id大的數據情況下的一致,但是對於那種先接收到id小的情況,我們的拒絕策略完全無效,那麼有個思路就是,假如可以設計成一致,那麼不就可以實現即使先收到id小的情況也可以保證一致性嗎?
那麼開始下,zk的選主從QuorumPeer這個類的run()開始看起,剛開始的那部分都是註冊jmx方便管理,可以忽略掉。
case LOOKING: if (Boolean.getBoolean("readonlymode.enabled")) //判斷是否是只讀模式 ...//創建只能zkserver 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); } } }; ... setCurrentVote(makeLEStrategy().lookForLeader()); //採用選主策略來進行選主
對於選主來說lookForLeader()定製了選主的策略,將以FastLeaderElection來簡單地舉例,順著這個方法,我們可以看到
synchronized(this){ logicalclock++; //更新邏輯時間 updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch()); //更新選票,把票投給自己 sendNotifications(); //通知下其它節點 }
我們可以看到logicalclock這個邏輯時鐘的表示,它通過一個變數來表示經歷了幾輪地選舉,假如出現logicalclock小於接收到的其它節點的信息,我們其實可以假想自己本身的這個peer可能被網路分區過,造成錯過了,則需要同步到選舉情況,否則,可能就會出現自己的信息還是過期的數據。
while ((self.getPeerState() == ServerState.LOOKING) && (!stop)) //當節點還處於looking狀態時則不斷執行 Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);//從收到的信息隊列中等待取數據 if(n == null){ if(manager.haveDelivered()){ //假如暫未取到數據,並且已經發送過了,那應該採用積極地發送 sendNotifications(); } else { //還未完全發送,可以出現分區,那麼嘗試連接 manager.connectAll(); }
那當拿到節點的反饋數據,那麼將會做怎樣的process呢?由於從上面的邏輯時鐘我們講到由於如果接收到的小於我們本身的邏輯時鐘,那麼可能就是網路分區導致的,那麼將會直接break;而只有在等於或者大於邏輯時鐘的情況下,
if (n.electionEpoch > logicalclock) { logicalclock = n.electionEpoch; recvset.clear(); if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) { updateProposal(n.leader, n.zxid, n.peerEpoch); } else { updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch()); } sendNotifications(); }
可以看到當收到的邏輯時鐘大於自身時,那麼默認就把recvset清空,因為本身自己不是leader了,並且採用totalOrderPredicate驗證是否合理,驗證的邏輯就是peer的sid是否比自己大,zxid是否比自己大,epoch是否比自己大,如果成功,那就將節點的leader同步到自己,否則還是默認選自己,那為什麼還是選自己呢,即使我們可以看到邏輯時鐘已經遠低於其它節點,這種情況是為了防止假如自己本身是leader,並且已經達到大多數使得zxid增加1,而剛剛的那個結點是之前的少數由於分區未同步的,如果直接同意它的,那麼就會導致之前已經同意的事情被反悔了,對於一致性不能忍受。
if (n.electionEpoch < logicalclock) { if(LOG.isDebugEnabled()){ LOG.debug("Notification election epoch is smaller than logicalclock. n.electionEpoch = 0x" + Long.toHexString(n.electionEpoch) + ", logicalclock=0x" + Long.toHexString(logicalclock)); } break; }
針對於邏輯時鐘小的,那麼肯定是得丟棄的,因為不可能存在同一時空的,這採用的是相對地來看
推薦閱讀:
※一致性相關知識的索引
※關於一致性協議的一些對比和總結
※分散式系統理論進階 - Paxos
※etcd-raft日誌管理
※小議分散式系統的一致性模型