本來想系統回顧下 ZooKeeper的,可是網上沒找到一篇合自己胃口的文章,寫的差不多的,感覺大部分都是基於《從Paxos到ZooKeeper 分散式一致性原理與實踐》寫的,所以自己讀了一遍,加上專案中的使用,做個整理。加油,奧利給!
前言
面試常常被要求「熟悉分散式技術」,當年搞 “XXX管理系統” 的時候,我都不知道分散式系統是個啥。分散式系統是一個硬體或軟體元件分佈在不同的網路計算機中上,彼此之間僅僅通過訊息傳遞進行通訊和協調的系統。
計算機系統從集中式到分散式的變革伴隨著包括分散式網路、分散式事務、分散式資料一致性等在內的一系列問題和挑戰,同時也催生了一大批諸如ACID
、CAP
和 BASE
等經典理論的快速發展。
為了解決分散式一致性問題,湧現出了一大批經典的一致性協議和演算法,最為著名的就是二階段提交協議(2PC),三階段提交協議(3PC)和Paxos
演算法。Zookeeper
的一致性是通過基於 Paxos
演算法的 ZAB
協議完成的。一致性協議之前的文章也有介紹:「走進分散式一致性協議」從2PC、3PC、Paxos 到 ZAB,這裡就不再說了。
1. 概述
1.1 定義
ZooKeeper 官網是這麼介紹的:”Apache ZooKeeper 致力於開發和維護一個支援高度可靠的分散式協調的開源伺服器“
1.2 ZooKeeper是個啥
ZooKeeper 是 Apache 軟體基金會的一個軟體專案,它為大型「分散式計算」提供開源的分散式配置服務、同步服務和命名註冊。
Zookeeper 最早起源於雅虎研究院的一個研究小組。在當時,研究人員發現,在雅虎內部很多大型系統基本都需要依賴一個類似的系統來進行分散式協調,但是這些系統往往都存在分散式單點問題。所以,雅虎的開發人員就試圖開發一個通用的無單點問題的分散式協調框架,以便讓開發人員將精力集中在處理業務邏輯上,Zookeeper 就這樣誕生了。後來捐贈給了 Apache
,現已成為 Apache
頂級專案。
關於“ZooKeeper”這個專案的名字,其實也有一段趣聞。在立項初期,考慮到之前內部很多專案都是使用動物的名字來命名的(例如著名的Pig專案),雅虎的工程師希望給這個專案也取一個動物的名字。時任研究院的首席科學家 RaghuRamakrishnan 開玩笑地說:“再這樣下去,我們這兒就變成動物園了!”此話一出,大家紛紛表示就叫動物園管理員吧一一一因為各個以動物命名的分散式元件放在一起,雅虎的整個分散式系統看上去就像一個大型的動物園了,而 Zookeeper 正好要用來進行分散式環境的協調一一於是,Zookeeper 的名字也就由此誕生了。
ZooKeeper 是用於維護配置資訊,命名,提供分散式同步和提供組服務的集中式服務。所有這些型別的服務都以某種形式被分散式應用程式使用。每次實施它們時,都會進行很多工作來修復不可避免的 bug 和競爭條件。由於難以實現這類服務,因此應用程式最初通常會跳過它們,這會使它們在存在更改的情況下變得脆弱並且難以管理。即使部署正確,這些服務的不同實現也會導致管理複雜。
ZooKeeper 的目標是將這些不同服務的精華提煉為一個非常簡單的介面,用於集中協調服務。服務本身是分散式的,並且高度可靠。服務將實現共識,組管理和狀態協議,因此應用程式不需要自己實現它們。
1.3 ZooKeeper工作機制
ZooKeeper 從設計模式角度來理解:就是一個基於觀察者模式設計的分散式服務管理框架,它負責儲存和管理大家都關心的資料,然後接受觀察者的註冊,一旦這些資料的狀態發生變化,ZK 就將負責通知已經在 ZK 上註冊的那些觀察者做出相應的反應,從而實現叢集中類似 Master/Slave 管理模式。
1.4 特性
-
ZooKeeper:一個領導者(leader),多個跟隨者(follower)組成的叢集。
-
Leader 負責進行投票的發起和決議,更新系統狀態。
-
Follower 用於接收客戶請求並向客戶端返回結果,在選舉 Leader 過程中參與投票。
-
叢集中只要有半數以上節點存活,Zookeeper 叢集就能正常服務。
-
全域性資料一致(單一檢視):每個 Server 儲存一份相同的資料副本,Client 無論連線到哪個 Server,資料都是一致的。
-
順序一致性: 從同一客戶端發起的事務請求,最終將會嚴格地按照順序被應用到 ZooKeeper 中去。
-
原子性: 所有事務請求的處理結果在整個叢集中所有機器上的應用情況是一致的,也就是說,要麼整個叢集中所有的機器都成功應用了某一個事務,要麼都沒有應用。
-
實時性,在一定時間範圍內,client 能讀到最新資料。
-
可靠性: 一旦一次更改請求被應用,更改的結果就會被持久化,直到被下一次更改覆蓋。
1.5 設計目標
- 簡單的資料結構 :Zookeeper 使得分散式程式能夠通過一個共享的樹形結構的名字空間來進行相互協調,即Zookeeper 伺服器記憶體中的資料模型由一系列被稱為
ZNode
的資料節點組成,Zookeeper 將全量的資料儲存在記憶體中,以此來提高伺服器吞吐、減少延遲的目的。 - 可以構建叢集 : Zookeeper 叢集通常由一組機器構成,組成 Zookeeper 叢集的每臺機器都會在記憶體中維護當前伺服器狀態,並且每臺機器之間都相互通訊。
- 順序訪問 : 對於來自客戶端的每個更新請求,Zookeeper 都會分配一個全域性唯一的遞增編號,這個編號反映了所有事務操作的先後順序。
- 高效能 :Zookeeper 和 Redis 一樣全量資料儲存在記憶體中,100% 讀請求壓測 QPS 12-13W
1.6 資料結構
Zookeeper 資料模型的結構與 Unix 檔案系統的結構相似,整體上可以看做是一棵樹,每個節點稱作一個 「ZNode」。每個 ZNode 預設能儲存 1MB 的資料,每個 ZNode 都可以通過其路徑唯一標識。
1.7 應用場景
ZooKeeper 是一個典型的分散式資料一致性解決方案,分散式應用程式可以基於 ZooKeeper 實現諸如資料釋出/訂閱、負載均衡、命名服務、分散式協調/通知、叢集管理、Master 選舉、分散式鎖和分散式佇列等功能
統一命名服務
在分散式系統中,通過使用命名服務,客戶端應用能夠根據指定名字來獲取資源或服務的地址,提供者等資訊。被命名的實體通常可以是叢集中的機器,提供的服務地址,程式物件等等——這些我們都可以統稱他們為名字(Name)。其中較為常見的就是一些分散式服務框架(如RPC、RMI)中的服務地址列表。通過呼叫 Zookeeper 提供的建立節點的 API,能夠很容易建立一個全域性唯一的 path,這個 path 就可以作為一個名稱。
阿里巴巴開源的分散式服務框架 Dubbo 就使用 ZooKeeper 來作為其命名服務,維護全域性的服務地址列表。
資料釋出與訂閱(配置中心)
釋出與訂閱模型,即所謂的配置中心,顧名思義就是釋出者將資料釋出到 ZooKeeper 節點上,供訂閱者動態獲取資料,實現配置資訊的集中式管理和動態更新。例如全域性的配置資訊,服務式服務框架的服務地址列表等就非常適合使用。
-
分散式環境下,配置檔案管理和同步是一個常見問題
-
一個叢集中,所有節點的配置資訊是一致的,比如 Hadoop 叢集、叢集中的資料庫配置資訊等全域性配置
-
對配置檔案修改後,希望能夠快速同步到各個節點上。
-
-
配置管理可交由 ZooKeeper 實現
- 可將配置資訊寫入 ZooKeeper 上的一個 Znode
- 各個節點監聽這個 Znode
- 一旦 Znode 中的資料被修改,ZooKeeper 將通知各個節點
統一叢集管理
所謂叢集管理無在乎兩點:是否有機器退出和加入、選舉 Master。
管理節點
-
分散式環境中,實時掌握每個節點的狀態是必要的,比如我們要知道叢集中各機器狀態、收集各個機器的執行時狀態資料、伺服器動態上下線等。
-
交由 ZooKeeper 實現的方式
- 可將節點資訊寫入 ZooKeeper 上的一個 Znode
- 監聽這個 Znode 可獲取它的實時狀態變化
- 典型應用:HBase 中 Master 狀態監控和選舉。
Master選舉
在分散式環境中,相同的業務應用分佈在不同的機器上,有些業務邏輯(例如一些耗時的計算,網路I/O處理),往往只需要讓整個叢集中的某一臺機器進行執行,其餘機器可以共享這個結果,這樣可以大大減少重複勞動,提高效能,於是這個master選舉便是這種場景下的碰到的主要問題。
利用 Zookeeper 的強一致性,能夠很好的保證在分散式高併發情況下節點的建立一定是全域性唯一的,即:同時有多個客戶端請求建立 /currentMaster
節點,最終一定只有一個客戶端請求能夠建立成功。Zookeeper 通過這種節點唯一的特性,可以建立一個 Master 節點,其他客戶端 Watcher 監控當前 Master 是否存活,一旦 Master 掛了,其他機器再建立這樣的一個 Master 節點,用來重新選舉。
軟負載均衡
分散式系統中,負載均衡是一種很普遍的技術,為了保證高可用性,通常同一個應用或同一個服務的提供方都會部署多份,達到對等服務。可以是硬體的負載均衡,如 F5,也可以是軟體的負載,我們熟知的 Nginx,或者這裡介紹的 Zookeeper。
分散式協調/通知
Zookeeper 中特有的 「Watcher」 註冊與非同步通知機制,能夠很好的實現分散式環境下不同機器,甚至不同系統之間的協調和通知,從而實現對資料變更的實時處理。
使用方法通常是不同系統都對 ZK 上同一個 znode 進行註冊,監聽 znode 的變化(包括 znode 本身內容及子節點的),其中一個系統 update 了 znode,那麼另一個系統能夠收到通知,並作出相應處理。
- 心跳檢測中可以讓檢測系統和被檢測系統之間並不直接關聯起來,而是通過 ZK 上某個節點關聯,減少系統耦合;
- 系統排程模式中,假設某系統有控制檯和推送系統兩部分組成,控制檯的職責是控制推送系統進行相應的推送工作。管理人員在控制檯作的一些操作,實際上是修改了 ZK 上某些節點的狀態,而 ZK 就把這些變化通知給他們註冊 Watcher 的客戶端,即推送系統,於是,作出相應的推送任務。
分散式鎖
分散式鎖,這個主要得益於 ZooKeeper 為我們保證了資料的強一致性。
鎖服務可以分為兩類,一個是保持獨佔,另一個是控制時序。
-
所謂保持獨佔,就是所有試圖來獲取這個鎖的客戶端,最終只有一個可以成功獲得這把鎖。通常的做法是把 zk 上的一個 znode 看作是一把鎖,通過
create znode
的方式來實現。所有客戶端都去建立/distribute_lock
節點,最終成功建立的那個客戶端也即擁有了這把鎖。 -
控制時序,就是所有試圖來獲取這個鎖的客戶端,最終都是會被安排執行,只是有個全域性時序了。做法和上面基本類似,只是這裡
/distribute_lock
已預先存在,客戶端在它下面建立臨時有序節點(這個可以通過節點的屬性控制:CreateMode.EPHEMERAL_SEQUENTIAL
來指定)。ZK 的父節點(/distribute_lock
)維持一份 sequence,保證子節點建立的時序性,從而也形成了每個客戶端的全域性時序。
個人感覺還是用 Redis 實現分散式鎖更加方便。
PS:阿里中介軟體團隊:“其實,ZK 並非天生就是為這些應用場景設計的,都是後來眾多開發者根據其框架的特性,利用其提供的一系列API介面(或者稱為原語集),摸索出來的典型使用方法。”
2. Hello ZooKeeper
ZooKeeper 的三種部署方式:
- 單機模式,即部署在單臺機器上的一個 ZK 服務,適用於學習、瞭解 ZK 基礎功能
- 偽分佈模式,即部署在一臺機器上的多個(原則上大於3個)ZK 服務,偽叢集,適用於學習、開發和測試
- 全分散式模式(複製模式),即在多臺機器上部署服務,真正的叢集模式,生產環境中使用
計劃寫三篇的,第二篇會實戰 coding,運用各種 API,到時候再裝叢集,本節先來個單機玩~~
2.1 本地模式安裝部署
2.1.1 安裝前準備
-
安裝 Jdk
-
拷貝或下載 Zookeeper 安裝包到 Linux 系統下(這裡有個小問題,如果你下載 ZK 版本是3.5+ 的話,要下載 bin.tar.gz,愚笨的我最先沒看到官網說明,一頓操作各種報錯找不到 Main 方法)
-
解壓到指定目錄
tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz
2.1.2 配置修改
-
將 zookeeper-3.5.7/conf 這個路徑下的
zoo_sample.cfg
修改為zoo.cfg
;mv zoo_sample.cfg zoo.cfg
-
開啟 zoo.cfg 檔案,修改 dataDir 路徑:
dataDir=XXX/zookeeper-3.5.7/zkData
2.1.3 操作 Zookeeper
- 啟動 Zookeeper:
bin/zkServer.sh start
/usr/local/bin/java
ZooKeeper JMX enabled by default
Using config: /home/sync360/test/apache-zookeeper-3.5.7-bin/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
- 檢視程式是否啟動:
jps
4020 Jps
4001 QuorumPeerMain
- 檢視狀態:
bin/zkServer.sh status
/usr/local/bin/java
ZooKeeper JMX enabled by default
Using config: /home/apache-zookeeper-3.5.7-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: standalone
- 啟動客戶端:
bin/zkCli.sh
Connecting to localhost:2181
2020-03-25 15:41:19,112 [myid:] - INFO [main:Environment@109] - Client environment:zookeeper.version=3.5.7-f0fdd52973d373ffd9c86b81d99842dc2c7f660e, built on 02/10/2020 11:30 GMT
...
2020-03-25 15:41:19,183 [myid:] - INFO [main:ClientCnxn@1653] - zookeeper.request.timeout value is 0. feature enabled=
Welcome to ZooKeeper!
...
WATCHER::
WatchedEvent state:SyncConnected type:None path:null
-
退出客戶端:
quit
-
停止 Zookeeper:
bin/zkServer.sh stop
2.2 常用命令
命令基本語法 | 功能描述 |
---|---|
help | 顯示所有操作命令 |
ls path [watch] | 使用 ls 命令來檢視當前znode中所包含的內容 |
ls2 path [watch] | 檢視當前節點資料並能看到更新次數等資料 |
create | 普通建立-s 含有序列-e 臨時(重啟或者超時消失) |
get path [watch] | 獲得節點的值 |
set | 設定節點的具體值 |
stat | 檢視節點狀態 |
delete | 刪除節點 |
rmr | 遞迴刪除節點 |
ls 檢視當前 zk 中所包含的內容
[zk: localhost:2181(CONNECTED) 1] ls /
[lazyegg, zookeeper]
create 建立一個新的 znode
[zk: localhost:2181(CONNECTED) 2] create /test
Created /test
get 檢視新的 znode 的值
[zk: localhost:2181(CONNECTED) 4] get /test
null
可以看到值為 null,我們剛才設定了一個沒有值的節點,也可以通過 create /zoo dog
直接建立有內容的節點
set 對 zk 所關聯的字串進行設定
set /test hello
delete 刪除節點
delete /test
2.3 配置引數解讀
在 Zookeeper 的設計中,如果是叢集模式,那所有機器上的 zoo.cfg 檔案內容應該都是一致的。
Zookeeper 中的配置檔案 zoo.cfg
中引數含義解讀如下:
-
tickTime =2000:通訊心跳數
Zookeeper 使用的基本時間,伺服器之間或客戶端與伺服器之間維持心跳的時間間隔,也就是每個 tickTime時間就會傳送一個心跳,時間單位為毫秒
它用於心跳機制,並且設定最小的 session 超時時間為兩倍心跳時間。(session的最小超時時間是2*tickTime);
-
initLimit =10:主從初始通訊時限,叢集中的 Follower 跟隨者伺服器與 Leader 領導者伺服器之間初始連線時能容忍的最多心跳數(tickTime的數量),用它來限定叢集中的 ZK 伺服器連線到 Leader 的時限;
-
syncLimit =5:主從同步通訊時限,叢集中 Leader 與 Follower 之間的最大響應時間單位,假如響應超過
syncLimit * tickTime
,Leader 認為 Follwer 死掉,從伺服器列表中刪除 Follwer; -
dataDir:資料檔案目錄+資料持久化路徑;
-
clientPort =2181:客戶端連線埠
3. 你要知道的概念
- ZooKeeper 本身就是一個分散式程式(只要半數以上節點存活,ZooKeeper 就能正常服務)。
- 為了保證高可用,最好是以叢集形態來部署 ZooKeeper,這樣只要叢集中大部分機器是可用的(能夠容忍一定的機器故障),那麼 ZooKeeper 本身仍然是可用的。
- ZooKeeper 將資料儲存在記憶體中,這也就保證了高吞吐量和低延遲(但是記憶體限制了能夠儲存的容量不太大,此限制也是保持 znode 中儲存的資料量較小的進一步原因)。
- ZooKeeper 是高效能的。 在“讀”多於“寫”的應用程式中尤其的高效能,因為“寫”會導致所有的伺服器間同步狀態。(“讀”多於“寫”是協調服務的典型場景。)
- ZooKeeper 底層其實只提供了兩個功能:
- 管理(儲存、讀取)使用者程式提交的資料
- 為使用者程式提交資料節點監聽服務
這裡引入一個簡單的例子,逐個介紹一些 ZK 中的概念。
在分散式系統中經常會遇到這種情況,多個應用讀取同一個配置。例如:Client1,Client2 兩個應用都會讀取配置 B 中的內容,一旦 B 中的內容出現變化,就會通知 Client1 和 Client2。
一般的做法是在 Client1,Client2 中按照時脈頻率詢問 B 的變化,或者使用觀察者模式來監聽 B 的變化,發現變化以後再更新兩個客戶端。那麼 ZooKeeper 如何協調這種場景?
這兩個客戶端連線到 ZooKeeper 的伺服器,並獲取其中存放的 B。儲存 B 值的地方在 ZooKeeper 服務端中就稱為 ZNode。
3.1 資料節點(Znode)
在談到分散式的時候,我們通常說的“節點"是指組成叢集的每一臺機器。然而,在 Zookeeper 中,“節點"分為兩類,第一類同樣是指構成叢集的機器,我們稱之為「機器節點」;第二類則是指資料模型中的資料單元,我們稱之為「資料節點」一一ZNode。上圖中的 A、B 就是一個資料結點。
Zookeeper 將所有資料儲存在記憶體中,資料模型是一棵樹(Znode Tree),由斜槓(/)進行分割的路徑,就是一個 Znode,例如 /Configuration/B
。每個 Znode 上都會儲存自己的資料內容,同時還會儲存一系列屬性資訊。
在 Zookeeper 中,Znode 可以分為持久節點和臨時節點兩類。
- 所謂持久節點是指一旦這個 ZNode 被建立了,除非主動進行 ZNode 的移除操作,否則這個 ZNode 將一直儲存在 Zookeeper 上。
- 而臨時節點就不一樣了,它的生命週期和客戶端會話繫結,一旦客戶端會話失效,那麼這個客戶端建立的所有臨時節點都會被移除。
另外,ZooKeeper 還允許使用者為每個節點新增一個特殊的屬性:SEQUENTIAL。也被叫做 順序結點,一旦節點被標記上這個屬性,那麼在這個節點被建立的時候,Zookeeper 會自動在其節點名後面追加上一個整型數字,這個整型數字是一個由父節點維護的自增數字。
3.2 事件監聽器(Watcher)
上面說了 ZooKeeper 用來存放資料的 ZNode,並且把 B 的值儲存在裡面。如果 B 被更新了,兩個客戶端(Client1、Client2)如何獲得通知呢?
Zookeeper 允許使用者在指定節點上註冊一些 Watcher,當 Znode 發生變化時,將觸發並刪除一個 watch。當 watch 被觸發時客戶端會收到一個資料包,指示 znode 已經被修改。如果客戶端和 ZooKeeper 伺服器之間的連線中斷,客戶端將收到本地通知。該機制是 Zookeeper 實現分散式協調服務的重要特性。
3.6.0中的新增功能:客戶端還可以在 znode 上設定永久性的遞迴監視,這些監視在觸發時不會刪除,並且會以遞迴方式觸發已註冊 znode 以及所有子 znode 的更改。
ZooKeeper 客戶端(Client)會在指定的節點(/Configuration/B)上註冊一個 Watcher,ZNode 上的 B 被更新的時候,服務端就會通知 Client1 和 Client2。
3.3 版本
有了 Watcher 機制,就可以實現分散式協調/通知了,假設有這樣的場景,兩個客戶端同時對 B 進行寫入操作,這兩個客戶端就會存在競爭關係,通常需要對 B 進行加鎖操作,ZK 通過 version 版本號來控制實現樂觀鎖中的“寫入校驗”機制。
Zookeeper 的每個 ZNode 上都會儲存資料,對應於每個 ZNode,Zookeeper 都會為其維護一個叫作 Stat 的資料結構,Stat 中記錄了這個 ZNode 的三個資料版本,分別是 version(當前ZNode的版本)、cversion(當前ZNode 子節點的版本)和 aversion(當前ZNode的ACL版本)。
znode 裡都有些啥呢?
3.4 Stat 結構體
Znodes 維護了一個 stat 結構,其中包含資料更改、ACL更改的版本號、時間戳等。
狀態屬性 | 說明 |
---|---|
czxid | 建立節點的事務zxid。 每次修改 ZK 狀態都會收到一個zxid形式的時間戳,也就是 ZK 事務ID。事務ID是 ZK 中所有修改總的次序。每個修改都有唯一的zxid,如果zxid1小於zxid2,那麼zxid1在zxid2之前發生 |
ctime | znode被建立的毫秒數(從1970年開始) |
mzxid | znode最後更新的事務zxid |
mtime | znode最後修改的毫秒數(從1970年開始) |
pzxid | znode最後更新的子節點zxid |
version | 資料節點版本號 |
cversion | 子節點版本號,znode子節點修改次數 |
aversion | znode訪問控制列表的變化號 |
ephemeralOwner | 如果是臨時節點,這個是znode擁有者的session id。如果不是臨時節點則是0 |
dataLength | znode的資料長度 |
numChildren | znode子節點數量 |
3.5 會話(Session)
Session 指的是 ZooKeeper 伺服器與客戶端會話。
在 ZooKeeper 中,一個客戶端連線是指客戶端和伺服器之間的一個 TCP 長連線。客戶端啟動的時候,首先會與伺服器建立一個 TCP 連線,從第一次連線建立開始,客戶端會話的生命週期也開始了。通過這個連線,客戶端能夠通過心跳檢測與伺服器保持有效的會話,也能夠向 Zookeeper 伺服器傳送請求並接受響應,同時還能夠通過該連線接收來自伺服器的 Watch 事件通知。
Session 作為會話實體,用來代表客戶端會話,其包括 4 個屬性:
- SessionID,用來全域性唯一識別會話;
- TimeOut,會話超時事件。客戶端在創造 Session 例項的時候,會設定一個會話超時的時間。當由於伺服器壓力太大、網路故障或是客戶端主動斷開連線等各種原因導致客戶端連線斷開時,只要在 sessionTimeout 規定的時間內能夠重新連線上叢集中任意一臺伺服器,那麼之前建立的會話仍然有效;
- TickTime,下次會話超時時間點;
- isClosing,當服務端如果檢測到會話超時失效了,會通過設定這個屬性將會話關閉。
3.6 ACL
Zookeeper 採用 ACL(Access Control Lists)策略來進行許可權控制,類似於 UNIX 檔案系統的許可權控制。Zookeeper 定義瞭如下 5 種許可權:
- CREATE: 建立子節點的許可權
- READ: 獲取節點資料和子節點列表的許可權
- WRITE: 更新節點資料的許可權
- DELETE: 刪除子節點的許可權
- ADMIN: 設定節點ACL的許可權
其中尤其需要注意的是,CREATE 和 DELETE 這兩種許可權都是針對子節點的許可權控制。
3.7 叢集角色
最典型叢集模式:Master/Slave 模式(主備模式)。在這種模式中,通常 Master 伺服器作為主伺服器提供寫服務,其他的 Slave 從伺服器通過非同步複製的方式獲取 Master 伺服器最新的資料提供讀服務。
但是,在 ZooKeeper 中沒有選擇傳統的 Master/Slave 概念,而是引入了Leader、Follower 和 Observer 三種角色。
- Leader: 為客戶端提供讀和寫的服務,負責投票的發起和決議,更新系統狀態
- Follower: 為客戶端提供讀服務,如果是寫服務則轉發給 Leader。在選舉過程中參與投票
- Observer: 為客戶端提供讀伺服器,如果是寫服務則轉發給 Leader。不參與選舉過程中的投票,也不參與“過半寫成功”策略。在不影響寫效能的情況下提升叢集的讀效能。此角色是在 zookeeper3.3 系列新增的角色。
server 狀態
- LOOKING:尋找Leader狀態
- LEADING:領導者狀態,表明當前伺服器角色是 Leader
- FOLLOWING:跟隨者狀態,表明當前伺服器角色是 Follower
- OBSERVING:觀察者狀態,表明當前伺服器角色是 Observer
選舉機制
- 伺服器1啟動,此時只有它一臺伺服器啟動了,它發出去的報文沒有任何響應,所以它的選舉狀態一直是LOOKING 狀態。
- 伺服器2啟動,它與最開始啟動的伺服器1進行通訊,互相交換自己的選舉結果,由於兩者都沒有歷史資料,所以 id 值較大的伺服器2勝出,但是由於沒有達到超過半數以上的伺服器都同意選舉它(這個例子中的半數以上是3),所以伺服器1、2還是繼續保持 LOOKING 狀態。
- 伺服器3啟動,根據前面的理論分析,伺服器3成為伺服器1、2、3中的老大,而與上面不同的是,此時有三臺伺服器選舉了它,所以它成為了這次選舉的Leader。
- 伺服器4啟動,根據前面的分析,理論上伺服器4應該是伺服器1、2、3、4中最大的,但是由於前面已經有半數以上的伺服器選舉了伺服器3,所以它只能接受當小弟的命了。
- 伺服器5啟動,同4一樣當小弟。
Watcher 監聽器
Zookeeper 中最有特色且最不容易理解的是監視(Watches)。
Zookeeper 所有的讀操作——getData(),getChildren(), 和 exists() 都可以設定監視(watch),監視事件可以理解為一次性的觸發器, 官方定義如下: a watch event is one-time trigger, sent to the client that set the watch, which occurs when the data for which the watch was set changes。對此需要作出如下理解:
-
One-time trigger(一次性觸發)
當設定監視的資料發生改變時,該監視事件會被髮送到客戶端,例如,如果客戶端呼叫了
getData("/znode1", true)
並且稍後/znode1
節點上的資料發生了改變或者被刪除了,客戶端將會獲取到/znode1
發生變化的監視事件,而如果/znode1
再一次發生了變化,除非客戶端再次對/znode1
設定監視,否則客戶端不會收到事件通知。(3.6之後可以設定永久監視) -
Sent to the client(傳送至客戶端)
Zookeeper 客戶端和服務端是通過 socket 進行通訊的,由於網路存在故障,所以監視事件很有可能不會成功到達客戶端,監視事件是非同步傳送至監視者的,Zookeeper 本身提供了保序性(ordering guarantee):即客戶端只有首先看到了監視事件後,才會感知到它所設定監視的 znode 發生了變化(a client will never see a change for which it has set a watch until it first sees the watch event)。 網路延遲或者其他因素可能導致不同的客戶端在不同的時刻感知某一監視事件,但是不同的客戶端所看到的一切具有一致的順序。
-
The data for which the watch was set(被設定 watch 的資料)
這意味著 znode 節點本身具有不同的改變方式。你也可以想象 Zookeeper 維護了兩條監視連結串列:資料監視和子節點監視(data watches and child watches),
getData()
和exists()
設定資料監視,getChildren()
設定子節點監視。 或者,你也可以想象 Zookeeper 設定的不同監視返回不同的資料,getData()
和exists()
返回 znode 節點的相關資訊,而getChildren()
返回子節點列表。因此,setData()
會觸發設定在某一節點上所設定的資料監視(假定資料設定成功),而一次成功的create()
操作則會觸發當前節點上所設定的資料監視以及父節點的子節點監視。一次成功的delete()
操作將會觸發當前節點的資料監視和子節點監視事件,同時也會觸發該節點父節點的child watch
。
Zookeeper 中的監視是輕量級的,因此容易設定、維護和分發。當客戶端與 Zookeeper 伺服器端失去聯絡時,客戶端並不會收到監視事件的通知,只有當客戶端重新連線後,若在必要的情況下,以前註冊的監視會重新被註冊並觸發,對於開發人員來說這通常是透明的。只有一種情況會導致監視事件的丟失,即:通過 exists()
設定了某個 znode 節點的監視,但是如果某個客戶端在此 znode 節點被建立和刪除的時間間隔內與 zookeeper 伺服器失去了聯絡,該客戶端即使稍後重新連線 zookeepe r伺服器後也得不到事件通知。
從上圖可以看到,Watcher 機制包括三個角色:客戶端執行緒、客戶端的 WatchManager 以及 ZooKeeper 伺服器。Watcher 機制就是這三個角色之間的互動,整個過程分為註冊、儲存和通知三個步驟:
- 客戶端向 ZooKeeper 伺服器註冊一個 Watcher 監聽;
- 把這個監聽資訊儲存到客戶端的 WatchManager 中;
- 當 ZooKeeper 中的節點發生變化時,會通知客戶端,客戶端會呼叫相應 Watcher 物件中的回撥方法。
文章持續更新,可以微信搜「 JavaKeeper 」第一時間閱讀,無套路領取 500+ 本電子書和 30+ 視訊教學和原始碼,本文 GitHub github.com/JavaKeeper 已經收錄,Javaer 開發、面試必備技能兵器譜,有你想要的。
參考:
《從Paxos到ZooKeeper 分散式一致性原理與實踐》
《阿里中介軟體團隊部落格》http://jm.taobao.org/2011/10/08/1232/
《Zookeeper官方文件》https://zookeeper.apache.org/doc/
《尚矽谷Zookeeper》