Zookeeper基本功能和應用場景

  • 2019 年 11 月 3 日
  • 筆記

1. 簡介

Zookeeper是一個開源的分佈式的,為分佈式應用提供協調服務的Apache項目。是大數據Hadoop生態體系中用的非常廣泛的基礎組件

2. Zookeeper基本功能和應用場景

2.1 基本功能

  • 文件系統:zookeeper可以為客戶端管理狀態信息,雖然存儲的數量較小(每個節點默認不可超過1M)
  (k - v):    /aa/bb "hello"    /aa    "word"    /cc    "hadoop"
  • 通知機制:可以為客戶端監聽指定數據節點的狀態,並在數據節點發生改變時,通知客戶端

總上所述,我們可以認為 zookeeper = 文件系統 + 通知機制

2.3 Zookeeper應用場景

2.3.1 數據發佈和訂閱

數據發佈/訂閱系統,需要發佈者將數據發佈到zk的節點上,供訂閱者進行數據訂閱,進而達到動態獲取數據的目的,實現配置信息的集中式管理和數據的動態更新。

發佈/訂閱一般有兩種設計模式:推模式和拉模式,服務端主動將數據更新發送給所有的訂閱的客戶端稱為推模式;客戶端註冊請求獲取最新數據稱為拉模式,zk採用了推拉相結合的模式,客戶端將服務器註冊自己需要關注的節點,一旦節點數據發生變更,那麼服務器就會向相應的客戶端推送Watcher事件通知,客戶端接收到此通知後,主動到服務端獲取最新的數據。

注意:對於像Dubbo這樣的RPC框架來說,zk將作為註冊中心,客戶端第一次通過向zk集群獲取服務的地址,然後會存儲到本地,下一次進行調用時就不會再去zk集群查詢了,而是直接使用本地存儲的地址,只要當服務地址發生變更時,才會通知客戶端再次獲取。

在平時開發中,經常會碰到這樣的需求:系統中需要使用一些通用的配置信息,例如:機器列表信息,數據的配置信息(比如:要實現數據庫切換的應用場景),運行時的開關配置等。這些全局配置信息通常有3個特性:數據量通常比較小數據內容在運行時會發生動態變化集群中各機器共享、配置一致。假設,我們的集群規模很大,且配置信息經常變更,所以通過存儲本地配置文件或內存變量的形式實現都很困難,這時就需要zk來做一個全局配置信息管理。

2.3.2 負載均衡

負載均衡是一種相當常見的計算機網絡技術,用來對多個計算機、網絡連接、CPU、磁盤驅動或其他資源進行分配負載,已達到優化資源使用最大化吞吐量最小化響應時間避免過載的目的。

通常負載均衡可以分為硬件(F5)和軟件(Nginx)負載均衡兩類。Zookeeper也可以作為實現軟負載的一種形式。

分佈式系統為保證可用性,通常通過副本的方式對數據和服務進行部署,而對於客戶端來說,只需要在這樣對等的服務提供方中選擇一個執行相關的業務邏輯,怎麼選,這就是負載均衡的應用。

比如,典型的需要負載均衡的DNS(Domain Name System)服務,我們可以用zookeeper實現動態的DNS方案(具體可自行百度)

zk實現負載均衡就是通過 watcher 機制臨時節點判斷哪些節點宕機來獲取可用的節點來實現的: zk會維護一個樹形的數據結構,類似於window的資源管理器目錄,其中 EPHEMERAL(臨時)節點會隨着創建它的客戶端端口而被刪除,利用這個特性很容易實現軟負載均衡。

基本原理:每個應用的Server啟動時創建一個 EPHEMERAL 節點,應用客戶端通過讀取節點列表獲得可用服務器列表,並訂閱節點事件,有Server宕機斷開時觸發事件,客戶端監測到後把Server從可用列表中刪除。

消息中間件中發佈者和訂閱者的負載均衡,linkedin開源的KafkaMQ和阿里開源的MetaQ都是通過Zookeeper來做生產者、消費者負載均衡的。

2.3.3 命名服務

