Zookeeper--Zookeeper是什么
博客借鑒http://www.cnblogs.com/yuyijq/p/3391945.html?
Google的三篇論文影響了很多很多人,也影響了很多很多系統。這三 篇論文一直是分布式領域傳閱的經典。根據MapReduce,于是我們有了Hadoop;根據GFS,于是我們有了HDFS;根據BigTable,于是 我們有了HBase。而在這三篇論文里都提及Google的一個lock service---Chubby,哦,于是我們有了Zookeeper。
隨著大數據的火熱,Hxx們已經變得耳熟能詳,現在作為一個開發人員如果都不知道這幾個名詞出門都好像不好意思跟人打招呼。但實際上對我們這些非大 數據開發人員而言,Zookeeper是比Hxx們可能接觸到更多的一個基礎服務。但是,無奈的是它一直默默的位于二線,從來沒有Hxx們那么耀眼。
那么 到底什么是Zookeeper呢?Zookeeper可以用來干什么?我們將如何使用Zookeeper?Zookeeper又是怎么實現的?
伴隨著Zookeeper有兩篇論文:一篇是Zab,就是介紹Zookeeper背后使用的一致性協議的(Zookeeper atomic broadcast protocol),還有一篇就是介紹Zookeeper本身的。在這兩篇論文里都提到Zookeeper是一個分布式協調服務(a service for coordinating processes of distributed applications)。那分布式協調服務又是個什么東西呢?首先我們來看“協調”是什么意思。
說到協調,我首先想到的是北京很多十字路口的交通協管,他們手握著小紅旗,指揮車輛和行人是不是可以通行。如果我們把車輛和行人比喻成運行在計算機 中的單元(線程),那么這個協管是干什么的?很多人都會想到,這不就是鎖么?對,在一個并發的環境里,我們為了避免多個運行單元對共享數據同時進行修改, 造成數據損壞的情況出現,我們就必須依賴像鎖這樣的協調機制,讓有的線程可以先操作這些資源,然后其他線程等待。對于進程內的鎖來講,我們使用的各種語言 平臺都已經給我們準備很多種選擇。
就拿Java來說,有最普通不過的同步方法或同步塊:
public synchronized void sharedMethod(){ //對共享數據進行操作}
使用了這種方式后,多個線程對sharedMethod進行操作的時候,就會協調好步驟,不會對sharedMethod里的資源進行破壞,產生不 一致的情況。這個最簡單的協調方法,但有的時候我們可能需要更復雜的協調。比如我們常常為了提高性能,我們使用讀寫鎖。因為大部分時候我們對資源是讀取多 而修改少,而如果不管三七二十一全部使用排他的寫鎖,那么性能有可能就會受到影響。還是用java舉例:
public class SharedSource{
private ReadWriteLock rwlock = new ReentrantReadWriteLock();
private Lock rlock = rwlock.readLock(); private Lock wlock =
rwlock.writeLock();
public void read(){
rlock.lock(); try{ //讀取資源 }finally{ rlock.unlock(); } }
public void write(){
wlock.lock();
try{ //寫資源 }
finally{
wlock.unlock(); }
}
}
我們在進程內還有各種各樣的協調機制(一般我們稱之為同步機制)。現在我們大概了解了什么是協調了,但是上面介紹的協調都是在進程內進行協調。在進 程內進行協調我們可以使用語言,平臺,操作系統等為我們提供的機制。那么如果我們在一個分布式環境中呢?也就是我們的程序運行在不同的機器上,這些機器可 能位于同一個機架,同一個機房又或不同的數據中心。在這樣的環境中,我們要實現協調該怎么辦?那么這就是分布式協調服務要干的事情。
ok,可能有人會講,這個好像也不難。無非是將原來在同一個進程內的一些原語通過網絡實現在分布式環境中。是的,表面上是可以這么說。但分布式系統中,說往往比做容易得多。在分布式系統中,所有同一個進程內的任何假設都不存在:因為網絡是不可靠的。
比如,在同一個進程內,你對一個方法的調用如果成功,那就是成功(當然,如果你的代碼有bug那就另說了),如果調用失敗,比如拋出異常那就是調用 失敗。在同一個進程內,如果這個方法先調用先執行,那就是先執行。
但是在分布式環境中呢? 由于網絡的不可靠,你對一個服務的調用失敗了并不表示一定是失敗的,可能是執行成功了,但是響應返回的時候失敗了。
還有,A和B都去調用C服務,在時間上 A還先調用一些,B后調用,那么最后的結果是不是一定A的請求就先于B到達呢?
這些本來在同一個進程內的種種假設我們都要重新思考,我們還要思考這些問題給我們的設計和編碼帶來了哪些影響。還有,在分布式環境中為了提升可靠性,我們 往往會部署多套服務,但是如何在多套服務中達到一致性,這在同一個進程內很容易解決的問題,但在分布式環境中確實一個大難題。
所以分布式協調遠遠比同一個進程里的協調復雜得多,所以類似Zookeeper這類基礎服務就應運而生。這些系統都在各個系統久經考驗,它的可靠 性,可用性都是經過理論和實踐的驗證的。所以我們在構建一些分布式系統的時候,就可以以這類系統為起點來構建我們的系統,這將節省不少成本,而且bug也 將更少。
本篇文章試圖從外圍介紹一下Zookeeper是一個什么樣子的服務和我們為什么需要這樣一種服務。在后面的文章中會介紹Zookeeper到底能干些什么
Zookeeper-Zookeeper可以干什么
在Zookeeper的官網上有這么一句話:ZooKeeper is a centralized service for maintaining configuration information, naming, providing distributed synchronization, and providing group services.
這大概描述了Zookeeper主要可以干哪些事情:配置管理,名字服務,提供分布式同步以及集群管理。那這些服務又到底是什么呢?我們為什么需要 這樣的服務?
我們又為什么要使用Zookeeper來實現呢,使用Zookeeper有什么優勢?接下來我會挨個介紹這些到底是什么,以及有哪些開源系統 中使用了。
配置管理
在我們的應用中除了代碼外,還有一些就是各種配置。比如數據庫連接等。一般我們都是使用配置文件的方式,在代碼中引入這些配置文件。但是當我們只有 一種配置,只有一臺服務器,并且不經常修改的時候,使用配置文件是一個很好的做法,但是如果我們配置非常多,有很多服務器都需要這個配置,而且還可能是動 態的話使用配置文件就不是個好主意了。這個時候往往需要尋找一種集中管理配置的方法,我們在這個集中的地方修改了配置,所有對這個配置感興趣的都可以獲得 變更。比如我們可以把配置放在數據庫里,然后所有需要配置的服務都去這個數據庫讀取配置。但是,因為很多服務的正常運行都非常依賴這個配置,所以需要這個 集中提供配置服務的服務具備很高的可靠性。一般我們可以用一個集群來提供這個配置服務,但是用集群提升可靠性,那如何保證配置在集群中的一致性呢? 這個時候就需要使用一種實現了一致性協議的服務了。Zookeeper就是這種服務,它使用Zab這種一致性協議來提供一致性?,F在有很多開源項目使用 Zookeeper來維護配置,比如在HBase中,客戶端就是連接一個Zookeeper,獲得必要的HBase集群的配置信息,然后才可以進一步操 作。還有在開源的消息隊列Kafka中,也使用Zookeeper來維護broker的信息。在Alibaba開源的SOA框架Dubbo中也廣泛的使用 Zookeeper管理一些配置來實現服務治理。
名字服務
名字服務這個就很好理解了。比如為了通過網絡訪問一個系統,我們得知道對方的IP地址,但是IP地址對人非常不友好,這個時候我們就需要使用域名來 訪問。但是計算機是不能是別域名的。怎么辦呢?如果我們每臺機器里都備有一份域名到IP地址的映射,這個倒是能解決一部分問題,但是如果域名對應的IP發 生變化了又該怎么辦呢?于是我們有了DNS這個東西。我們只需要訪問一個大家熟知的(known)的點,它就會告訴你這個域名對應的IP是什么。在我們的 應用中也會存在很多這類問題,特別是在我們的服務特別多的時候,如果我們在本地保存服務的地址的時候將非常不方便,但是如果我們只需要訪問一個大家都熟知 的訪問點,這里提供統一的入口,那么維護起來將方便得多了。
分布式鎖
其實在第一篇文章中已經介紹了Zookeeper是一個分布式協調服務。這樣我們就可以利用Zookeeper來協調多個分布式進程之間的活動。比 如在一個分布式環境中,為了提高可靠性,我們的集群的每臺服務器上都部署著同樣的服務。但是,一件事情如果集群中的每個服務器都進行的話,那相互之間就要 協調,編程起來將非常復雜。而如果我們只讓一個服務進行操作,那又存在單點。通常還有一種做法就是使用分布式鎖,在某個時刻只讓一個服務去干活,當這臺服 務出問題的時候鎖釋放,立即fail over到另外的服務。這在很多分布式系統中都是這么做,這種設計有一個更好聽的名字叫Leader Election(leader選舉)。比如HBase的Master就是采用這種機制。但要注意的是分布式鎖跟同一個進程的鎖還是有區別的,所以使用的 時候要比同一個進程里的鎖更謹慎的使用。
集群管理
在分布式的集群中,經常會由于各種原因,比如硬件故障,軟件故障,網絡問題,有些節點會進進出出。有新的節點加入進來,也有老的節點退出集群。這個 時候,集群中其他機器需要感知到這種變化,然后根據這種變化做出對應的決策。比如我們是一個分布式存儲系統,有一個中央控制節點負責存儲的分配,當有新的 存儲進來的時候我們要根據現在集群目前的狀態來分配存儲節點。這個時候我們就需要動態感知到集群目前的狀態。還有,比如一個分布式的SOA架構中,服務是 一個集群提供的,當消費者訪問某個服務時,就需要采用某種機制發現現在有哪些節點可以提供該服務(這也稱之為服務發現,比如Alibaba開源的SOA框 架Dubbo就采用了Zookeeper作為服務發現的底層機制)。還有開源的Kafka隊列就采用了Zookeeper作為Cosnumer的上下線管 理。
后記
在這篇文章中,列出了一些Zookeeper可以提供的服務,并給出了一些開源系統里面的實例。后面我們從Zookeeper的安裝配置開始,并用示例進一步介紹Zookeeper如何使用。
Zookeeper-Zookeeper的配置
前面兩篇文章介紹了Zookeeper是什么和可以干什么,那么接下來 我們就實際的接觸一下Zookeeper這個東西,看看具體如何使用,有個大體的感受,后面再描述某些地方的時候也能在大腦中有具體的印象。本文只關注分 布式模式的zookeeper,因為這也是在生產環境的唯一部署方式,單機的zookeeper可以在測試和開發環境使用,但是單機環境的zookeeper就不再是zookeeper了。
安裝配置很簡單,官網也有介紹,這里就只對后面的文章有提到的點說明下。
配置-zoo.cfg
這是zookeeper的主要配置文件,因為Zookeeper是一個集群服務,集群的每個節點都需要這個配置文件。為了避免出差 錯,zoo.cfg這個配置文件里沒有跟特定節點相關的配置,所以每個節點上的這個zoo.cfg都是一模一樣的配置。這樣就非常便于管理了,比如我們可 以把這個文件提交到版本控制里管理起來。其實這給我們設計集群系統的時候也是個提示:集群系統一般有很多配置,應該盡量將通用的配置和特定每個服務的配置 (比如服務標識)分離,這樣通用的配置在不同服務之間copy就ok了。ok,下面來介紹一些配置點:
clientPort=2181
client port,顧名思義,就是客戶端連接zookeeper服務的端口。這是一個TCP port。
dataDir=/data
dataLogDir=/datalog
dataLogDir如果沒提供的話使用的則是dataDir。zookeeper的持久化都存儲在這兩個目錄里。dataLogDir里是放到的 順序日志(WAL)。而dataDir里放的是內存數據結構的snapshot,便于快速恢復。為了達到性能最大化,一般建議把dataDir和 dataLogDir分到不同的磁盤上,這樣就可以充分利用磁盤順序寫的特性。
下面是集群中服務的列表
server.1=127.0.0.1:20881:30881server.2=127.0.0.1:20882:30882server.3=127.0.0.1:20883:30883
在上面的例子中,我把三個zookeeper服務放到同一臺機器上。上面的配置中有兩個TCP port。后面一個是用于Zookeeper選舉用的,而前一個是Leader和Follower或Observer交換數據使用的。我們還注意到 server.后面的數字。這個就是myid(關于myid是什么下一節會介紹)。
上面這幾個是一些基本配置。
還有像 tickTime,這是個時間單位定量。比如tickTime=1000,這就表示在zookeeper里1 tick表示1000 ms,所有其他用到時間的地方都會用多少tick來表示。
比如 syncLimit = 2 就表示fowller與leader的心跳時間是2 tick。
maxClientCnxns -- 對于一個客戶端的連接數限制,默認是60,這在大部分時候是足夠了。但是在我們實際使用中發現,在測試環境經常超過這個數,經過調查發現有的團隊將幾十個應用全部部署到一臺機器上,以方便測試,于是這個數字就超過了。
minSessionTimeout, maxSessionTimeout -- 一般,客戶端連接zookeeper的時候,都會設置一個session timeout,如果超過這個時間client沒有與zookeeper server有聯系,則這個session會被設置為過期(如果這個session上有臨時節點,則會被全部刪除,這就是實現集群感知的基礎,后面的文章 會介紹這一點)。但是這個時間不是客戶端可以無限制設置的,服務器可以設置這兩個參數來限制客戶端設置的范圍。
autopurge.snapRetainCount,autopurge.purgeInterval -- 客戶端在與zookeeper交互過程中會產生非常多的日志,而且zookeeper也會將內存中的數據作為snapshot保存下來,這些數據是不會被 自動刪除的,這樣磁盤中這樣的數據就會越來越多。不過可以通過這兩個參數來設置,讓zookeeper自動刪除數據。 autopurge.purgeInterval就是設置多少小時清理一次。而autopurge.snapRetainCount是設置保留多少個 snapshot,之前的則刪除。
不過如果你的集群是一個非常繁忙的集群,然后又碰上這個刪除操作,可能會影響zookeeper集群的性能,所以一般會讓這個過程在訪問低谷的時候 進行,但是遺憾的是zookeeper并沒有設置在哪個時間點運行的設置,所以有的時候我們會禁用這個自動刪除的功能,而在服務器上配置一個cron,然 后在凌晨來干這件事。
以上就是zoo.cfg里的一些配置了。下面就來介紹myid。
配置-myid
在dataDir里會放置一個myid文件,里面就一個數字,用來唯一標識這個服務。這個id是很重要的,一定要保證整個集群中唯一。 zookeeper會根據這個id來取出server.x上的配置。比如當前id為1,則對應著zoo.cfg里的server.1的配置。
- 而且在后面我們介紹leader選舉的時候,這個id的大小也是有意義的。
OK,上面就是配置的講解了,現在我們可以啟動zookeeper集群了。進入到bin目錄,執行 ./zkServer.sh start即可。
?Zookeeper-Zookeeper啟動過程
在上一篇,我們了解了zookeeper最基本的配置,也從中了解一些配置的作用,那么這篇文章中,我們將介紹Zookeeper的啟動過程,我們在了解啟動過程的時候還要回過頭看看上一篇中各個配置參數在啟動時的位置。
Zookeeper的啟動入口在org.apache.zookeeper.server.quorum.QuorumPeerMain。
在這個類的main方法里進入了zookeeper的啟動過程,首先我們會解析配置文件,即zoo.cfg和myid。
這樣我們就知道了dataDir和dataLogDir指向哪兒了,然后就可以啟動日志清理任務了(如果配置了的話)。
DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config .getDataDir(), config.getDataLogDir(), config .getSnapRetainCount(), config.getPurgeInterval());purgeMgr.start();
接下來會初始化ServerCnxnFactory,這個是用來接收來自客戶端的連接的,也就是這里啟動的是一個tcp server。在Zookeeper里提供兩種tcp server的實現,一個是使用java原生NIO的方式,另外一個是使用Netty。默認是java nio的方式,一個典型的Reactor模型。因為java nio編程并不是本文的重點,所以在這里就只是簡單的介紹一下。
//首先根據配置創建對應factory的實例:NIOServerCnxnFactory 或者 NettyServerCnxnFactoryServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();//初始化配置cnxnFactory.configure(config.getClientPortAddress(),config.getMaxClientCnxns());
創建幾個SelectorThread處理具體的數據讀取和寫出。
先是創建ServerSocketChannel,bind等
this.ss = ServerSocketChannel.open();ss.socket().setReuseAddress(true);ss.socket().bind(addr);ss.configureBlocking(false);
然后創建一個AcceptThread線程來接收客戶端的連接。
這一部分就是處理客戶端請求的模塊了,如果遇到有客戶端請求的問題可以看看這部分。
接下來就進入初始化的主要部分了,首先會創建一個QuorumPeer實例,這個類就是表示zookeeper集群中的一個節點。初始化QuorumPeer的時候有這么幾個關鍵點: - 初始化FileTxnSnapLog,這個類主要管理Zookeeper中的操作日志(WAL)和snapshot。
- 初始化ZKDatabase,這個類就是Zookeeper的目錄結構在內存中的表示,所有的操作最后都會映射到這個類上面來。
- 初始化決議validator(QuorumVerifier->QuorumMaj) (其實這一步,是在配置)。這一步是從zoo.cfg的server.n這一部分初始化出集群的成員出來,有哪些需要參與投票(follower),有哪些只是observer。還有決定half是多少等,這些都是zookeeper的核心。在這一步,對于每個節點會初始化一個QuorumServer對象,并且放到allMembers,votingMembers,observingMembers這幾個map里。而且這里也對參與者的個數進行了一些判斷。
- leader選舉 這一步非常重要,也是zookeeper里最復雜而最精華的一部分。
到這里,我們的zookeeper就啟動完成了。后面我將會分三部分進一步深入理解zookeeper: - leader選舉
- 存儲
- 處理客戶端請求
Zookeeper-Zookeeper leader選舉
在上一篇文章中我們大致瀏覽了zookeeper的啟動過程,并且提到 在Zookeeper的啟動過程中leader選舉是非常重要而且最復雜的一個環節。那么什么是leader選舉呢?zookeeper為什么需要 leader選舉呢?zookeeper的leader選舉的過程又是什么樣子的?本文的目的就是解決這三個問題。
首先我們來看看什么是leader選舉。其實這個很好理解,leader選舉就像總統選舉一樣,每人一票,獲得多數票的人就當選為總統了。在 zookeeper集群中也是一樣,每個節點都會投票,如果某個節點獲得超過半數以上的節點的投票,則該節點就是leader節點了。
國家選舉總統是為了選一個最高統帥,治理國家。那么zookeeper集群選舉的目的又是什么呢?其實這個要清楚明白的解釋還是挺復雜的。我們可以 簡單點想這個問題:我們有一個zookeeper集群,有好幾個節點。每個節點都可以接收請求,處理請求。那么,如果這個時候分別有兩個客戶端向兩個節點 發起請求,請求的內容是修改同一個數據。比如客戶端c1,請求節點n1,請求是set a = 1; 而客戶端c2,請求節點n2,請求內容是set a = 2;
那么最后a是等于1還是等于2呢? 這在一個分布式環境里是很難確定的。解決這個問題有很多辦法,而zookeeper的辦法是,我們選一個總統出來,所有的這類決策都提交給總統一個人決策,那之前的問題不就沒有了么。
那我們現在的問題就是怎么來選擇這個總統呢? 在現實中,選擇****總統是需要宣講拉選票的,那么在zookeeper的世界里這又如何處理呢?我們還是show code吧。
在QuorumPeer的startLeaderElection方法里包含leader選舉的邏輯。Zookeeper默認提供了4種選舉方式,默認是第4種: FastLeaderElection。
我們先假設我們這是一個嶄新的集群,嶄新的集群的選舉和之前運行過一段時間的選舉是有稍許不同的,后面會提及。
節點狀態: 每個集群中的節點都有一個狀態 LOOKING, FOLLOWING, LEADING, OBSERVING。都屬于這4種,每個節點啟動的時候都是LOOKING狀態,如果這個節點參與選舉但最后不是leader,則狀態是 FOLLOWING,如果不參與選舉則是OBSERVING,leader的狀態是LEADING。
開始這個選舉算法前,每個節點都會在zoo.cfg上指定的監聽端口啟動監聽(server.1=127.0.0.1:20881:20882),這里的20882就是這里用于選舉的端口。
在FastLeaderElection里有一個Manager的內部類,這個類里有啟動了兩個線 程:WorkerReceiver, WorkerSender。為什么說選舉這部分復雜呢,我覺得就是這些線程就像左右互搏一樣,非常難以理解。顧名思 義,這兩個線程一個是處理從別的節點接收消息的,一個是向外發送消息的。對于外面的邏輯接收和發送的邏輯都是異步的。
這里配置好了,QuorumPeer的run方法就開始執行了,這里實現的是一個簡單的狀態機。因為現在是LOOKING狀態,所以進入LOOKING的分支,調用選舉算法開始選舉了:
setCurrentVote(makeLEStrategy().lookForLeader());
而在lookForLeader里主要是干什么呢?首先我們會更新一下一個叫邏輯時鐘的東西,這也是在分布式算法里很重要的一個概念,但是在這里先 不介紹,可以參考后面的論文。然后決定我要投票給誰。不過zookeeper這里的選舉真直白,每個節點都選自己(汗),選我,選我,選我...... 然后向其他節點廣播這個選舉信息。這里實際上并沒有真正的發送出去,只是將選舉信息放到由WorkerSender管理的一個隊列里。
synchronized(this){ //邏輯時鐘 logicalclock++; //getInitLastLoggedZxid(), getPeerEpoch()這里先不關心是什么,后面會討論 updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());}//getInitId() 即是獲取選誰,id就是myid里指定的那個數字,所以說一定要唯一private long getInitId(){ if(self.getQuorumVerifier().getVotingMembers().containsKey(self.getId())) return self.getId(); else return Long.MIN_VALUE;}//發送選舉信息,異步發送sendNotifications();
現在我們去看看怎么把投票信息投遞出去。這個邏輯在WorkerSender里,WorkerSender從sendqueue里取出投票,然后交 給QuorumCnxManager發送。因為前面發送投票信息的時候是向集群所有節點發送,所以當然也包括自己這個節點,所以 QuorumCnxManager的發送邏輯里會判斷,如果這個要發送的投票信息是發送給自己的,則不發送了,直接進入接收隊列。
public void toSend(Long sid, ByteBuffer b) { if (self.getId() == sid) { b.position(0); addToRecvQueue(new Message(b.duplicate(), sid)); } else { //發送給別的節點,判斷之前是不是發送過 if (!queueSendMap.containsKey(sid)) { //這個SEND_CAPACITY的大小是1,所以如果之前已經有一個還在等待發送,則會把之前的一個刪除掉,發送新的 ArrayBlockingQueue bq = new ArrayBlockingQueue(SEND_CAPACITY); queueSendMap.put(sid, bq); addToSendQueue(bq, b); } else { ArrayBlockingQueue bq = queueSendMap.get(sid); if(bq != null){ addToSendQueue(bq, b); } else { LOG.error("No queue for server " + sid); } } //這里是真正的發送邏輯了 connectOne(sid); } }
connectOne就是真正發送了。在發送之前會先把自己的id和選舉地址發送過去。然后判斷要發送節點的id是不是比自己的id大,如果大則不 發送了。如果要發送又是啟動兩個線程:SendWorker,RecvWorker(這種一個進程內許多不同種類的線程,各自干活的狀態真的很難理解)。 發送邏輯還算簡單,就是從剛才放到那個queueSendMap里取出,然后發送。并且發送的時候將發送出去的東西放到一個 lastMessageSent的map里,如果queueSendMap里是空的,就發送lastMessageSent里的東西,確保對方一定收到 了。
看完了SendWorker的邏輯,再來看看數據接收的邏輯吧。還記得前面提到的有個Listener在選舉端口上啟動了監聽么,現在這里應該接收 到數據了。我們可以看到receiveConnection方法。在這里,如果接收到的的信息里的id比自身的id小,則斷開連接,并嘗試發送消息給這個 id對應的節點(當然,如果已經有SendWorker在往這個節點發送數據,則不用了)。
如果接收到的消息的id比當前的大,則會有RecvWorker接收數據,RecvWorker會將接收到的數據放到recvQueue里。
而FastLeaderElection的WorkerReceiver線程里會不斷地從這個recvQueue里讀取Message處理。在 WorkerReceiver會處理一些協議上的事情,比如消息格式等。除此之外還會看看接收到的消息是不是來自投票成員。如果是投票成員,則會看看這個 消息里的狀態,如果是LOOKING狀態并且當前的邏輯時鐘比投票消息里的邏輯時鐘要高,則會發個通知過去,告訴誰是leader。在這里,剛剛啟動的嶄 新集群,所以邏輯時鐘基本上都是相同的,所以這里還沒判斷出誰是leader。不過在這里我們注意到如果當前節點的狀態是LOOKING的話,接收邏輯會 將接收到的消息放到FastLeaderElection的recvqueue里。而在FastLeaderElection會從這個recvqueue 里讀取東西。
這里就是選舉的主要邏輯了:totalOrderPredicate
protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {return ((newEpoch > curEpoch) || ((newEpoch == curEpoch) && ((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId))))); } - 判斷消息里的epoch是不是比當前的大,如果大則消息里id對應的server我就承認它是leader
- 如果epoch相等則判斷zxid,如果消息里的zxid比我的大我就承認它是leader
- 如果前面兩個都相等那就比較一下server id吧,如果比我的大我就承認它是leader。
關于前面兩個東西暫時我們不去關心它,對于新啟動的集群這兩者都是相等的。
那這樣看來server id的大小也是leader選舉的一環?。ㄓ械娜松聛碜⒍ň筒黄椒?,這都是命?。?。
最后我們來看看,很多文章所介紹的,如果超過一半的人說它是leader,那它就是leader的邏輯吧
private boolean termPredicate( HashMap votes, Vote vote) { HashSet set = new HashSet(); //遍歷已經收到的投票集合,將等于當前投票的集合取出放到set中 for (Map.Entry entry : votes.entrySet()) { if (self.getQuorumVerifier().getVotingMembers().containsKey(entry.getKey()) && vote.equals(entry.getValue())){ set.add(entry.getKey()); } } //統計set,也就是投某個id的票數是否超過一半 return self.getQuorumVerifier().containsQuorum(set); } public boolean containsQuorum(Set ackSet) { return (ackSet.size() > half); }
最后一關:如果選的是自己,則將自己的狀態更新為LEADING,否則根據type,要么是FOLLOWING,要么是OBSERVING。
到這里選舉就結束了。
這里介紹的是一個新集群啟動時候的選舉過程,啟動的時候就是根據zoo.cfg里的配置,向各個節點廣播投票,一般都是選投自己。然后收到投票后就會進行進行判斷。如果某個節點收到的投票數超過一半,那么它就是leader了。
了解了這個過程,我們來看看另外一個問題:
一個集群有3臺機器,掛了一臺后的影響是什么?掛了兩臺呢?
掛了一臺:掛了一臺后就是收不到其中一臺的投票,但是有兩臺可以參與投票,按照上面的邏輯,它們開始都投給自己,后來按照選舉的原則,兩個人都投票 給其中一個,那么就有一個節點獲得的票等于2,2 > (3/2)=1 的,超過了半數,這個時候是能選出leader的。
掛了兩臺: 掛了兩臺后,怎么弄也只能獲得一張票, 1 不大于 (3/2)=1的,這樣就無法選出一個leader了。
在前面介紹時,為了簡單我假設的是這是一個嶄新的剛啟動的集群,這樣的集群與工作一段時間后的集群有什么不同呢?不同的就是epoch和zxid這 兩個參數。在新啟動的集群里這兩個一般是相等的,而工作一段時間后這兩個參數有可能有的節點落后其他節點,至于是為什么,這個還要在后面的存儲和處理額胡 斷請求的文章里介紹。
- 關于邏輯時鐘,我們的分布式大牛Leslie Lamport曾寫過一篇論文:Time, Clocks, and the Ordering of Events in a Distributed System
Zookeeper-Zookeeper client
Zookeeper leader選舉后,準備看看Zookeeper的存儲和處理客戶端請求的時候發現,如果能看看Zookeeper的API是不是在理解后面的過程更好些呢。
Zookeeper的client是通過Zookeeper類提供的。前面曾經說過,Zookeeper給使用者提供的是一個類似操作系統的文件結 構,只不過這個結構是分布式的。可以理解為一個分布式的文件系統。我們可以通過Zookeeper來訪問這個分布式的文件系統。
Zookeeper的client api給我們提供以下這些API:
- create
在給定的path上創建節點,這個path就像文件系統的路徑,比如/myapp/data/1,在創建節點的時候還可以指定節點的類型:是永久節 點,永久順序節點,臨時節點,臨時順序節點。這個節點類型是非常強大的。永久節點一經創建就永久保留了,就像我們在文件系統上創建一個普通文件,這個文件 的生命周期跟創建它的應用沒有任何關系。而臨時節點呢,當創建這個臨時節點的應用與zookeeper之間的會話過期之后就會被zookeeper自動刪 除了。這個特性是實現很多功能的關鍵。比如我們做集群感知,我們的應用啟動的時候將自己的ip地址作為臨時節點創建在某個節點下面。當我們的應用因為某些 原因,比如網絡斷掉或者宕機,它與zookeeper的會話就會過期了,過期后這個臨時節點就刪除了。這樣我們就可以通過這個特性來感知到我們的服務的集 群有哪些機器是活者的。那么順序節點又是什么呢。一般,如果我們在指定的path上創建節點,如果這個節點已經被創建了,則會拋出一個 NodeExistsException的異常。如果我們在指定的路徑上創建順序節點,則Zookeeper會自動的在我們給定的path上加上一個順序 編號。這個特性就是實現分布式鎖的關鍵。假設我們有幾個節點共享一個資源,我們這幾個節點都想爭用這個資源,那我們就都向某個路徑創建臨時順序節點。然后 順序最小的那個就獲得鎖,然后如果某個節點釋放了鎖,那順序第二小的那個就獲得鎖,以此類推,這樣一個分布式的公平鎖就實現了。
除此之外,每個節點上還可以保存一些數據。 - delete 刪除給定節點。刪除節點的時候還可以給定一個version,只有路徑和version都匹配的時候節點才會被刪除。有了這個version在分布式環境 種我們就可以用樂觀鎖的方式來確保一致性。比如我們先讀取一下節點,獲得了節點的version,然后刪除,如果刪除成功了則說明在這之間沒有人操作過這 個節點,否則就是并發沖突了。
- exists 這個節點會返回一個Stat對象,如果給定的path不存在的話則返回null。這個方法有一個關鍵參數,可以提供一個Watcher對象。 Wathcer是Zookeeper強大功能的源泉。Watcher就是一個事件處理器,一個回調。比如這個exists方法,調用后,如果別人對這個 path上的節點進行操作,比如創建,刪除或設置數據,這個Wather都會接收到對應的通知。
- setData/getData 設置或獲取節點的數據,getData也可以設置Watcher
- getChildren 獲取子節點,可以設置Watcher
- sync zookeeper是一個集群,創建節點的時候只要半數以上的節點確認就認為是創建成功了,但是如果讀取的時候正好讀取到一個落后的節點上,那就有可能讀取到舊的數據,這個時候可以執行一個sync操作,這個操作可以確保讀取到最新的數據。
zookeeper的client api基本上介紹完了。zookeeper強大的功能都是通過這些API來實現的,zookeeper通過一個簡單的文件系統數據模型對外提供服務。通過臨時節點,Watcher等手段我們可以實現一些在分布式環境種很難做到的事情。