ZooKeeper 典型應用場景

ZooKeeper 是一個高可用的分布式數據管理與系統協調框架。基于對 Paxos 算法的實現,使該框架保證了分布式環境中數據的強一致性,也正是基于這樣的特性,使得 ZooKeeper 解決很多分布式問題。網上對 ZK 的應用場景也有丌少介紹,本文將結合作者身邊的項目例子,系統地對ZK 的應用場景進行一個分門歸類的介紹。

值得注意的是,ZK 并非天生就是為這些應用場景設計的,都是后來眾多開發者根據其框架的特性,利用其提供的一系列 API 接口(或者稱為原語集),摸索出來的典型使用方法。因此,也非常歡迎讀者分享你在 ZK 使用上的奇技淫巧。

什么是Zookeeper

zookeeper是一個高效的分布式協調服務,它暴露出一些公用服務,比如命名、配置管理、同步控制、群組服務等。我們可以使用ZK來實現比如達成共識、集群管理、leader選舉等。

zookeeper是一個高可用的分布式管理與協調框架,基于ZAB算法(原子消息廣播協議)的實現。該框架能夠保證分布式環境中數據的一致性。也正式基于這樣的特性,使得zookeeper成為了解決分布式一致性問題的利器。

順序一致性:從一個客戶端發起事務請求,最終將會嚴格的按照其發起的順序被應用到zookeeper中去。

原子性:所有事務請求的處理結果在整個集群中所有機器上的應用情況是一致的,也就是說,要么整個集群所有的機器都成功應用了某一個事務,要么就沒有應用,一定不會出現部分機器應用了該事務,而另一部分沒有應用的情況。

單一視圖:無論客戶端連接的是那個服務器,起看到服務端的數據模型都是一致的。

可靠性:一旦服務器成功的應用了一個事務,并完成對客戶端的響應,那么該事務所引起的服務端狀態將會被一致保留下來。除非有另外一個事務對其修改。

實時性:通常所說的實時性就是指一旦事務被成功應用,那么客戶端就能立刻從服務器上獲取變更后的新數據,zookeeper僅僅能保證在一段時間內,客戶端最終一定能從服務器讀取最新的數據狀態。


Zookeeper設計目標

目標1:簡單數據結構。zookeeper就是以簡單的樹形結構來進行相互協調的(也叫樹形名字空間)

目標2:構建集群。一般zookeeper集群通常由一組機器構成,一般3-5臺機器就可以組成一個zookeeper集群,只要集群中超過半數以上的機器能夠正常工作,那么整個集群就能夠正常對外提供服務。

目標3:順序訪問。對于來自每個客戶端的每個請求,zookeeper都會分配一個全局唯一的遞增編號,這個編號反應了所以事務操作的先后順序,應用程序可以使用zookeeper的這個特性來實現更高層次的同步。

目標4:高性能。由于zookeeper將全量數據存儲在內存中,并直接服務與所有的非事務請求,因此尤其是在讀操作為主的場景下性能非常突出。在JMater壓力測試下(100%讀請求場景下),其結果大約在12-13W的QPS。


數據發布與訂閱(配置中心)

發布與訂閱模型,即所謂的配置中心,顧名思義就是發布者將數據發布到 ZK 節點上,供訂閱者動態獲取數據,實現配置信息的集中式管理和動態更新。例如全局的配置信息,服務式服務框架的服務地址列表等就非常適合使用。

1、應用中用到的一些配置信息放到 ZK 上進行集中管理。這類場景通常是這樣:應用在啟動的時候會主動來獲取一次配置,同時,在節點上注冊一個 Watcher,這樣一來,以后每次配置有更新的時候,都會實時通知到訂閱的客戶端,從來達到獲取最新配置信息的目的。

2、分布式搜索服務中,索引的元信息和服務器集群機器的節點狀態存放在 ZK 的一些指定節點,供各個客戶端訂閱使用。

3、分布式日志收集系統。這個系統的核心工作是收集分布在不同機器的日志。收集器通常是按照應用來分配收集任務單元,因此需要在 ZK 上創建一個以應用名作為 path 的節點 P,并將這個應用的所有機器 ip,以子節點的形式注冊到節點 P 上,這樣一來就能夠實現機器變動的時候,能夠實時通知到收集器調整任務分配。

4、系統中有些信息需要動態獲取,并且還會存在人工手動去修改這個信息的發問。通常是暴露出接口,例如 JMX 接口,來獲取一些運行時的信息。引入 ZK 之后,就不用自己實現一套方案了,只要將這些信息存放到指定的 ZK 節點上即可。

注意:在上面提到的應用場景中,有個默認前提是:數據量很小,但是數據更新可能會比較快的場景。

負載均衡

這里說的負載均衡是指軟負載均衡。在分布式環境中,為了保證高可用性,通常同一個應用或同一個服務的提供方都會部署多份,達到對等服務。而消費者就須要在這些對等的服務器中選擇一個來執行相關的業務邏輯,其中比較典型的是消息中間件中的生產者,消費者負載均衡。

消息中間件中發布者和訂閱者的負載均衡,linkedin 開源的 KafkaMQ 和阿里開源的 metaq 都是通過 zookeeper 來做到生產者、消費者的負載均衡。這里以 metaq 為例如講下:

生產者負載均衡:

metaq 發送消息的時候,生產者在發送消息的時候必須選擇一臺 broker 上的一個分區來發送消息,因此metaq 在運行過程中,會把所有 broker和對應的分區信息全部注冊到 ZK 指定節點上,默認的策略是一個依次輪詢的過程,生產者在通過 ZK 獲取分區列表之后,會按照 brokerId 和partition 的順序排列組織成一個有序的分區列表,發送的時候按照從頭到尾循環往復的方式選擇一個分區來發送消息。

消費負載均衡:

在消費過程中,一個消費者會消費一個或多個分區中的消息,但是一個分區只會由一個消費者來消費。MetaQ 的消費策略是:

1. 每個分區針對同一個 group 只掛載一個消費者。

2. 如果同一個 group 的消費者數目大于分區數目,則多出來的消費者將不參與消費。

3. 如果同一個 group 的消費者數目小于分區數目,則有部分消費者需要額外承擔消費任務。

在某個消費者故障或者重啟等情況下,其他消費者會感知到這一變化(通過 zookeeper watch 消費者列表),然后重新進行負載均衡,保證所有的分區都有消費者進行消費。

命名服務(Naming Service)

命名服務也是分布式系統中比較常見的一類場景。在分布式系統中,通過使用命名服務,客戶端應用能夠根據指定名字來獲取資源或服務的地址,提供者等信息。被命名的實體通常可以是集群中的機器,提供的服務地址,進程對象等等——這些我們都可以統稱他們為名字(Name)。其中較為常見的就是一些分布式服務框架中的服務地址列表。通過調用 ZK 提供的創建節點的 API,能夠很容易創建一個全局唯一的 path,這個 path 就可以作為一個名稱。

阿里開源的分布式服務框架 Dubbo 中使用 ZooKeeper 來作為其命名服務,維護全局的服務地址列表。在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)定時檢測每個機器,或者每個機器自己定時向監控系統匯報“我還活著”。 這種做法可行,但是存在兩個比較明顯的問題:

1. 集群中機器有變動的時候,牽連修改的東西比較多。

2. 有一定的延時。

利用 ZooKeeper 有兩個特性,就可以實時另一種集群機器存活性監控系統:

a. 客戶端在節點 x 上注冊一個 Watcher,那么如果 x 的子節點變化了,會通知該客戶端。

b. 創建 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 發現自己下面的子節點滿足指定個數,就可以進行下一步按序進行處理了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容