命名服務是分佈式系統中較為常見的一類場景,分佈式系統中,被命名的實體通常可以是集群中的機器、提供的服務地址或遠程對象等。通過命名服務,客戶端可以根據指定名字來獲取資源的實體、服務地址和提供者信息,最為創建的就是RPC框架的服務地址列表命名。

zk也可幫助應用系統通過資源引用的方式來實現對象資源的定位和使用,廣義上的命名服務的資源定位都不是真正意義的實體資源,在分佈式環境中,上層應用僅僅需要一個全局唯一的名字。zk可以實現一套分佈式全局唯一ID的分配機制。(用UUID的方式問題在於生成的字符串過長,浪費存儲空間且字符串無規律不利於開發調試)通過調用zk節點創建API接口就可以創建一個順序節點,並且在API返回值中返回這個節點的完整名稱,利用此特性,可以生成全局ID,其步驟如下:

1. 客戶端根據任務類型,在指定類型的任務下通過調用接口創建一個順序節點,如"job-".  2. 創建完成後,會返回一個完整的節點名,如"job-0000001".  3. 客戶端拼接type類型和返回值後,就可以作為全局唯一的ID 了,如"type2-job-0000001".

阿里開源分佈式服務框架Dubbo中使用zookeeper來作為其命名服務,維護全局的服務列表。在Dubbo實現中:

  • 服務提供者:啟動的時候,向zk的指定節點 /dubbo/${serverName}/providers 目錄下寫入自己的URL地址,這個操作完成了服務的發佈。
  • 服務消費者:啟動的時候,訂閱 /dubbo/${serverName}/providers 目錄下的提供者URL地址,並向 /dubbo/${serverName}/consumers 目錄下寫入自己的URL地址。 注意:所有向ZK上註冊的地址都是臨時節點,這樣就能保證服務提供者和消費者能夠自動感應資源的變化。另外,Dubbo還有針對服務粒度的監控,方法是訂閱/dubbo/${serverName}目錄下的所有提供者和消費者信息。

2.3.4 分佈式協調通知

zk中特有的 Watcher註冊於異步通知機制,能夠很好地實現分佈式環境下不同機器,甚至不同系統之間的協調和通知,從而實現對數據變更的實時處理。通常的做法是不同的客戶端對zk上同一個數據節點進行Watcher註冊,監聽數據節點的變化(包括節點本身和子節點),若數據節點發生變化,那麼所有訂閱的客戶端都能夠接受到相應的Watcher通知,並作出相應的處理。

在絕大多數分佈式系統中,系統機器間的通信無外乎心跳檢測、工作進度彙報和系統調度。這三種類型的機器通信方式都可以使用zookeeper來實現:

  1. 心跳檢測:不同機器間需要檢測到彼此知否在正常運行,可以使用zk實現機器間的心跳檢測,基於其臨時節點特性(臨時節點的生存周期是客戶端會話,客戶端若宕機後,其臨時節點自然不再存在),可以讓不同機器都在zookeeper的指定節點下創建臨時子節點,不同的機器之間可以根據這個臨時自己點來判斷對應的客戶端機器是否存活。通過zookeeper可以大大減少系統耦合。
  2. 工作進度彙報:通常任務被分發到不同的機器後,需要實時的將自己的任務執行進度彙報給分發系統,可以在zookeeper上選擇一個節點,每個任務客戶端都在這個節點下面創建臨時子節點,這不僅可以判斷機器是否存活,同時各個機器可以將自己的任務執行進度寫到該臨時子節點中去,以便中心系統能夠實時獲取任務的執行進度。
  3. 系統調度:zookeeper能夠實現如下系統調度模式:分佈式系統有控制台和一些客戶端系統兩部分構成,控制台的職責就是需要將一些指令信息發送給所有的客戶端,以控制他們進行相應的業務邏輯,後台管理人員在控制台上做一些操作,實際上就是修改zookeeper上某個節點的數據,zookeeper可以把數據變更以時間通知的形式發送給訂閱客戶端。

2.3.5 集群管理

