ZooKeeper是Hadoop Ecosystem中非常重要的組件,它的主要功能是為分布式系統提供一致性協調(Coordination)服務,與之對應的Google的類似服務叫Chubby。今天這篇文章分為三個部分來介紹ZooKeeper,第一部分介紹ZooKeeper的基本原理,第二部分介紹ZooKeeper提供的Client API的使用,第三部分介紹一些ZooKeeper典型的應用場景。
1. 數據模型
如上圖所示,ZooKeeper數據模型的結構與Unix文件系統很類似,整體上可以看作是一棵樹,每個節點稱做一個ZNode。每個ZNode都可以通過其路徑唯一標識,比如上圖中第三層的第一個ZNode, 它的路徑是/app1/c1。在每個ZNode上可存儲少量數據(默認是1M, 可以通過配置修改, 通常不建議在ZNode上存儲大量的數據),這個特性非常有用,在后面的典型應用場景中會介紹到。另外,每個ZNode上還存儲了其Acl信息,這里需要注意,雖說ZNode的樹形結構跟Unix文件系統很類似,但是其Acl與Unix文件系統是完全不同的,每個ZNode的Acl的獨立的,子結點不會繼承父結點的,關于ZooKeeper中的Acl可以參考之前寫過的一篇文章《說說Zookeeper中的ACL》。
2.重要概念
2.1 ZNode
前文已介紹了ZNode, ZNode根據其本身的特性,可以分為下面兩類:
Regular ZNode: 常規型ZNode, 用戶需要顯式的創建、刪除
Ephemeral ZNode: 臨時型ZNode, 用戶創建它之后,可以顯式的刪除,也可以在創建它的Session結束后,由ZooKeeper Server自動刪除
ZNode還有一個Sequential的特性,如果創建的時候指定的話,該ZNode的名字后面會自動Append一個不斷增加的SequenceNo。
2.2 Session
Client與ZooKeeper之間的通信,需要創建一個Session,這個Session會有一個超時時間。因為ZooKeeper集群會把Client的Session信息持久化,所以在Session沒超時之前,Client與ZooKeeper Server的連接可以在各個ZooKeeper Server之間透明地移動。
在實際的應用中,如果Client與Server之間的通信足夠頻繁,Session的維護就不需要其它額外的消息了。否則,ZooKeeper Client會每t/3 ms發一次心跳給Server,如果Client 2t/3 ms沒收到來自Server的心跳回應,就會換到一個新的ZooKeeper Server上。這里t是用戶配置的Session的超時時間。
2.3 Watcher
ZooKeeper支持一種Watch操作,Client可以在某個ZNode上設置一個Watcher,來Watch該ZNode上的變化。如果該ZNode上有相應的變化,就會觸發這個Watcher,把相應的事件通知給設置Watcher的Client。需要注意的是,ZooKeeper中的Watcher是一次性的,即觸發一次就會被取消,如果想繼續Watch的話,需要客戶端重新設置Watcher。這個跟epoll里的oneshot模式有點類似。
3. ZooKeeper特性
3.1 讀、寫(更新)模式
在ZooKeeper集群中,讀可以從任意一個ZooKeeper Server讀,這一點是保證ZooKeeper比較好的讀性能的關鍵;寫的請求會先Forwarder到Leader,然后由Leader來通過ZooKeeper中的原子廣播協議,將請求廣播給所有的Follower,Leader收到一半以上的寫成功的Ack后,就認為該寫成功了,就會將該寫進行持久化,并告訴客戶端寫成功了。
3.2 WAL和Snapshot
和大多數分布式系統一樣,ZooKeeper也有WAL(Write-Ahead-Log),對于每一個更新操作,ZooKeeper都會先寫WAL, 然后再對內存中的數據做更新,然后向Client通知更新結果。另外,ZooKeeper還會定期將內存中的目錄樹進行Snapshot,落地到磁盤上,這個跟HDFS中的FSImage是比較類似的。這么做的主要目的,一當然是數據的持久化,二是加快重啟之后的恢復速度,如果全部通過Replay WAL的形式恢復的話,會比較慢。
3.3 FIFO
對于每一個ZooKeeper客戶端而言,所有的操作都是遵循FIFO順序的,這一特性是由下面兩個基本特性來保證的:一是ZooKeeper Client與Server之間的網絡通信是基于TCP,TCP保證了Client/Server之間傳輸包的順序;二是ZooKeeper Server執行客戶端請求也是嚴格按照FIFO順序的。
3.4 Linearizability
在ZooKeeper中,所有的更新操作都有嚴格的偏序關系,更新操作都是串行執行的,這一點是保證ZooKeeper功能正確性的關鍵。
ZooKeeper Client Library提供了豐富直觀的API供用戶程序使用,下面是一些常用的API:
create(path, data, flags): 創建一個ZNode, path是其路徑,data是要存儲在該ZNode上的數據,flags常用的有: PERSISTEN, PERSISTENT_SEQUENTAIL, EPHEMERAL, EPHEMERAL_SEQUENTAIL
delete(path, version): 刪除一個ZNode,可以通過version刪除指定的版本, 如果version是-1的話,表示刪除所有的版本
exists(path, watch): 判斷指定ZNode是否存在,并設置是否Watch這個ZNode。這里如果要設置Watcher的話,Watcher是在創建ZooKeeper實例時指定的,如果要設置特定的Watcher的話,可以調用另一個重載版本的exists(path, watcher)。以下幾個帶watch參數的API也都類似
getData(path, watch): 讀取指定ZNode上的數據,并設置是否watch這個ZNode
setData(path, watch): 更新指定ZNode的數據,并設置是否Watch這個ZNode
getChildren(path, watch): 獲取指定ZNode的所有子ZNode的名字,并設置是否Watch這個ZNode
sync(path): 把所有在sync之前的更新操作都進行同步,達到每個請求都在半數以上的ZooKeeper Server上生效。path參數目前沒有用
setAcl(path, acl): 設置指定ZNode的Acl信息
getAcl(path): 獲取指定ZNode的Acl信息
1. 名字服務(NameService)
分布式應用中,通常需要一套完備的命令機制,既能產生唯一的標識,又方便人識別和記憶。 我們知道,每個ZNode都可以由其路徑唯一標識,路徑本身也比較簡潔直觀,另外ZNode上還可以存儲少量數據,這些都是實現統一的NameService的基礎。下面以在HDFS中實現NameService為例,來說明實現NameService的基本布驟:
目標:通過簡單的名字來訪問指定的HDFS機群
定義命名規則:這里要做到簡潔易記憶。下面是一種可選的方案: [serviceScheme://][zkCluster]-[clusterName],比如hdfs://lgprc-example/表示基于lgprc ZooKeeper集群的用來做example的HDFS集群
配置DNS映射: 將zkCluster的標識lgprc通過DNS解析到對應的ZooKeeper集群的地址
創建ZNode: 在對應的ZooKeeper上創建/NameService/hdfs/lgprc-example結點,將HDFS的配置文件存儲于該結點下
用戶程序要訪問hdfs://lgprc-example/的HDFS集群,首先通過DNS找到lgprc的ZooKeeper機群的地址,然后在ZooKeeper的/NameService/hdfs/lgprc-example結點中讀取到HDFS的配置,進而根據得到的配置,得到HDFS的實際訪問入口
2. 配置管理(Configuration Management)
在分布式系統中,常會遇到這樣的場景: 某個Job的很多個實例在運行,它們在運行時大多數配置項是相同的,如果想要統一改某個配置,一個個實例去改,是比較低效,也是比較容易出錯的方式。通過ZooKeeper可以很好的解決這樣的問題,下面的基本的步驟:
將公共的配置內容放到ZooKeeper中某個ZNode上,比如/service/common-conf
所有的實例在啟動時都會傳入ZooKeeper集群的入口地址,并且在運行過程中Watch /service/common-conf這個ZNode
如果集群管理員修改了了common-conf,所有的實例都會被通知到,根據收到的通知更新自己的配置,并繼續Watch /service/common-conf
3. 組員管理(Group Membership)
在典型的Master-Slave結構的分布式系統中,Master需要作為“總管”來管理所有的Slave, 當有Slave加入,或者有Slave宕機,Master都需要感知到這個事情,然后作出對應的調整,以便不影響整個集群對外提供服務。以HBase為例,HMaster管理了所有的RegionServer,當有新的RegionServer加入的時候,HMaster需要分配一些Region到該RegionServer上去,讓其提供服務;當有RegionServer宕機時,HMaster需要將該RegionServer之前服務的Region都重新分配到當前正在提供服務的其它RegionServer上,以便不影響客戶端的正常訪問。下面是這種場景下使用ZooKeeper的基本步驟:
Master在ZooKeeper上創建/service/slaves結點,并設置對該結點的Watcher
每個Slave在啟動成功后,創建唯一標識自己的臨時性(Ephemeral)結點/service/slaves/${slave_id},并將自己地址(ip/port)等相關信息寫入該結點
Master收到有新子結點加入的通知后,做相應的處理
如果有Slave宕機,由于它所對應的結點是臨時性結點,在它的Session超時后,ZooKeeper會自動刪除該結點
Master收到有子結點消失的通知,做相應的處理
4. 簡單互斥鎖(Simple Lock)
我們知識,在傳統的應用程序中,線程、進程的同步,都可以通過操作系統提供的機制來完成。但是在分布式系統中,多個進程之間的同步,操作系統層面就無能為力了。這時候就需要像ZooKeeper這樣的分布式的協調(Coordination)服務來協助完成同步,下面是用ZooKeeper實現簡單的互斥鎖的步驟,這個可以和線程間同步的mutex做類比來理解:
多個進程嘗試去在指定的目錄下去創建一個臨時性(Ephemeral)結點 /locks/my_lock
ZooKeeper能保證,只會有一個進程成功創建該結點,創建結點成功的進程就是搶到鎖的進程,假設該進程為A
其它進程都對/locks/my_lock進行Watch
當A進程不再需要鎖,可以顯式刪除/locks/my_lock釋放鎖;或者是A進程宕機后Session超時,ZooKeeper系統自動刪除/locks/my_lock結點釋放鎖。此時,其它進程就會收到ZooKeeper的通知,并嘗試去創建/locks/my_lock搶鎖,如此循環反復
5. 互斥鎖(Simple Lock without Herd Effect)
上一節的例子中有一個問題,每次搶鎖都會有大量的進程去競爭,會造成羊群效應(Herd Effect),為了解決這個問題,我們可以通過下面的步驟來改進上述過程:
每個進程都在ZooKeeper上創建一個臨時的順序結點(Ephemeral Sequential) /locks/lock_${seq}
${seq}最小的為當前的持鎖者(${seq}是ZooKeeper生成的Sequenctial Number)
其它進程都對只watch比它次小的進程對應的結點,比如2 watch 1, 3 watch 2, 以此類推
當前持鎖者釋放鎖后,比它次大的進程就會收到ZooKeeper的通知,它成為新的持鎖者,如此循環反復
這里需要補充一點,通常在分布式系統中用ZooKeeper來做Leader Election(選主)就是通過上面的機制來實現的,這里的持鎖者就是當前的“主”。
6. 讀寫鎖(Read/Write Lock)
我們知道,讀寫鎖跟互斥鎖相比不同的地方是,它分成了讀和寫兩種模式,多個讀可以并發執行,但寫和讀、寫都互斥,不能同時執行行。利用ZooKeeper,在上面的基礎上,稍做修改也可以實現傳統的讀寫鎖的語義,下面是基本的步驟:
每個進程都在ZooKeeper上創建一個臨時的順序結點(Ephemeral Sequential) /locks/lock_${seq}
${seq}最小的一個或多個結點為當前的持鎖者,多個是因為多個讀可以并發
需要寫鎖的進程,Watch比它次小的進程對應的結點
需要讀鎖的進程,Watch比它小的最后一個寫進程對應的結點
當前結點釋放鎖后,所有Watch該結點的進程都會被通知到,他們成為新的持鎖者,如此循環反復
7. 屏障(Barrier)
在分布式系統中,屏障是這樣一種語義: 客戶端需要等待多個進程完成各自的任務,然后才能繼續往前進行下一步。下用是用ZooKeeper來實現屏障的基本步驟:
Client在ZooKeeper上創建屏障結點/barrier/my_barrier,并啟動執行各個任務的進程
Client通過exist()來Watch /barrier/my_barrier結點
每個任務進程在完成任務后,去檢查是否達到指定的條件,如果沒達到就啥也不做,如果達到了就把/barrier/my_barrier結點刪除
Client收到/barrier/my_barrier被刪除的通知,屏障消失,繼續下一步任務
8. 雙屏障(Double Barrier)
雙屏障是這樣一種語義: 它可以用來同步一個任務的開始和結束,當有足夠多的進程進入屏障后,才開始執行任務;當所有的進程都執行完各自的任務后,屏障才撤銷。下面是用ZooKeeper來實現雙屏障的基本步驟:
進入屏障:
Client Watch /barrier/ready結點, 通過判斷該結點是否存在來決定是否啟動任務
每個任務進程進入屏障時創建一個臨時結點/barrier/process/${process_id},然后檢查進入屏障的結點數是否達到指定的值,如果達到了指定的值,就創建一個/barrier/ready結點,否則繼續等待
Client收到/barrier/ready創建的通知,就啟動任務執行過程
離開屏障:
Client Watch /barrier/process,如果其沒有子結點,就可以認為任務執行結束,可以離開屏障
每個任務進程執行任務結束后,都需要刪除自己對應的結點/barrier/process/${process_id}
轉載地址:http://www.wuzesheng.com/?p=2609
數據發布與訂閱(配置中心)
發布與訂閱模型,即所謂的配置中心,顧名思義就是發布者將數據發布到ZK節點上,供訂閱者動態獲取數據,實現配置信息的集中式管理和動態更新。例如全局的配置信息,服務式服務框架的服務地址列表等就非常適合使用。
應用中用到的一些配置信息放到ZK上進行集中管理。這類場景通常是這樣:應用在啟動的時候會主動來獲取一次配置,同時,在節點上注冊一個Watcher,這樣一來,以后每次配置有更新的時候,都會實時通知到訂閱的客戶端,從來達到獲取最新配置信息的目的。
分布式搜索服務中,索引的元信息和服務器集群機器的節點狀態存放在ZK的一些指定節點,供各個客戶端訂閱使用。
分布式日志收集系統。這個系統的核心工作是收集分布在不同機器的日志。收集器通常是按照應用來分配收集任務單元,因此需要在ZK上創建一個以應用名作為path的節點P,并將這個應用的所有機器ip,以子節點的形式注冊到節點P上,這樣一來就能夠實現機器變動的時候,能夠實時通知到收集器調整任務分配。
系統中有些信息需要動態獲取,并且還會存在人工手動去修改這個信息的發問。通常是暴露出接口,例如JMX接口,來獲取一些運行時的信息。引入ZK之后,就不用自己實現一套方案了,只要將這些信息存放到指定的ZK節點上即可。
注意:在上面提到的應用場景中,有個默認前提是:數據量很小,但是數據更新可能會比較快的場景。
負載均衡
這里說的負載均衡是指軟負載均衡。在分布式環境中,為了保證高可用性,通常同一個應用或同一個服務的提供方都會部署多份,達到對等服務。而消費者就須要在這些對等的服務器中選擇一個來執行相關的業務邏輯,其中比較典型的是消息中間件中的生產者,消費者負載均衡。
消息中間件中發布者和訂閱者的負載均衡,linkedin開源的KafkaMQ和阿里開源的metaq都是通過zookeeper來做到生產者、消費者的負載均衡。這里以metaq為例如講下:
生產者負載均衡:metaq發送消息的時候,生產者在發送消息的時候必須選擇一臺broker上的一個分區來發送消息,因此metaq在運行過程中,會把所有broker和對應的分區信息全部注冊到ZK指定節點上,默認的策略是一個依次輪詢的過程,生產者在通過ZK獲取分區列表之后,會按照brokerId和partition的順序排列組織成一個有序的分區列表,發送的時候按照從頭到尾循環往復的方式選擇一個分區來發送消息。
消費負載均衡:
在消費過程中,一個消費者會消費一個或多個分區中的消息,但是一個分區只會由一個消費者來消費。MetaQ的消費策略是:
每個分區針對同一個group只掛載一個消費者。
如果同一個group的消費者數目大于分區數目,則多出來的消費者將不參與消費。
如果同一個group的消費者數目小于分區數目,則有部分消費者需要額外承擔消費任務。
在某個消費者故障或者重啟等情況下,其他消費者會感知到這一變化(通過 zookeeper watch消費者列表),然后重新進行負載均衡,保證所有的分區都有消費者進行消費。
命名服務(Naming Service)
命名服務也是分布式系統中比較常見的一類場景。在分布式系統中,通過使用命名服務,客戶端應用能夠根據指定名字來獲取資源或服務的地址,提供者等信息。被命名的實體通??梢允羌褐械臋C器,提供的服務地址,遠程對象等等——這些我們都可以統稱他們為名字(Name)。其中較為常見的就是一些分布式服務框架中的服務地址列表。通過調用ZK提供的創建節點的API,能夠很容易創建一個全局唯一的path,這個path就可以作為一個名稱。
阿里巴巴集團開源的分布式服務框架Dubbo中使用ZooKeeper來作為其命名服務,維護全局的服務地址列表,點擊這里查看Dubbo開源項目。在Dubbo實現中:
服務提供者在啟動的時候,向ZK上的指定節點/dubbo/${serviceName}/providers目錄下寫入自己的URL地址,這個操作就完成了服務的發布。
服務消費者啟動的時候,訂閱/dubbo/${serviceName}/providers目錄下的提供者URL地址, 并向/dubbo/${serviceName} /consumers目錄下寫入自己的URL地址。
注意,所有向ZK上注冊的地址都是臨時節點,這樣就能夠保證服務提供者和消費者能夠自動感應資源的變化。
另外,Dubbo還有針對服務粒度的監控,方法是訂閱/dubbo/${serviceName}目錄下所有提供者和消費者的信息。
分布式通知/協調
ZooKeeper中特有watcher注冊與異步通知機制,能夠很好的實現分布式環境下不同系統之間的通知與協調,實現對數據變更的實時處理。使用方法通常是不同系統都對ZK上同一個znode進行注冊,監聽znode的變化(包括znode本身內容及子節點的),其中一個系統update了znode,那么另一個系統能夠收到通知,并作出相應處理
另一種心跳檢測機制:檢測系統和被檢測系統之間并不直接關聯起來,而是通過zk上某個節點關聯,大大減少系統耦合。
另一種系統調度模式:某系統有控制臺和推送系統兩部分組成,控制臺的職責是控制推送系統進行相應的推送工作。管理人員在控制臺作的一些操作,實際上是修改了ZK上某些節點的狀態,而ZK就把這些變化通知給他們注冊Watcher的客戶端,即推送系統,于是,作出相應的推送任務。
另一種工作匯報模式:一些類似于任務分發系統,子任務啟動后,到zk來注冊一個臨時節點,并且定時將自己的進度進行匯報(將進度寫回這個臨時節點),這樣任務管理者就能夠實時知道任務進度。
總之,使用zookeeper來進行分布式通知和協調能夠大大降低系統之間的耦合
集群管理與Master選舉
集群機器監控:這通常用于那種對集群中機器狀態,機器在線率有較高要求的場景,能夠快速對集群中機器變化作出響應。這樣的場景中,往往有一個監控系統,實時檢測集群機器是否存活。過去的做法通常是:監控系統通過某種手段(比如ping)定時檢測每個機器,或者每個機器自己定時向監控系統匯報“我還活著”。 這種做法可行,但是存在兩個比較明顯的問題:
集群中機器有變動的時候,牽連修改的東西比較多。
有一定的延時。
利用ZooKeeper有兩個特性,就可以實時另一種集群機器存活性監控系統:
客戶端在節點 x 上注冊一個Watcher,那么如果 x?的子節點變化了,會通知該客戶端。
創建EPHEMERAL類型的節點,一旦客戶端和服務器的會話結束或過期,那么該節點就會消失。
例如,監控系統在 /clusterServers 節點上注冊一個Watcher,以后每動態加機器,那么就往 /clusterServers 下創建一個 EPHEMERAL類型的節點:/clusterServers/{hostname}. 這樣,監控系統就能夠實時知道機器的增減情況,至于后續處理就是監控系統的業務了。
Master選舉則是zookeeper中最為經典的應用場景了。
在分布式環境中,相同的業務應用分布在不同的機器上,有些業務邏輯(例如一些耗時的計算,網絡I/O處理),往往只需要讓整個集群中的某一臺機器進行執行,其余機器可以共享這個結果,這樣可以大大減少重復勞動,提高性能,于是這個master選舉便是這種場景下的碰到的主要問題。
利用ZooKeeper的強一致性,能夠保證在分布式高并發情況下節點創建的全局唯一性,即:同時有多個客戶端請求創建 /currentMaster 節點,最終一定只有一個客戶端請求能夠創建成功。利用這個特性,就能很輕易的在分布式環境中進行集群選取了。
另外,這種場景演化一下,就是動態Master選舉。這就要用到?EPHEMERAL_SEQUENTIAL類型節點的特性了。
上文中提到,所有客戶端創建請求,最終只有一個能夠創建成功。在這里稍微變化下,就是允許所有請求都能夠創建成功,但是得有個創建順序,于是所有的請求最終在ZK上創建結果的一種可能情況是這樣: /currentMaster/{sessionId}-1 ,?/currentMaster/{sessionId}-2 ,?/currentMaster/{sessionId}-3 ….. 每次選取序列號最小的那個機器作為Master,如果這個機器掛了,由于他創建的節點會馬上小時,那么之后最小的那個機器就是Master了。
在搜索系統中,如果集群中每個機器都生成一份全量索引,不僅耗時,而且不能保證彼此之間索引數據一致。因此讓集群中的Master來進行全量索引的生成,然后同步到集群中其它機器。另外,Master選舉的容災措施是,可以隨時進行手動指定master,就是說應用在zk在無法獲取master信息時,可以通過比如http方式,向一個地方獲取master。
在Hbase中,也是使用ZooKeeper來實現動態HMaster的選舉。在Hbase實現中,會在ZK上存儲一些ROOT表的地址和HMaster的地址,HRegionServer也會把自己以臨時節點(Ephemeral)的方式注冊到Zookeeper中,使得HMaster可以隨時感知到各個HRegionServer的存活狀態,同時,一旦HMaster出現問題,會重新選舉出一個HMaster來運行,從而避免了HMaster的單點問題
分布式鎖
分布式鎖,這個主要得益于ZooKeeper為我們保證了數據的強一致性。鎖服務可以分為兩類,一個是保持獨占,另一個是控制時序。
所謂保持獨占,就是所有試圖來獲取這個鎖的客戶端,最終只有一個可以成功獲得這把鎖。通常的做法是把zk上的一個znode看作是一把鎖,通過create znode的方式來實現。所有客戶端都去創建 /distribute_lock 節點,最終成功創建的那個客戶端也即擁有了這把鎖。
控制時序,就是所有視圖來獲取這個鎖的客戶端,最終都是會被安排執行,只是有個全局時序了。做法和上面基本類似,只是這里 /distribute_lock 已經預先存在,客戶端在它下面創建臨時有序節點(這個可以通過節點的屬性控制:CreateMode.EPHEMERAL_SEQUENTIAL來指定)。Zk的父節點(/distribute_lock)維持一份sequence,保證子節點創建的時序性,從而也形成了每個客戶端的全局時序。
分布式隊列
隊列方面,簡單地講有兩種,一種是常規的先進先出隊列,另一種是要等到隊列成員聚齊之后的才統一按序執行。對于第一種先進先出隊列,和分布式鎖服務中的控制時序場景基本原理一致,這里不再贅述。
第二種隊列其實是在FIFO隊列的基礎上作了一個增強。通??梢栽?/queue 這個znode下預先建立一個/queue/num 節點,并且賦值為n(或者直接給/queue賦值n),表示隊列大小,之后每次有隊列成員加入后,就判斷下是否已經到達隊列大小,決定是否可以開始執行了。這種用法的典型場景是,分布式環境中,一個大任務Task A,需要在很多子任務完成(或條件就緒)情況下才能進行。這個時候,凡是其中一個子任務完成(就緒),那么就去 /taskList 下建立自己的臨時時序節點(CreateMode.EPHEMERAL_SEQUENTIAL),當 /taskList 發現自己下面的子節點滿足指定個數,就可以進行下一步按序進行處理了。
原文地址:http://blog.csdn.net/xinguan1267/article/details/38422149