本文主要從應用的角度對ZooKeeper做了淺析,試圖闡明ZooKeeper是什么、主要應用場景有哪些、常用場景可以怎么設計,由于篇幅原因暫未寫ZooKeeper自身設計及一致性方案的實現等,歡迎大家共同探討、對有誤之處進行指正。
ZooKeeper是一個開源的分布式協調服務,由雅虎創建,是Google Chubby的開源實現,設計目標是將復雜且易出錯的分布式一致性服務封裝成高效可靠的原語集,以一系列簡單易用的接口提供給用戶使用。ZooKeeper同時是一個典型的分布式數據一致性解決方案,致力于提供一個高性能、高可用并且具有嚴格寫順序的分布式協調服務,分布式應用可以基于它實現如數據發布/訂閱、負載均衡、命名服務、分布式協調/通知、集群管理、Master選舉、分布式鎖和分布式隊列等功能,官方經典概述點這里。
隨著分布式架構的出現,很多分布式應用會遇到數據一致性問題,在解決該問題上ZooKeeper是唯一一個成熟穩定且被大規模應用的解決方案。ZooKeeper無論從穩定性、性能及易用性上來說都達到了一個工業級產品的標準。在工程實踐中ZooKeeper(以下簡稱ZK)已經作為核心組件被廣泛應用在很多大型分布式系統中,包括Hadoop、Hbase、Storm、Kafka、Dubbo等。
那從分布式應用的角度看ZK能看到一幅怎樣的視圖?一棵樹。這里所說的樹是一個共享的、樹形結構的名字空間,應用可以通過api在這個命名空間中創建組織結構為樹形的數據節點,并且這個空間下的各應用程序看到的視圖是一致的。應用可以通過ZK客戶端連接到服務端(通常是是機器數量為奇數的集群),同時會建立相應的會話。
上述節點對應于ZK服務器內存中一系列被為ZNode的數據節點,而ZNode之間的層級關系就像文件系統的目錄結構一樣,但和傳統的磁盤文件系統不同的是全量數據都存儲在內存中,以此來實現提高服務器吞吐、減少延遲的目的,從這一點來說應用只應該存儲控制信息和配置信息到ZNode,而不應該用它來存儲大量數據。
在ZK中“節點”分為兩類,第一類同樣是指構成zk集群的機器,稱之為機器節點;第二類則是指數據模型中的數據節點ZNode。ZK將所有數據存儲于內存中,數據模型是一顆ZNode Tree,由斜杠分隔的的路徑就是一個ZNode,如/app1/p_1,每個ZNode上都會保存數據內容,還會保存相應屬性信息。
ZNode可以分為持久節點和臨時節點兩類。持久節點是指一旦該ZNode被創建了,除非主動進行刪除操作,這個節點就會一直存在;而臨時節點的生命周期會和客戶端會話綁定在一起,一旦客戶端會話失效其所創建的所有臨時節點都會被刪除。
ZK還支持客戶端創建節點時指定一個特殊的SEQUENTIAL屬性,這個節點被創建的時候ZK會自動在其節點名后面追加上一個整形數字,該數字是一個由服務端維護的自增數字,以此實現創建名稱自增的順序節點。
Watcher是ZK中很重要的特性,ZK允許用戶在指定節點上注冊一些Watcher,在該節點相關特定事件(比如節點添加、刪除、子節點變更等)發生時Watcher會監聽到,ZK服務端會將事件通知到感興趣的客戶端上去,該機制是ZK實現分布式協調服務的重要特性。
通知的時候服務端只會告訴客戶端一個簡單的事件(通知狀態、事件類型、節點路徑)而不包含具體的變化信息(如原始數據及變更后的數據),客戶端如要具體信息再次主動去重新獲取數據;此外,無論是服務端還是客戶端,只要Watcher被觸發ZK就會將其刪除,因此在Watcher的使用上需要反復注冊,這樣輕量的設計有效減輕了服務端壓力,如果Watcher一直有效,節點更新頻繁時服務端會不斷向客戶端發送通知,對網絡及服務端性能影響會非常大。
Dubbo是集團開源的分布式服務框架,致力于提供高性能和透明化的遠程服務調用解決方案和基于服務框架展開的完整SOA服務治理方案。
其中服務自動發現是最核心的模塊之一,該模塊提供基于注冊中心的目錄服務,使服務消費方能夠動態的查找服務提供方,讓服務地址透明化,同時服務提供方可以平滑的對機器進行擴容和縮容,其注冊中心可以基于提供的外部接口來實現各種不同類型的注冊中心,例如數據庫、ZK和Redis等。接下來看一下基于ZK實現的Dubbo注冊中心。
/dubbo: 這是Dubbo在ZK上創建的根節點。
/dubbo/com.foo.BarService: 這是服務節點,代表了Dubbo的一個服務。
/dubbo/com.foo.BarService/Providers: 這是服務提供者的根節點,其子節點代表了每個服務的真正提供者。
/dubbo/com.foo.BarService/Consumers: 這是服務消費者的根節點,其子節點代表了沒一個服務的真正消費者
Dubbo基于ZK實現注冊中心的工作流程:
服務提供者:在初始化啟動的時候首先在/dubbo/com.foo.BarService/Providers節點下創建一個子節點,同時寫入自己的url地址,代表這個服務的一個提供者。
服務消費者:在啟動的時候讀取并訂閱ZooKeeper上/dubbo/com.foo.BarService/Providersz節點下的所有子節點,并解析所有提供者的url地址類作為該服務的地址列表,開始發起正常調用。同時在Consumers節點下創建一個臨時節點,寫入自己的url地址,代表自己是BarService的一個消費者
監控中心:監控中心是Dubbo服務治理體系的重要一部分,它需要知道一個服務的所有提供者和訂閱者及變化情況。監控中心在啟動的時候會通過ZK的/dubbo/com.foo.BarService節點來獲取所有提供者和消費者的url地址,并注冊Watcher來監聽其子節點變化情況。
所有服務提供者在ZK上創建的節點都是臨時節點,利用的是臨時節點的生命周期和客戶端會話綁定的特性,一旦提供者機器掛掉無法對外提供服務時該臨時節點就會從ZK上摘除,這樣服務消費者和監控中心都能感知到服務提供者的變化。
命名服務也是分布式系統中比較常見的一類場景,被命名的實體通??梢允羌褐械臋C器、提供的服務地址或遠程對象,其中較為常見的是一些分布式服務框架中的服務地址列表,通過使用命名服務客戶端應用能夠制定名字來獲取資源的實體、服務地址和提供者的信息等。
上層應用使用命名服務時可能僅需要一個全局唯一的名字,類似于數據庫中的唯一主鍵,用數據庫自增id是可以的,但分庫分表的情況下就無法依靠數據庫的自增屬性來唯一標識一條記錄了。另外UUID也是一種廣泛應用的ID實現方式,但如果是用UUID對服務進行命名的話就太不直觀了,從字面意思根本看不出其表達的含義。下面看下用ZK如何實現全局唯一ID的生成。
之前在ZNode介紹時提過,創建節點時可以設定為SEQUENTIAL順序節點,創建后API會返回這個節點的完整名字,利用這個特性我們就可以來生成全局唯一ID了。
所有客戶端根據自己的任務類型,在指定類型的任務下創建一個順序節點,例如“Job-”節點
節點創建完畢后會返回一個完整的節點名稱,如Job-0000000001
客戶端拿到這個返回值后拼接上type類型,例如type1-Job-000000001,這樣就可以作為一個全局唯一的ID了
在ZK中每個數據節點都能維護一份子節點的順序序列,當客戶端對其創建一個順序子節點時ZK會自動以后綴的形式在其子節點上添加一個序號,該場景就利用了ZK的這個特性。
Master選舉是一個在分布式系統中常見的應用場景。分布式系統中Master是一個往往用來協調集群中其他角色、處理寫操作及復雜邏輯、數據同步等,通常決定對分布式系統狀態變更。針對Master選舉的需求,我們通常可以使用依靠關系數據庫的主鍵特性在集群中選舉出唯一的Master,但是如果當前的Master掛了slave收不到通知怎么辦?顯然關系數據庫做不到通知這一點。
ZK創建節點時有一個重要的特性,利用ZK的強一致性能夠很好的保證在分布式高并發情況下節點的創建一定能夠保證全局唯一,即ZK會保證客戶端無法重復創建一個已經存在的數據節點。也就是說同時有多個客戶端請求創建同一個節點最終一定只有一個客戶端能夠請求創建成功,利用這個特性就能很容易的在分布式環境中進行Master選舉了。
進行Master選舉時客戶端啟動后可以向ZK請求創建一個臨時節點,例如/master_election/master。在多個客戶端創建時只有一個能創建成功,那么這個創建成功的客戶端所在的機器就成為了Master。同時其他沒有創建成功的客戶端都可以在節點/master_election上注冊一個子節點變更的Watcher來監控當前Master是否在線,一旦發現Master掛了臨時節點會被刪除,其它客戶端會收到通知,開始重新進行Master選舉。
因此,如果只要求靜態Maser選舉的話,可以選擇關系數據庫;如果需要動態Master選舉實現HA的話,ZK是更好的選擇。
分布式鎖是控制分布式系統之間同步訪問共享資源的一種方式。如果不同系統或同一系統不同機器之間共享了同一資源,那訪問這些資源時通常需要一些互斥手段來保證一致性,這種情況下就需要用到分布式鎖了。
使用關系型數據庫是一種簡單、廣泛的實現方案,但大多數大型分布式系統中數據庫已經是性能瓶頸了,如果再給數據庫添加額外的鎖會更加不堪重負;另外,使用數據庫做分布式鎖,當搶到鎖的機器掛掉的話如何釋放鎖也是個頭疼的問題。
接下來看下使用ZK如何實現排他鎖。排他鎖的核心是如何保證當前有且只有一個事務獲得鎖,并且鎖被釋放后所有等待獲取鎖的事務能夠被通知到。
和Master選舉類似,在需要獲取排他鎖時,所有客戶端都會試圖在/exclusive_lock下創建臨時子節點/exclusive_lock/lock,最終只有一個客戶端能創建成功,該客戶端就獲取到了鎖。同時沒有獲取到鎖的客戶端需要到/exclusive_lock節點上注冊一個子節點變更的Watcher監聽,用于實時監聽lock節點的變更情況。
/exclusive_lock/lock是一個臨時節點,在一下兩種情況下都有可能釋放鎖:
當獲取鎖的客戶端掛掉,ZK上的該節點會被刪除
正常執行完業務邏輯之后客戶端會主動將自己創建的臨時節點刪除。
無論在什么情況下刪除了lock臨時節點ZK都會通知在/exclusive_lock節點上注冊了子節點變更Watcher監聽的客戶端,重新發起鎖的獲取。
分布式屏障,舉個栗子,在大規模分布式并行計算的場景下,最終的合并計算需要基于很多并行計算的子結果來進行,即系統需要滿足特定的條件,一個隊列的元素必須都聚齊之后才能進行后續處理,否則一直等待??聪氯绾斡肸K來支持這種場景。
開始時/queue_barrier是一個存在的節點,數據內容賦值為一個數字n來代表滿足條件的機器總數,例如n=10表示只有當/queue_barrier節點下的子節點數量達到10后才會打開屏障繼續處理。然后所有的客戶端都會到/queue_barrier節點下創建一個臨時節點,如/queue_barrier/192.168.0.1。創建完節點之后根據以下步驟來確定執行順序
調用獲取節點數據的api獲取/queue_barrier節點的內容:10
調用獲取子節點總數的api獲取/queue_barrier下的所有子節點,并且注冊對子節點變更的Watcher監聽
統計子節點個數
如果子節點個數小于10則繼續等待,否則打開屏障繼續處理
接收到Watcher通知后,重復步驟2
其它常用場景還包括集群管理及分布式隊列等,相信大家都已經有方案了,這里就不再敘述。
在Hadoop中ZooKeeper主要用于實現HA做主備切換(類似上面講的Master選舉),同時在YARN中又特別提供了ZK來存儲應用的運行狀態。
Kafaka是由LinkedIn開源的分布式消息系統,是一個吞吐量極高的分布式消息系統,主要用于實現低延遲的發送和收集大量的事件和日志等活躍數據。Kafaka使用ZK作為其分布式協調框架,將消息生產、消息存儲和消息消費的過程結合起來,保持包括生產者消費者和Broker在內的所有組件無狀態的情況下,建立起生產者和消費者之間的訂閱關系,并實現生產者和消費者之間的負載均衡。
HBase全稱Hadoop DataBase,是一個基于Hadoop文件系統設計、面向海量數據的高可靠性、高性能、面向列、可伸縮的分布式存儲系統。在HBase向在線分布式存儲方向發展過程中,開發者發現如果有RegionServer服務器掛掉時系統和客戶端都無法及時得知信息,服務難以快速遷移到其它RegionServer服務器上,問題原因是缺少相應的分布式協調組件,于是后來ZooKeeper被加入到HBase的技術體系中。
目前ZooKeeper已經成為HBase的核心組件,應用場景包括系統容錯、RootRegion管理、Region狀態管理、分布式SplitLog任務管理和Replication管理,除此之外還包括HMaster選舉、Table的enable/disable狀態記錄及幾乎所有元數據的存儲等。Hbase中所有對ZK的操作都封裝在了org.apache.hadoop.hbase.zookeeper這個包中,感興趣可自行研究。