zookeeper的兩大特性:節點特性watcher機制

  • 對在zookeeper上創建的臨時節點,一旦客戶端與服務器之間的會話失效,那麼臨時節點就會被自動刪除。
  • 客戶端如果對zookeeper的數據節點註冊watcher監聽,那麼當該數據節點內容或是其子節點列表發生變更時,zookeeper服務器就會向訂閱的客戶端發送變更通知。

機器在線率有較高要求的場景,能夠快速對集群中機器變化做出響應。這樣的場景中,往往有一個監控系統,實時監測集群機器是否存活。過去的做法通常是:監控系統通過某種手段(比如ping)定時監測每個機器,或者每個機器自己定時向監控系統彙報「我還活着」。這種做法可行,但是存在兩個比較明顯的問題:

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

利用zookeeper的兩個特性,就可以實現另一種集群機器存活性監控系統。若監控系統在 /clusterServers 節點上註冊一個 watcher監聽,那麼但凡進行動態添加機器的操作,就會在 /clusterServices 節點下創建一個臨時節點:/clusterService/[Hostname],這樣,監控系統就能夠實時監測機器的變動情況。

下面通過分佈式日誌收集系統的典型應用來學習zookeeper如何實現集群管理。

分佈式日誌收集系統的核心工作就是收集分佈在不同機器上的系統日誌,在典型的日誌系統架構設計中,整個日誌系統會把所有需要收集的日誌機器分為多個組別,每個組別對應一個收集器,這個收集器其實就是一個後台機器,用於收集日誌,對於大規模的分佈式日誌收集系統場景,通常需要解決兩個問題:

  • 變化的日誌源機器
  • 變化的收集器機器

無論是日誌源機器還是收集器機器的變更,最終都可以歸結為如何快速、合理、動態地為每個收集器分配對應的日誌源機器。

  1. 註冊收集器機器,在zookeeper上創建一個節點作為收集器的根節點,例如 /logs/collector 的收集器節點,每個收集器機器啟動時都會在收集器節點上創建自己的子節點,如/logs/collector/[Hostname]
  1. 任務分發,所有的收集器機器都創建完對應的節點後,系統根據收集器節點下子節點的個數,將所有的日誌源機器分成對應的若干組,然後將分組後的機器列表分別寫到這些收集器創建的子節點,如 /logs/collector/host1(持久節點)上去。這樣,收集器機器就能夠根據自己對應的收集器節點上獲取日誌源機器列表,進而開始進行日誌的收集工作。
  2. 狀態彙報,完成分發後,機器隨時會宕機,所以需要一個收集器的狀態彙報機制,每個收集器機器上創建完節點後,還需要在對應的子節點上創建一個狀態子節點,如/logs/collector/host/status(臨時節點),每個收集器機器都需要定期向該節點寫入自己的狀態信息,這可看做是心跳檢測機制,通常收集器機器都會寫入日誌收集狀態信息,日誌系統通過判斷狀態子節點最後的變更時間,一旦有機器停止彙報或有新機器加入,就開始進行任務的重新分配,此時通常有兩種做法:
    • 全局動態分配,當收集器宕機或有新的機器加入,系統根據新的收集器列表,立即對所有的日誌源機器重新進行一次分組,然後將其分配給剩下的收集器機器。
    • 局部動態分配,每個收集器機器在彙報自己日誌收集狀態的同時,也會把自己的負載彙報上去,如果一個機器宕機了,那麼日誌系統會把之前分配給這個機器的任務重新分配給那些負載較低的機器,同樣,如果有新的機器加入,會從那些負載較高的機器上轉移一部分任務給新機器。

2.3.6 Master選舉

在分佈式系統中,Master往往用來協調集群中其他系統單元,具有對分佈式系統狀態變更的決定權,如在讀寫分離的應用場景中,客戶端的寫請求往往是有Master來處理,或者其常常處理一些複雜的邏輯並將其處理結果同步給其他系統單元。利用zookeeper的一致性,能夠很好的保證在分佈式高並發情況下節點的創建一定能夠保證全局唯一性,即zookeeper將會保證客戶端無法重複創建一個已經存在的數據節點(由其保證分佈式數據的一致性)。

