zookeeper的原理和使用(一)
ZooKeeper是Hadoop Ecosystem中非常重要的元件,它的主要功能是為分散式系統提供一致性協調(Coordination)服務,與之對應的Google的類似服務叫Chubby。今天這篇文章分為三個部分來介紹ZooKeeper,第一部分介紹ZooKeeper的基本原理,第二部分介紹ZooKeeper提供的Client API的使用,第三部分介紹一些ZooKeeper典型的應用場景。
ZooKeeper基本原理
1. 資料模型
如上圖所示,ZooKeeper資料模型的結構與Unix檔案系統很類似,整體上可以看作是一棵樹,每個節點稱做一個ZNode。每個ZNode都可以通過其路徑唯一標識,比如上圖中第三層的第一個ZNode, 它的路徑是/app1/c1。在每個ZNode上可儲存少量資料(預設是1M, 可以通過配置修改, 通常不建議在ZNode上儲存大量的資料),這個特性非常有用,在後面的典型應用場景中會介紹到。另外,每個ZNode上還儲存了其Acl資訊,這裡需要注意,雖說ZNode的樹形結構跟Unix檔案系統很類似,但是其Acl與Unix檔案系統是完全不同的,每個ZNode的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 API
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資訊
ZooKeeper典型應用場景
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}
資料釋出與訂閱(配置中心) |
釋出與訂閱模型,即所謂的配置中心,顧名思義就是釋出者將資料釋出到ZK節點上,供訂閱者動態獲取資料,實現配置資訊的集中式管理和動態更新。例如全域性的配置資訊,服務式服務框架的服務地址列表等就非常適合使用。 |
注意:在上面提到的應用場景中,有個預設前提是:資料量很小,但是資料更新可能會比較快的場景。 |
負載均衡 |
這裡說的負載均衡是指軟負載均衡。在分散式環境中,為了保證高可用性,通常同一個應用或同一個服務的提供方都會部署多份,達到對等服務。而消費者就須要在這些對等的伺服器中選擇一個來執行相關的業務邏輯,其中比較典型的是訊息中介軟體中的生產者,消費者負載均衡。 |
訊息中介軟體中釋出者和訂閱者的負載均衡,linkedin開源的KafkaMQ和阿里開源的metaq都是通過zookeeper來做到生產者、消費者的負載均衡。這裡以metaq為例如講下: 生產者負載均衡:metaq傳送訊息的時候,生產者在傳送訊息的時候必須選擇一臺broker上的一個分割槽來發送訊息,因此metaq在執行過程中,會把所有broker和對應的分割槽資訊全部註冊到ZK指定節點上,預設的策略是一個依次輪詢的過程,生產者在通過ZK獲取分割槽列表之後,會按照brokerId和partition的順序排列組織成一個有序的分割槽列表,傳送的時候按照從頭到尾迴圈往復的方式選擇一個分割槽來發送訊息。
消費負載均衡: 在消費過程中,一個消費者會消費一個或多個分割槽中的訊息,但是一個分割槽只會由一個消費者來消費。MetaQ的消費策略是:
在某個消費者故障或者重啟等情況下,其他消費者會感知到這一變化(通過 zookeeper watch消費者列表),然後重新進行負載均衡,保證所有的分割槽都有消費者進行消費。 |
命名服務(Naming Service) |
命名服務也是分散式系統中比較常見的一類場景。在分散式系統中,通過使用命名服務,客戶端應用能夠根據指定名字來獲取資源或服務的地址,提供者等資訊。被命名的實體通常可以是叢集中的機器,提供的服務地址,遠端物件等等——這些我們都可以統稱他們為名字(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,那麼另一個系統能夠收到通知,並作出相應處理 |
總之,使用zookeeper來進行分散式通知和協調能夠大大降低系統之間的耦合 |
叢集管理與Master選舉 |
利用ZooKeeper有兩個特性,就可以實時另一種叢集機器存活性監控系統:
例如,監控系統在 /clusterServers 節點上註冊一個Watcher,以後每動態加機器,那麼就往 /clusterServers 下建立一個 EPHEMERAL型別的節點:/clusterServers/{hostname}. 這樣,監控系統就能夠實時知道機器的增減情況,至於後續處理就是監控系統的業務了。
在分散式環境中,相同的業務應用分佈在不同的機器上,有些業務邏輯(例如一些耗時的計算,網路I/O處理),往往只需要讓整個叢集中的某一臺機器進行執行,其餘機器可以共享這個結果,這樣可以大大減少重複勞動,提高效能,於是這個master選舉便是這種場景下的碰到的主要問題。 利用ZooKeeper的強一致性,能夠保證在分散式高併發情況下節點建立的全域性唯一性,即:同時有多個客戶端請求建立 /currentMaster 節點,最終一定只有一個客戶端請求能夠建立成功。利用這個特性,就能很輕易的在分散式環境中進行叢集選取了。 另外,這種場景演化一下,就是動態Master選舉。這就要用到?EPHEMERAL_SEQUENTIAL型別節點的特性了。 上文中提到,所有客戶端建立請求,最終只有一個能夠建立成功。在這裡稍微變化下,就是允許所有請求都能夠建立成功,但是得有個建立順序,於是所有的請求最終在ZK上建立結果的一種可能情況是這樣: /currentMaster/{sessionId}-1 ,?/currentMaster/{sessionId}-2 ,?/currentMaster/{sessionId}-3 ….. 每次選取序列號最小的那個機器作為Master,如果這個機器掛了,由於他建立的節點會馬上小時,那麼之後最小的那個機器就是Master了。 |
|
分散式鎖 |
分散式鎖,這個主要得益於ZooKeeper為我們保證了資料的強一致性。鎖服務可以分為兩類,一個是保持獨佔,另一個是控制時序。
|
分散式佇列 |
佇列方面,簡單地講有兩種,一種是常規的先進先出佇列,另一種是要等到佇列成員聚齊之後的才統一按序執行。對於第一種先進先出佇列,和分散式鎖服務中的控制時序場景基本原理一致,這裡不再贅述。
第二種佇列其實是在FIFO佇列的基礎上作了一個增強。通常可以在 /queue 這個znode下預先建立一個/queue/num 節點,並且賦值為n(或者直接給/queue賦值n),表示佇列大小,之後每次有佇列成員加入後,就判斷下是否已經到達佇列大小,決定是否可以開始執行了。這種用法的典型場景是,分散式環境中,一個大任務Task A,需要在很多子任務完成(或條件就緒)情況下才能進行。這個時候,凡是其中一個子任務完成(就緒),那麼就去 /taskList 下建立自己的臨時時序節點(CreateMode.EPHEMERAL_SEQUENTIAL),當 /taskList 發現自己下面的子節點滿足指定個數,就可以進行下一步按序進行處理了。 |