首先,創建/master_election/2019-10-09節點,客戶端集群每天會定時往該節點寫創建臨時節點,如/master_election/2019-10-09/binding,這個過程中,只有一個客戶端能夠創建成功,此時其變成master,其他節點都會在節點/master_election/2019-10-09上註冊一個節點變更的Watcher,用於監控當前的Master機器是否存活,一旦發現當前Master掛了,其餘客戶端將會重新進行Master選舉。

另外,這種場景演化一下,就是動態Master選舉。這就要用到 EPHEMERAL_SEQUENTIAL 類型節點的特性了。

上面提到,所有客戶端創建請求,最終只有一個創建成功。在這裡稍微變化下,就是允許所有請求都創建成功,但是的有個創建順序,於是所有的請求最終在zk上創建結果的一種情況是這樣的:/currentMaster/{sessionId}-1,/currentMaster/{sessionId}-2,/currentMaster/{sessionId}-3…..,每次選取序列號最小的那個機器作為Master,如果這個機器掛了,由於他創建的節點會馬上消失,那麼之後的最小那個機器就是Master了。

其在實際中應用有:

  • 在搜索系統中,如果集群中每個機器都生產一份全量索引,不僅耗時,而且不能保證彼此之間索引數據的一致。因此讓集群中的Master來進行全局索引的生成,然後通過到集群中的其他機器。
  • 在Hbase中,也是使用zookeeper來實現動態HMaster選舉。在Hbase實現中,會在zk上存儲一些ROOT表的地址和HMaster的地址,HRegionServer也會把即的以臨時節點(Ephemeral)的方式註冊在zk中,使得HMaster可以隨時感知到各個HRegionServer的存活狀態,同時,一旦HMaster出現問題,會重新選舉出一個HMaster來運行,從而避免了HMaster的單點問題。

2.3.7 分佈式鎖

分佈式鎖用於控制分佈式系統之間同步訪問共享資源的一種方式,可以保證不同系統訪問一個或一組資源時的一致性,主要分為排它鎖共享鎖

  • 排它鎖又稱寫鎖或獨佔鎖,若事務T1對數據對象01加上了排它鎖,那麼整個加鎖期間,只允許事務T1對01進行讀取或更新操作,其他事務都不能再對整個數據對象進行任何類型的操作,直到T1釋放了排它鎖。
      1. 獲取鎖,在需要獲取排它鎖時,所有客戶端通過調用接口,在 /exclusive_lock 節點下創建臨時子節點 /exclusive_lock/lock。zookeeper可以保證只有一個客戶端能夠創建成功,沒有成功的客戶端需要註冊 /exclusive_lock節點監聽。
      1. 釋放鎖,當獲取鎖的客戶端宕機或者正常完成業務邏輯都會導致臨時節點刪除,此時,所有在/exclusive_lock節點上註冊監聽的客戶端都會收到通知,可以重新發起分佈式鎖獲取。
  • 共享鎖又稱讀鎖,若事務T1對數據對象O1加了共享鎖,那麼當前事務是能對01進行讀操作,其他事務只能對整個數據對象加共享鎖,知道該數據對象上所有共享鎖都被釋放。
    • 獲取鎖,當需要獲取共享鎖是,所有客戶端都會到 /shared_lock 下面創建一個臨時順序節點,如果是讀請求,那麼就創建如:/shared_lock/host-1-R0000001的節點,如果是寫請求就創建例如:/shared_lock/host-2-W-0000002的節點,以此類推。
    • 判斷讀寫順序,不同事務可以同時對一個數據對象進行讀操作,而更新操作必須在當前沒有任何事務進行讀寫情況下進行,通過zookeeper來確定分佈式讀寫順序,大致分為四步:
      1. 創建完節點後,獲取/shared_lock 節點下所有子節點,並對該節點變更註冊監聽。
      2. 確定自己的節點序號在所有子節點中的順序。
      3. 對於讀請求:若沒有比自己序號小或所有比自己序號小的請求都是讀請求,那麼表明自己已經成功獲取到共享鎖,同時開始執行讀取邏輯,若有寫請求,則需要等待。對於寫請求:若自己不是序號最小的子節點,那麼需要等待。
      4. 接受到Watcher通知後,重複步驟1.
    • 釋放鎖,其釋放鎖的流程和獨佔鎖一致。

上述共享鎖的實現方案,可以滿足一般分佈式集群競爭鎖的需求,但是如果機器規模擴大會出現一些問題,下面着重分析判斷讀寫順序的步驟3: 針對如上圖所示的情況進行分析: 1. host1首先進行讀操作,完成後將節點/shared_lock/host1-R-0000001刪除。 2. 餘下幾台機器收到這個節點移除的通知,然後重新從/shared_lock節點上獲取一份新的節點列表。 3. 每台機器判斷自己的讀寫順序,其中host2檢測到自己的序號最小,於是進行寫操作,餘下機器則繼續等待。 4. 繼續……

可以看到,host1客戶端在移除自己的共享鎖後,zookeeper發送子節點變更Watcher通知給所有的機器,然後除了給host2產生影響外,對其他機器並沒有任何作用。大量的Watcher通知和子節點列表獲取連個操作會重複執行,這樣會造成系統性能影響和網絡開銷,更為嚴重的是,如果同一時間點有多個節點對應的客戶端完成了事務或事務終端引起節點的消失,zookeeper服務器就會在短時間內向其他所有的客戶端發送大量的事件通知,這就是所謂的羊群效應

可以有如下改動來避免羊群效應: 1. 客戶端調用create接口創建類似於/shared_lock/[Hostname]- 請求類型-序號的臨時順序節點。 2. 客戶端調用getChildren接口獲取所有已經創建的子節點列表(不註冊任何watcher)。 3. 如果無法獲取共享鎖,就調用exist接口來對比自己小的節點註冊Watcher。對於讀請求:向比自己序號小的最後一個寫請求節點註冊Watcher監聽。對於寫請求:向比自己序號小的最後一個節點註冊watcher監聽。 4. 等待watcher通知,繼續進入步驟2. 此方案改動主要在於:每個鎖競爭者,只需要關注/shared_lock節點下序號比自己小的那個節點是否存在即可。

2.3.8 分佈式隊列

分佈式隊列可以簡單分為 先入先出隊列模式等待隊列元素聚集後統一安排處理執行的Barrier模型

  • **FIFO先入先出,先進入隊列的請求操作先完成後,才會開始後面的請求。FIFO隊列類似於全寫的共享模式,所有客戶端都會到/queue_fifo這個節點下創建臨時節點,如/queue_fifo/host1-0000001。

創建節點後,按照如下步驟執行: 1. 通過調用getChildren接口來獲取/queue_fifo節點的所有子節點,即獲取隊列中的所有元素。 2. 確定位元組的節點序號在所有子節點中的順序。 3. 如果自己的序號不是最小的,那麼需要等待,同時向比自己小的最後一個節點註冊Watcher監聽。 4. 接受到Watcher通知後,重複步驟1

  • Barrier分佈式屏障,最終的合併計算需要基於很多並行計算的子結果來進行,開始時,/queue_barrier節點已經默認存在,並且將結點數據內容覆蓋為數字n來代表Barrier值,之後,所有客戶端都會到/queue_barrier節點下創建一個臨時節點,例如/queue_barrier/host1。

創建完成後,按照如下步驟執行: 1. 通過調用getData接口獲取/queue_barrier節點的數據內容,如10。 2. 通過調用getChildren接口獲取/queue_barrier節點下的所有子節點,同時註冊對子節點變更的Watcher監聽。 3. 統計子節點的個數。 4. 如果子節點個數還不足10個,那麼需要等待。 5. 接受到Wacher通知後,重複步驟3

3. 總結

上邊我們介紹了Zookeeper的典型的應用場景。zookeeper已經被廣泛應用于越來越多的大型分佈式系統中了,其中包括:Dubbo的註冊中心,HDFS的namenode和YARN框架的ResourceManager的HA(用zookeeper解決單點問題實現HA),HBase,Kafka等大數據和分佈式系統框架中。我們可以學習這些內容時,注意一下Zookeeper的具體的應用實現。