ZooKeeper核心知識總結!
之前分享過幾個系列:
今天這篇介紹一下ZooKeeper!
「文章較長,可以點贊,收藏再看!」
文章內(nèi)容會同步到個人網(wǎng)站上,方便閱讀:https://xiaoflyfish.cn/,(「可以訪問了!」)
基本介紹Apache ZooKeeper 是由Apache Hadoop的子項目發(fā)展而來,為分布式應用提供高效且可靠的分布式協(xié)調(diào)服務。
- 在解決分布式數(shù)據(jù)一致性方面,ZK沒有直接采用Paxos算法,而是采用了ZAB(ZooKeeper Atomic Broadcast)協(xié)議。
ZK可以提供諸如數(shù)據(jù)發(fā)布/訂閱、負載均衡、命名服務、分布式協(xié)調(diào)/通知,集群管理,Master選舉,分布式鎖,分布式隊列等功能。
「它具有以下特性:」
- 「順序一致性」:從一個客戶端發(fā)起的事務請求,最終都會嚴格按照其發(fā)起順序被應用到 Zookeeper 中;
- 「原子性」:要么所有應用,要么不應用;不存在部分機器應用了該事務,而「另一部分沒有應用」的情況;
- 「單一視圖」:所有客戶端看到的服務端數(shù)據(jù)模型都是一致的,無論客戶連接的是哪個ZK服務器;
- 「可靠性」:一旦服務端成功應用了一個事務,則其引起的改變會一直保留,直到被另外一個事務所更改;
- 「實時性」:一旦一個事務被成功應用后,Zookeeper 可以保證客戶端立即可以讀取到這個事務變更后的最新狀態(tài)的數(shù)據(jù)(「一段時間」)。
ZooKeeper 中的數(shù)據(jù)模型是一種樹形結構,非常像電腦中的文件系統(tǒng),有一個根文件夾,下面還有很多子文件夾。
ZooKeeper的數(shù)據(jù)模型也具有一個固定的根節(jié)點
(/),我們可以在根節(jié)點下創(chuàng)建子節(jié)點,并在子節(jié)點下繼續(xù)創(chuàng)建下一級節(jié)點。ZooKeeper 樹中的每一層級用斜杠
(/)分隔開,且只能用絕對路徑(如get /work/task)的方式查詢 ZooKeeper 節(jié)點,而不能使用相對路徑。

「為什么 ZooKeeper 不能采用相對路徑查找節(jié)點呢?」
?這是因為 ZooKeeper 大多是應用場景是定位數(shù)據(jù)模型上的節(jié)點,并在相關節(jié)點上進行操作。
?
像這種查找與給定值相等的記錄問題最適合用散列來解決。
因此 ZooKeeper 在底層實現(xiàn)的時候,使用了一個 hashtable,即 hashtableConcurrentHashMap,用節(jié)點的完整路徑來作為 key 存儲節(jié)點數(shù)據(jù)。
這樣就大大提高了 ZooKeeper 的性能。
「節(jié)點類型」
ZooKeeper 中的數(shù)據(jù)節(jié)點也分為持久節(jié)點、臨時節(jié)點和有序節(jié)點三種類型:
?1、持久節(jié)點
?
一旦將節(jié)點創(chuàng)建為持久節(jié)點,該數(shù)據(jù)節(jié)點會一直存儲在 ZooKeeper 服務器上,即使創(chuàng)建該節(jié)點的客戶端與服務端的會話關閉了,該節(jié)點依然不會被刪除。如果我們想刪除持久節(jié)點,就要顯式調(diào)用 delete 函數(shù)進行刪除操作。
?2、臨時節(jié)點
?
如果將節(jié)點創(chuàng)建為臨時節(jié)點,那么該節(jié)點數(shù)據(jù)不會一直存儲在 ZooKeeper 服務器上。
當創(chuàng)建該臨時節(jié)點的客戶端會話因超時或發(fā)生異常而關閉時,該節(jié)點也相應在 ZooKeeper 服務器上被刪除,同樣,我們可以像刪除持久節(jié)點一樣主動刪除臨時節(jié)點。
在平時的開發(fā)中,我們可以利用臨時節(jié)點的這一特性來做服務器集群內(nèi)機器運行情況的統(tǒng)計,將集群設置為/servers節(jié)點,并為集群下的每臺服務器創(chuàng)建一個臨時節(jié)點/servers/host,當服務器下線時該節(jié)點自動被刪除,最后統(tǒng)計臨時節(jié)點個數(shù)就可以知道集群中的運行情況。
?3、有序節(jié)點
?
節(jié)點有序是說在我們創(chuàng)建有序節(jié)點的時候,ZooKeeper 服務器會自動使用一個單調(diào)遞增的數(shù)字作為后綴,追加到我們創(chuàng)建節(jié)點的后邊。
例如一個客戶端創(chuàng)建了一個路徑為 works/task-的有序節(jié)點,那么 ZooKeeper 將會生成一個序號并追加到該節(jié)點的路徑后,最后該節(jié)點的路徑為works/task-1。
- 通過這種方式我們可以直觀的查看到節(jié)點的創(chuàng)建順序。
ZooKeeper 中的每個節(jié)點都維護有這些內(nèi)容:一個二進制數(shù)組(byte data[]),用來存儲節(jié)點的數(shù)據(jù)、ACL 訪問控制信息、子節(jié)點數(shù)據(jù)(因為臨時節(jié)點不允許有子節(jié)點,所以其子節(jié)點字段為 null),除此之外每個數(shù)據(jù)節(jié)點還有一個記錄自身狀態(tài)信息的字段 stat。
「節(jié)點的狀態(tài)結構」
執(zhí)行stat /zk_test,可以看到控制臺輸出了一些信息,這些就是節(jié)點狀態(tài)信息。
每一個節(jié)點都有一個自己的狀態(tài)屬性,記錄了節(jié)點本身的一些信息:
| 「狀態(tài)屬性」 | 「說明」 |
|---|---|
| czxid | 數(shù)據(jù)節(jié)點創(chuàng)建時的事務 ID |
| ctime | 數(shù)據(jù)節(jié)點創(chuàng)建時的時間 |
| mzxid | 數(shù)據(jù)節(jié)點最后一次更新時的事務 ID |
| mtime | 數(shù)據(jù)節(jié)點最后一次更新時的時間 |
| pzxid | 數(shù)據(jù)節(jié)點的子節(jié)點最后一次被修改時的事務 ID |
| 「cversion」 | 「子節(jié)點的版本」 |
| 「version」 | 「當前節(jié)點數(shù)據(jù)的版本」 |
| 「aversion」 | 「節(jié)點的 ACL 的版本」 |
| ephemeralOwner | 如果節(jié)點是臨時節(jié)點,則表示創(chuàng)建該節(jié)點的會話的 SessionID;如果節(jié)點是持久節(jié)點,則該屬性值為 0 |
| dataLength | 數(shù)據(jù)內(nèi)容的長度 |
| numChildren | 數(shù)據(jù)節(jié)點當前的子節(jié)點個數(shù) |
「數(shù)據(jù)節(jié)點的版本」
在 ZooKeeper 中為數(shù)據(jù)節(jié)點引入了版本的概念,每個數(shù)據(jù)節(jié)點有 3 種類型的版本信息,對數(shù)據(jù)節(jié)點的任何更新操作都會引起版本號的變化。
ZooKeeper 的版本信息表示的是對節(jié)點數(shù)據(jù)內(nèi)容、子節(jié)點信息或者是 ACL 信息的修改次數(shù)。
數(shù)據(jù)存儲從存儲位置上來說,事務日志和數(shù)據(jù)快照一樣,都存儲在本地磁盤上;而從業(yè)務角度來講,內(nèi)存數(shù)據(jù)就是我們創(chuàng)建數(shù)據(jù)節(jié)點、添加監(jiān)控等請求時直接操作的數(shù)據(jù)。
事務日志數(shù)據(jù)主要用于記錄本地事務性會話操作,用于 ZooKeeper 集群服務器之間的數(shù)據(jù)同步。
事務快照則是將內(nèi)存數(shù)據(jù)持久化到本地磁盤。
?這里要注意的一點是,數(shù)據(jù)快照是每間隔一段時間才把內(nèi)存數(shù)據(jù)存儲到本地磁盤,因此數(shù)據(jù)并不會一直與內(nèi)存數(shù)據(jù)保持一致。
?
在單臺 ZooKeeper 服務器運行過程中因為異常而關閉時,可能會出現(xiàn)數(shù)據(jù)丟失等情況。
「內(nèi)存數(shù)據(jù)」
ZooKeeper 的數(shù)據(jù)模型可以看作一棵樹形結構,而數(shù)據(jù)節(jié)點就是這棵樹上的葉子節(jié)點。
從數(shù)據(jù)存儲的角度看,ZooKeeper 的數(shù)據(jù)模型是存儲在內(nèi)存中的。
我們可以把 ZooKeeper 的數(shù)據(jù)模型看作是存儲在內(nèi)存中的數(shù)據(jù)庫,而這個數(shù)據(jù)庫不但存儲數(shù)據(jù)的節(jié)點信息,還存儲每個數(shù)據(jù)節(jié)點的 ACL 權限信息以及 stat 狀態(tài)信息等。
- 而在底層實現(xiàn)中,ZooKeeper 數(shù)據(jù)模型是通過 DataTree 類來定義的。
DataTree 類定義了一個 ZooKeeper 數(shù)據(jù)的內(nèi)存結構。
DataTree 的內(nèi)部定義類 nodes 節(jié)點類型、root 根節(jié)點信息、子節(jié)點的 WatchManager 監(jiān)控信息等數(shù)據(jù)模型中的相關信息。
可以說,一個 DataTree 類定義了 ZooKeeper 內(nèi)存數(shù)據(jù)的邏輯結構。
「事務日志」
為了整個 ZooKeeper 集群中數(shù)據(jù)的一致性,Leader 服務器會向 ZooKeeper 集群中的其他角色服務發(fā)送數(shù)據(jù)同步信息,在接收到數(shù)據(jù)同步信息后, ZooKeeper 集群中的 Follow 和 Observer 服務器就會進行數(shù)據(jù)同步。
?而這兩種角色服務器所接收到的信息就是 Leader 服務器的事務日志。
?
在接收到事務日志后,并在本地服務器上執(zhí)行。這種數(shù)據(jù)同步的方式,避免了直接使用實際的業(yè)務數(shù)據(jù),減少了網(wǎng)絡傳輸?shù)拈_銷,提升了整個 ZooKeeper 集群的執(zhí)行性能。
Watch機制ZooKeeper 的客戶端可以通過 Watch 機制來訂閱當服務器上某一節(jié)點的數(shù)據(jù)或狀態(tài)發(fā)生變化時收到相應的通知;
「如何實現(xiàn):」
我們可以通過向 ZooKeeper 客戶端的構造方法中傳遞 Watcher 參數(shù)的方式實現(xiàn):
new?ZooKeeper(String?connectString,?int?sessionTimeout,?Watcher?watcher)
上面代碼的意思是定義了一個了 ZooKeeper 客戶端對象實例,并傳入三個參數(shù):
connectString 服務端地址
sessionTimeout:超時時間
Watcher:監(jiān)控事件
這個 Watcher 將作為整個 ZooKeeper 會話期間的上下文 ,一直被保存在客戶端 ZKWatchManager 的 defaultWatcher 中。
除此之外,ZooKeeper 客戶端也可以通過 getData、exists 和 getChildren 三個接口來向 ZooKeeper 服務器注冊 Watcher,從而方便地在不同的情況下添加 Watch 事件:
getData(String?path,?Watcher?watcher,?Stat?stat)
觸發(fā)通知的條件:

上圖中列出了客戶端在不同會話狀態(tài)下,相應的在服務器節(jié)點所能支持的事件類型。
- 例如在客戶端連接服務端的時候,可以對數(shù)據(jù)節(jié)點的創(chuàng)建、刪除、數(shù)據(jù)變更、子節(jié)點的更新等操作進行監(jiān)控。
「當服務端某一節(jié)點發(fā)生數(shù)據(jù)變更操作時,所有曾經(jīng)設置了該節(jié)點監(jiān)控事件的客戶端都會收到服務器的通知嗎?」
答案是否定的,Watch 事件的觸發(fā)機制取決于會話的連接狀態(tài)和客戶端注冊事件的類型,所以當客戶端會話狀態(tài)或數(shù)據(jù)節(jié)點發(fā)生改變時,都會觸發(fā)對應的 Watch 事件。
「訂閱發(fā)布場景實現(xiàn)」
?提到 ZooKeeper 的應用場景,你可能第一時間會想到最為典型的發(fā)布訂閱功能。
?
發(fā)布訂閱功能可以看作是一個一對多的關系,即一個服務或數(shù)據(jù)的發(fā)布者可以被多個不同的消費者調(diào)用。
一般一個發(fā)布訂閱模式的數(shù)據(jù)交互可以分為消費者主動請求生產(chǎn)者信息的拉取模式,和生產(chǎn)者數(shù)據(jù)變更時主動推送給消費者的推送模式。
ZooKeeper 采用了兩種模式結合的方式實現(xiàn)訂閱發(fā)布功能。
?下面我們來分析一個具體案例:
?
在系統(tǒng)開發(fā)的過程中會用到各種各樣的配置信息,如數(shù)據(jù)庫配置項、第三方接口、服務地址等,這些配置操作在我們開發(fā)過程中很容易完成,但是放到一個大規(guī)模的集群中配置起來就比較麻煩了。
通常這種集群中,我們可以用配置管理功能自動完成服務器配置信息的維護,利用ZooKeeper 的發(fā)布訂閱功能就能解決這個問題。
我們可以把諸如數(shù)據(jù)庫配置項這樣的信息存儲在 ZooKeeper 數(shù)據(jù)節(jié)點中。
如/confs/data_item1。
服務器集群客戶端對該節(jié)點添加 Watch 事件監(jiān)控,當集群中的服務啟動時,會讀取該節(jié)點數(shù)據(jù)獲取數(shù)據(jù)配置信息。
而當該節(jié)點數(shù)據(jù)發(fā)生變化時,ZooKeeper 服務器會發(fā)送 Watch 事件給各個客戶端,集群中的客戶端在接收到該通知后,重新讀取節(jié)點的數(shù)據(jù)庫配置信息。
我們使用 Watch 機制實現(xiàn)了一個分布式環(huán)境下的配置管理功能,通過對 ZooKeeper 服務器節(jié)點添加數(shù)據(jù)變更事件,實現(xiàn)當數(shù)據(jù)庫配置項信息變更后,集群中的各個客戶端能接收到該變更事件的通知,并獲取最新的配置信息。
?會話機制要注意一點是,我們提到 Watch 具有一次性,所以當我們獲得服務器通知后要再次添加 Watch 事件。
?
ZooKeeper 的工作方式一般是通過客戶端向服務端發(fā)送請求而實現(xiàn)的。
而在一個請求的發(fā)送過程中,首先,客戶端要與服務端進行連接,而一個連接就是一個會話。
?在 ZooKeeper 中,一個會話可以看作是一個用于表示客戶端與服務器端連接的數(shù)據(jù)結構 Session。
?
這個數(shù)據(jù)結構由三個部分組成:分別是會話 ID(sessionID)、會話超時時間(TimeOut)、會話關閉狀態(tài)(isClosing)
會話 ID:會話 ID 作為一個會話的標識符,當我們創(chuàng)建一次會話的時候,ZooKeeper 會自動為其分配一個唯一的 ID 編碼。
會話超時時間:一般來說,一個會話的超時時間就是指一次會話從發(fā)起后到被服務器關閉的時長。而設置會話超時時間后,服務器會參考設置的超時時間,最終計算一個服務端自己的超時時間。而這個超時時間則是最終真正用于 ZooKeeper 中服務端用戶會話管理的超時時間。
會話關閉狀態(tài):會話關閉 isClosing 狀態(tài)屬性字段表示一個會話是否已經(jīng)關閉。如果服務器檢查到一個會話已經(jīng)因為超時等原因失效時, ZooKeeper 會在該會話的 isClosing 屬性值標記為關閉,再之后就不對該會話進行操作了。
「會話狀態(tài)」
在 ZooKeeper 服務的運行過程中,會話會經(jīng)歷不同的狀態(tài)變化。
這些狀態(tài)包括:
?正在連接(CONNECTING)、已經(jīng)連接(CONNECTIED)、正在重新連接(RECONNECTING)、已經(jīng)重新連接(RECONNECTED)、會話關閉(CLOSE)等。
?
當客戶端開始創(chuàng)建一個與服務端的會話操作時,它的會話狀態(tài)就會變成 CONNECTING,之后客戶端會根據(jù)服務器地址列表中的服務器 IP 地址分別嘗試進行連接。如果遇到一個 IP 地址可以連接到服務器,那么客戶端會話狀態(tài)將變?yōu)?CONNECTIED。
如果因為網(wǎng)絡原因造成已經(jīng)連接的客戶端會話斷開時,客戶端會重新嘗試連接服務端。而對應的客戶端會話狀態(tài)又變成 CONNECTING ,直到該會話連接到服務端最終又變成 CONNECTIED。
?在 ZooKeeper 服務的整個運行過程中,會話狀態(tài)經(jīng)常會在 CONNECTING 與 CONNECTIED 之間進行切換。
?
最后,當出現(xiàn)超時或者客戶端主動退出程序等情況時,客戶端會話狀態(tài)則會變?yōu)?CLOSE 狀態(tài)。
「會話異?!?/strong>
在 ZooKeeper 中,會話的超時異常包括客戶端 readtimeout 異常和服務器端 sessionTimeout 異常。
- 在我們平時的開發(fā)中,要明確這兩個異常的不同之處在于一個是發(fā)生在客戶端,而另一個是發(fā)生在服務端。
而對于那些對 ZooKeeper 接觸不深的開發(fā)人員來說,他們常常踩坑的地方在于,雖然設置了超時間,但是在實際服務運行的時候 ZooKeeper 并沒有按照設置的超時時間來管理會話。
- 這是因為 ZooKeeper 實際起作用的超時時間是通過客戶端和服務端協(xié)商決定。
ZooKeeper 客戶端在和服務端建立連接的時候,會提交一個客戶端設置的會話超時時間,而該超時時間會和服務端設置的最大超時時間和最小超時時間進行比對,如果正好在其允許的范圍內(nèi),則采用客戶端的超時時間管理會話。
如果大于或者小于服務端設置的超時時間,則采用服務端設置的值管理會話。
「分桶策略」
我們知道在 ZooKeeper 中為了保證一個會話的存活狀態(tài),客戶端需要向服務器周期性地發(fā)送心跳信息。
- 而客戶端所發(fā)送的心跳信息可以是一個 ping 請求,也可以是一個普通的業(yè)務請求。
ZooKeeper 服務端接收請求后,會更新會話的過期時間,來保證會話的存活狀態(tài)。
- 所以在 ZooKeeper 的會話管理中,最主要的工作就是管理會話的過期時間。
?ZooKeeper 中采用了獨特的會話管理方式來管理會話的過期時間。
?
在 ZooKeeper 中,會話將按照不同的時間間隔進行劃分,超時時間相近的會話將被放在同一個間隔區(qū)間中,這種方式避免了 ZooKeeper 對每一個會話進行檢查,而是采用分批次的方式管理會話。
這就降低了會話管理的難度,因為每次小批量的處理會話過期也提高了會話處理的效率。
「ZooKeeper 這種會話管理的好處?」
ZooKeeper 這種分段的會話管理策略大大提高了計算會話過期的效率,如果是在一個實際生產(chǎn)環(huán)境中,一個大型的分布式系統(tǒng)往往具有很高的訪問量。
而 ZooKeeper 作為其中的組件,對外提供服務往往要承擔數(shù)千個客戶端的訪問,這其中就要對這幾千個會話進行管理。
在這種場景下,要想通過對每一個會話進行管理和檢查并不合適,所以采用將同一個時間段的會話進行統(tǒng)一管理,這樣就大大提高了服務的運行效率。
「底層實現(xiàn)」
ZooKeeper 底層實現(xiàn)的原理,核心的一點就是過期隊列這個數(shù)據(jù)結構。所有會話過期的相關操作都是圍繞這個隊列進行的。
- 可以說 ZooKeeper 底層就是采用這個隊列結構來管理會話過期的。
「一個會話過期隊列是由若干個 bucket 組成的。」
bucket 是一個按照時間劃分的區(qū)間。
在 ZooKeeper 中,通常以 expirationInterval 為單位進行時間區(qū)間的劃分,它是 ZooKeeper 分桶策略中用于劃分時間區(qū)間的最小單位。
在 ZooKeeper 中,一個過期隊列由不同的 bucket 組成。
每個 bucket 中存放了在某一時間內(nèi)過期的會話。
將會話按照不同的過期時間段分別維護到過期隊列之后,在 ZooKeeper 服務運行的過程中,具體的執(zhí)行過程如下圖所示。

首先,ZooKeeper 服務會開啟一個線程專門用來檢索過期隊列,找出要過期的 bucket,而 ZooKeeper 每次只會讓一個 bucket 的會話過期,每當要進行會話過期操作時,ZooKeeper 會喚醒一個處于休眠狀態(tài)的線程進行會話過期操作,之后會按照上面介紹的操作檢索過期隊列,取出過期的會話后會執(zhí)行過期操作。
ACL權限ZooKeeper的ACL可針對znodes設置相應的權限信息。
一個 ACL 權限設置通??梢苑譃?3 部分,分別是:權限模式(Scheme)、授權對象(ID)、權限信息(Permission)。
- 最終組成一條例如
scheme:id:permission格式的 ACL 請求信息。
「權限模式:Scheme」
ZooKeeper 的權限驗證方式大體分為兩種類型,一種是范圍驗證,另外一種是口令驗證。
?范圍驗證
?
所謂的范圍驗證就是說 ZooKeeper 可以針對一個 IP 或者一段 IP 地址授予某種權限。
比如我們可以讓一個 IP 地址為ip:192.168.0.11的機器對服務器上的某個數(shù)據(jù)節(jié)點具有寫入的權限。
或者也可以通過ip:192.168.0.11/22給一段 IP 地址的機器賦權。
?口令驗證
?
可以理解為用戶名密碼的方式,這是我們最熟悉也是日常生活中經(jīng)常使用的模式,比如我們打開自己的電腦或者去銀行取錢都需要提供相應的密碼。
在 ZooKeeper 中這種驗證方式是 Digest 認證,我們知道通過網(wǎng)絡傳輸相對來說并不安全,所以絕不通過明文在網(wǎng)絡發(fā)送密碼也是程序設計中很重要的原則之一,而 Digest 這種認證方式首先在客戶端傳送username:password這種形式的權限表示符后,ZooKeeper 服務端會對密碼部分使用 SHA-1 和 BASE64 算法進行加密,以保證安全性。
?Super 權限模式
?
權限模式 Super 可以認為是一種特殊的 Digest 認證。
具有 Super 權限的客戶端可以對 ZooKeeper 上的任意數(shù)據(jù)節(jié)點進行任意操作。
下面這段代碼給出了 Digest 模式下客戶端的調(diào)用方式。
//創(chuàng)建節(jié)點
create?/digest_node1
//設置digest權限驗證
setAcl?/digest_node1?digest:用戶名:base64格式密碼:rwadc?
//查詢節(jié)點Acl權限
getAcl?/digest_node1?
//授權操作
addauth?digest?user:passwd
?如果一個客戶端對服務器上的一個節(jié)點設置了只有它自己才能操作的權限,那么等這個客戶端下線或被刪除后。
?
對其創(chuàng)建的節(jié)點要想進行修改應該怎么做呢?
我們可以通過「super 模式」即超級管理員的方式刪除該節(jié)點或變更該節(jié)點的權限驗證方式。
正因為「super 模式」有如此大的權限,我們在平時使用時也應該更加謹慎。
?world 模式
?
這種授權模式對應于系統(tǒng)中的所有用戶,本質(zhì)上起不到任何作用。
設置了 world 權限模式系統(tǒng)中的所有用戶操作都可以不進行權限驗證。
「授權對象(ID)」
所謂的授權對象就是說我們要把權限賦予誰,而對應于 4 種不同的權限模式來說,如果我們選擇采用 IP 方式,使用的授權對象可以是一個 IP 地址或 IP 地址段;而如果使用 Digest 或 Super 方式,則對應于一個用戶名。
如果是 World 模式,是授權系統(tǒng)中所有的用戶。
「權限信息(Permission)」
權限就是指我們可以在數(shù)據(jù)節(jié)點上執(zhí)行的操作種類,在 ZooKeeper 中已經(jīng)定義好的權限有 5 種:
數(shù)據(jù)節(jié)點(create)創(chuàng)建權限,授予權限的對象可以在數(shù)據(jù)節(jié)點下創(chuàng)建子節(jié)點;
數(shù)據(jù)節(jié)點(wirte)更新權限,授予權限的對象可以更新該數(shù)據(jù)節(jié)點;
數(shù)據(jù)節(jié)點(read)讀取權限,授予權限的對象可以讀取該節(jié)點的內(nèi)容以及子節(jié)點的信息;
數(shù)據(jù)節(jié)點(delete)刪除權限,授予權限的對象可以刪除該數(shù)據(jù)節(jié)點的子節(jié)點;
數(shù)據(jù)節(jié)點(admin)管理者權限,授予權限的對象可以對該數(shù)據(jù)節(jié)點體進行 ACL 權限設置。
?需要注意的一點是,每個節(jié)點都有維護自身的 ACL 權限數(shù)據(jù),即使是該節(jié)點的子節(jié)點也是有自己的 ACL 權限而不是直接繼承其父節(jié)點的權限。
?
「實現(xiàn)自己的權限口控制」
雖然 ZooKeeper 自身的權限控制機制已經(jīng)做得很細,但是它還是提供了一種權限擴展機制來讓用戶實現(xiàn)自己的權限控制方式。
官方文檔中對這種機制的定義是 Pluggable ZooKeeper Authenication,意思是可插拔的授權機制,從名稱上我們可以看出它的靈活性。那么這種機制是如何實現(xiàn)的呢?
?要想實現(xiàn)自定義的權限控制機制,最核心的一點是實現(xiàn) ZooKeeper 提供的權限控制器接口 AuthenticationProvider。
?
實現(xiàn)了自定義權限后,如何才能讓 ZooKeeper 服務端使用自定義的權限驗證方式呢?
接下來就需要將自定義的權限控制注冊到 ZooKeeper 服務器中,而注冊的方式通常有兩種。
- 第一種是通過設置系統(tǒng)屬性來注冊自定義的權限控制器:
-Dzookeeper.authProvider.x=CustomAuthenticationProvider
- 另一種是在配置文件
zoo.cfg中進行配置:
authProvider.x=CustomAuthenticationProvider
「實現(xiàn)原理」
首先是封裝該請求的類型,之后將權限信息封裝到 request 中并發(fā)送給服務端。而服務器的實現(xiàn)比較復雜,首先分析請求類型是否是權限相關操作,之后根據(jù)不同的權限模式(scheme)調(diào)用不同的實現(xiàn)類驗證權限最后存儲權限信息。
在授權接口中,值得注意的是會話的授權信息存儲在 ZooKeeper 服務端的內(nèi)存中,如果客戶端會話關閉,授權信息會被刪除。
下次連接服務器后,需要重新調(diào)用授權接口進行授權。
序列化方式在 ZooKeeper 中并沒有采用和 Java 一樣的序列化方式,而是采用了一個 Jute 的序列解決方案作為 ZooKeeper 框架自身的序列化方式。
?ZooKeeper 從最開始就采用 Jute 作為其序列化解決方案,直到其最新的版本依然沒有更改。
?
雖然 ZooKeeper 一直將 Jute 框架作為序列化解決方案,但這并不意味著 Jute 相對其他框架性能更好,反倒是 Apache Avro、Thrift 等框架在性能上優(yōu)于前者。
之所以 ZooKeeper 一直采用 Jute 作為序列化解決方案,主要是新老版本的兼容等問題。
「如何 使用 Jute 實現(xiàn)序列化」
如果我們要想將某個定義的類進行序列化,首先需要該類實現(xiàn) Record 接口的 serilize 和 deserialize 方法,這兩個方法分別是序列化和反序列化方法。
?下邊這段代碼給出了我們一般在 ZooKeeper 中進行序列化的具體實現(xiàn):
?
首先,我們定義了一個test_jute類,為了能夠?qū)λM行序列化,需要該test_jute類實現(xiàn) Record 接口,并在對應的 serialize 序列化方法和 deserialize 反序列化方法中編輯具體的實現(xiàn)邏輯。
class?test_jute?implements?Record{
??private?long?ids;
??private?String?name;
??...
??public?void?serialize(OutpurArchive?a_,String?tag){
????...
??}
??public?void?deserialize(INputArchive?a_,String?tag){
????...
??}
}
在序列化方法 serialize 中,我們要實現(xiàn)的邏輯是,首先通過字符類型參數(shù) tag 傳遞標記序列化標識符,之后使用 writeLong 和 writeString 等方法分別將對象屬性字段進行序列化。
public?void?serialize(OutpurArchive?a_,String?tag)?throws?...{
??a_.startRecord(this.tag);
??a_.writeLong(ids,"ids");
??a_.writeString(type,"name");
??a_.endRecord(this,tag);
}
調(diào)用 derseralize 在實現(xiàn)反序列化的過程則與我們上邊說的序列化過程正好相反。
public?void?deserialize(INputArchive?a_,String?tag)?throws?{
??a_.startRecord(tag);
??ids?=?a_.readLong("ids");
??name?=?a_.readString("name");
??a_.endRecord(tag);
}
序列化和反序列化的實現(xiàn)邏輯編碼方式相對固定,首先通過 startRecord 開啟一段序列化操作,之后通過 writeLong、writeString 或 readLong、 readString 等方法執(zhí)行序列化或反序列化。
本例中只是實現(xiàn)了長整型和字符型的序列化和反序列化操作,除此之外 ZooKeeper 中的 Jute 框架還支持整數(shù)類型(Int)、布爾類型(Bool)、雙精度類型(Double)以及 Byte/Buffer 類型。
集群「ZooKeeper集群模式的特點」
在 ZooKeeper 集群中將服務器分成 「Leader 、Follow 、Observer 三」種角色服務器,在集群運行期間這三種服務器所負責的工作各不相同:
Leader 角色服務器負責管理集群中其他的服務器,是集群中工作的分配和調(diào)度者,既可以為客戶端提供寫服務又能提供讀服務。
Follow 服務器的主要工作是選舉出 Leader 服務器,在發(fā)生 Leader 服務器選舉的時候,系統(tǒng)會從 Follow 服務器之間根據(jù)多數(shù)投票原則,選舉出一個 Follow 服務器作為新的 Leader 服務器,只能提供讀服務。
Observer 服務器則主要負責處理來自客戶端的獲取數(shù)據(jù)等請求,并不參與 Leader 服務器的選舉操作,也不會作為候選者被選舉為 Leader 服務器,只能提供讀服務。
在 ZooKeeper 集群接收到來自客戶端的會話請求操作后,首先會判斷該條請求是否是事務性的會話請求。
?對于事務性的會話請求,ZooKeeper 集群服務端會將該請求統(tǒng)一轉(zhuǎn)發(fā)給 Leader 服務器進行操作。
所謂事務性請求,是指 ZooKeeper 服務器執(zhí)行完該條會話請求后,是否會導致執(zhí)行該條會話請求的服務器的數(shù)據(jù)或狀態(tài)發(fā)生改變,進而導致與其他集群中的服務器出現(xiàn)數(shù)據(jù)不一致的情況。
?
Leader 服務器內(nèi)部執(zhí)行該條事務性的會話請求后,再將數(shù)據(jù)同步給其他角色服務器,從而保證事務性會話請求的執(zhí)行順序,進而保證整個 ZooKeeper 集群的數(shù)據(jù)一致性。
?在 ZooKeeper 集群的內(nèi)部實現(xiàn)中,是通過什么方法保證所有 ZooKeeper 集群接收到的事務性會話請求都能交給 Leader 服務器進行處理的呢?
?
在 ZooKeeper 集群內(nèi)部,集群中除 Leader 服務器外的其他角色服務器接收到來自客戶端的事務性會話請求后,必須將該條會話請求轉(zhuǎn)發(fā)給 Leader 服務器進行處理。
ZooKeeper 集群中的 Follow 和 Observer 服務器,都會檢查當前接收到的會話請求是否是事務性的請求,如果是事務性的請求,那么就將該請求以 REQUEST 消息類型轉(zhuǎn)發(fā)給 Leader 服務器。
在 ZooKeeper集群中的服務器接收到該條消息后,會對該條消息進行解析。
分析出該條消息所包含的原始客戶端會話請求。
之后將該條消息提交到自己的 Leader 服務器請求處理鏈中,開始進行事務性的會話請求操作。
如果不是事務性請求,ZooKeeper 集群則交由 Follow 和 Observer 角色服務器處理該條會話請求,如查詢數(shù)據(jù)節(jié)點信息。
當一個業(yè)務場景在查詢操作多而創(chuàng)建刪除等事務性操作少的情況下,ZooKeeper 集群的性能表現(xiàn)的就會很好。
?如果是在極端情況下,ZooKeeper 集群只有事務性的會話請求而沒有查詢操作,那么 Follow 和 Observer 服務器就只能充當一個請求轉(zhuǎn)發(fā)服務器的角色, 所有的會話的處理壓力都在 Leader 服務器。
?
在處理性能上整個集群服務器的瓶頸取決于 Leader 服務器的性能。
?ZooKeeper 集群的作用只能保證在 Leader 節(jié)點崩潰的時候,重新選舉出 Leader 服務器保證系統(tǒng)的穩(wěn)定性。
?
這也是 ZooKeeper 設計的一個缺點。
「Leader選舉」
Leader 服務器的選舉操作主要發(fā)生在兩種情況下。
第一種就是 ZooKeeper 集群服務啟動的時候,第二種就是在 ZooKeeper 集群中舊的 Leader 服務器失效時,這時 ZooKeeper 集群需要選舉出新的 Leader 服務器。
?ZooKeeper 集群重新選舉 Leader 的過程只有 Follow 服務器參與工作。
?
?服務器狀態(tài)
?
服務器具有四種狀態(tài),分別是LOOKING、FOLLOWING、LEADING、OBSERVING。
「LOOKING」:尋找Leader狀態(tài)。當服務器處于該狀態(tài)時,它會認為當前集群中沒有Leader,因此需要進入Leader選舉狀態(tài)。
「FOLLOWING」:跟隨者狀態(tài)。表明當前服務器角色是Follower。
「LEADING」:領導者狀態(tài)。表明當前服務器角色是Leader。
「OBSERVING」:觀察者狀態(tài)。表明當前服務器角色是Observer。
「事務ID(zxid)」
Zookeeper的狀態(tài)變化,都會由一個Zookeeper事務ID(ZXID)標識。
?寫入Zookeeper,會導致狀態(tài)變化,每次寫入都會導致ZXID發(fā)生變化。
?
ZXID由Leader統(tǒng)一分配,全局唯一,長度64位,遞增。
ZXID展示了所有的Zookeeper轉(zhuǎn)臺變更順序,每次變更都有一個唯一ZXID,如果zxid1小于zxid2,則說明zxid1的事務在zxid2的事務之前發(fā)生。
「選舉過程」
在 ZooKeeper 集群重新選舉 Leader 節(jié)點的過程中,主要可以分為 Leader 失效發(fā)現(xiàn)、重新選舉 Leader 、Follow 服務器角色變更、集群同步這幾個步驟。
?Leader 失效發(fā)現(xiàn)
?
在 ZooKeeper 集群中,當 Leader 服務器失效時,ZooKeeper 集群會重新選舉出新的 Leader 服務器。
- 在 ZooKeeper 集群中,探測 Leader 服務器是否存活的方式與保持客戶端活躍性的方法非常相似。
首先,F(xiàn)ollow 服務器會定期向 Leader 服務器發(fā)送 網(wǎng)絡請求,在接收到請求后,Leader 服務器會返回響應數(shù)據(jù)包給 Follow 服務器,而在 Follow 服務器接收到 Leader 服務器的響應后,如果判斷 Leader 服務器運行正常,則繼續(xù)進行數(shù)據(jù)同步和服務轉(zhuǎn)發(fā)等工作,反之,則進行 Leader 服務器的重新選舉操作。
?Leader重新選舉
?
當 Follow 服務器向 Leader 服務器發(fā)送狀態(tài)請求包后,如果沒有得到 Leader 服務器的返回信息,這時,如果是集群中個別的 Follow 服務器發(fā)現(xiàn)返回錯誤,并不會導致 ZooKeeper 集群立刻重新選舉 Leader 服務器,而是將該 Follow 服務器的狀態(tài)變更為 LOOKING 狀態(tài),并向網(wǎng)絡中發(fā)起投票,當 ZooKeeper 集群中有更多的機器發(fā)起投票,最后當投票結果滿足多數(shù)原則的情況下。
ZooKeeper 會重新選舉出 Leader 服務器。
?Follow 角色變更
?
在 ZooKeeper 集群中,F(xiàn)ollow 服務器作為 Leader 服務器的候選者,當被選舉為 Leader 服務器之后,其在 ZooKeeper 集群中的 Follow 角色,也隨之發(fā)生改變。也就是要轉(zhuǎn)變?yōu)?Leader 服務器,并作為 ZooKeeper 集群中的 Leader 角色服務器對外提供服務。
?集群同步數(shù)據(jù)
?
在 ZooKeeper 集群成功選舉 Leader 服務器,并且候選 Follow 服務器的角色變更后。
為避免在這期間導致的數(shù)據(jù)不一致問題,ZooKeeper 集群在對外提供服務之前,會通過 Leader 角色服務器管理同步其他角色服務器。
「底層實現(xiàn)」
首先,ZooKeeper 集群會先判斷 Leader 服務器是否失效,而判斷的方式就是 Follow 服務器向 Leader 服務器發(fā)送請求包,之后 Follow 服務器接收到響應數(shù)據(jù)后,進行解析,F(xiàn)ollow 服務器會根據(jù)返回的數(shù)據(jù),判斷 Leader 服務器的運行狀態(tài),如果返回的是 LOOKING 關鍵字,表明與集群中 Leader 服務器無法正常通信。
- 之后,在 ZooKeeper 集群選舉 Leader 服務器時,是通過 「FastLeaderElection」 類實現(xiàn)的。
該類實現(xiàn)了 TCP 方式的通信連接,用于在 ZooKeeper 集群中與其他 Follow 服務器進行協(xié)調(diào)溝通。
FastLeaderElection 類繼承了 Election 接口,定義其是用來進行選舉的實現(xiàn)類。
- 而在其內(nèi)部,又定義了選舉通信相關的一些配置參數(shù),比如 finalizeWait 最終等待時間、最大通知間隔時間 maxNotificationInterval 等。
在選舉的過程中,首先調(diào)用 ToSend 函數(shù)向 ZooKeeper 集群中的其他角色服務器發(fā)送本機的投票信息,其他服務器在接收投票信息后,會對投票信息進行有效性驗證等操作,之后 ZooKeeper 集群統(tǒng)計投票信息,如果過半數(shù)的機器投票信息一致,則集群就重新選出新的 Leader 服務器。
?這里我們要注意一個問題,那就是在重新選舉 Leader 服務器的過程中,ZooKeeper 集群理論上是無法進行事務性的請求處理的。
?
因此,發(fā)送到 ZooKeeper 集群中的事務性會話會被掛起,暫時不執(zhí)行,等到選舉出新的 Leader 服務器后再進行操作。
「Observer」
在 ZooKeeper 集群服務運行的過程中,Observer 服務器與 Follow 服務器具有一個相同的功能,那就是負責處理來自客戶端的諸如查詢數(shù)據(jù)節(jié)點等非事務性的會話請求操作。
- 但與 Follow 服務器不同的是,Observer 不參與 Leader 服務器的選舉工作,也不會被選舉為 Leader 服務器。
在早期的 ZooKeeper 集群服務運行過程中,只有 Leader 服務器和 Follow 服務器。
不過隨著 ZooKeeper 在分布式環(huán)境下的廣泛應用,早期模式的設計缺點也隨之產(chǎn)生,主要帶來的問題有如下幾點:
隨著集群規(guī)模的變大,集群處理寫入的性能反而下降。
ZooKeeper 集群無法做到跨域部署。
其中最主要的問題在于,當 ZooKeeper 集群的規(guī)模變大,集群中 Follow 服務器數(shù)量逐漸增多的時候,ZooKeeper 處理創(chuàng)建數(shù)據(jù)節(jié)點等事務性請求操作的性能就會逐漸下降。
這是因為 ZooKeeper 集群在處理事務性請求操作時,要在 ZooKeeper 集群中對該事務性的請求發(fā)起投票,只有超過半數(shù)的 Follow 服務器投票一致,才會執(zhí)行該條寫入操作。
正因如此,隨著集群中 Follow 服務器的數(shù)量越來越多,一次寫入等相關操作的投票也就變得越來越復雜,并且 Follow 服務器之間彼此的網(wǎng)絡通信也變得越來越耗時,導致隨著 Follow 服務器數(shù)量的逐步增加,事務性的處理性能反而變得越來越低。
- 為了解決這一問題,在 ZooKeeper 3.6 版本后,ZooKeeper 集群中創(chuàng)建了一種新的服務器角色,即 Observer——觀察者角色服務器。
Observer 可以處理 ZooKeeper 集群中的非事務性請求,并且不參與 Leader 節(jié)點等投票相關的操作。
這樣既保證了 ZooKeeper 集群性能的擴展性,又避免了因為過多的服務器參與投票相關的操作而影響 ZooKeeper 集群處理事務性會話請求的能力。
- 在實際部署的時候,因為 Observer 不參與 Leader 節(jié)點等操作,并不會像 Follow 服務器那樣頻繁的與 Leader 服務器進行通信。
因此,可以將 Observer 服務器部署在不同的網(wǎng)絡區(qū)間中,這樣也不會影響整個 ZooKeeper 集群的性能,也就是所謂的跨域部署。
「在我們?nèi)粘J褂?ZooKeeper 集群服務器的時候,集群中的機器個數(shù)應該選擇奇數(shù)個?」
兩個原因:
?在容錯能力相同的情況下,奇數(shù)臺更節(jié)省資源
?
Zookeeper中 Leader 選舉算法采用了Zab協(xié)議。
Zab核心思想是當多數(shù) Server 寫成功,則寫成功。
舉兩個例子:
假如zookeeper集群1 ,有3個節(jié)點,3/2=1.5 , ?即zookeeper想要正常對外提供服務(即leader選舉成功),至少需要2個節(jié)點是正常的。換句話說,3個節(jié)點的zookeeper集群,允許有一個節(jié)點宕機。
假如zookeeper集群2,有4個節(jié)點,4/2=2 , 即zookeeper想要正常對外提供服務(即leader選舉成功),至少需要3個節(jié)點是正常的。換句話說,4個節(jié)點的zookeeper集群,也允許有一個節(jié)點宕機。
集群1與集群2都有 允許1個節(jié)點宕機 的容錯能力,但是集群2比集群1多了1個節(jié)點。在相同容錯能力的情況下,本著節(jié)約資源的原則,zookeeper集群的節(jié)點數(shù)維持奇數(shù)個更好一些。
?防止由腦裂造成的集群不可用。
?
集群的腦裂通常是發(fā)生在節(jié)點之間通信不可達的情況下,集群會分裂成不同的小集群,小集群各自選出自己的master節(jié)點,導致原有的集群出現(xiàn)多個master節(jié)點的情況,這就是腦裂。
下面舉例說一下為什么采用奇數(shù)臺節(jié)點,就可以防止由于腦裂造成的服務不可用:
假如zookeeper集群有 5 個節(jié)點,發(fā)生了腦裂,腦裂成了A、B兩個小集群:
A :1個節(jié)點 ,B :4個節(jié)點
A :2個節(jié)點, B :3個節(jié)點
可以看出,上面這兩種情況下,A、B中總會有一個小集群滿足 可用節(jié)點數(shù)量 > 總節(jié)點數(shù)量/2 。
所以zookeeper集群仍然能夠選舉出leader , 仍然能對外提供服務,只不過是有一部分節(jié)點失效了而已。
假如zookeeper集群有4個節(jié)點,同樣發(fā)生腦裂,腦裂成了A、B兩個小集群:
A:1個節(jié)點 , ?B:3個節(jié)點
A:2個節(jié)點 , B:2個節(jié)點
因為A和B都是2個節(jié)點,都不滿足 可用節(jié)點數(shù)量 > 總節(jié)點數(shù)量/2 的選舉條件, 所以此時zookeeper就徹底不能提供服務了。
ZAB協(xié)議「ZAB 協(xié)議算法」
ZooKeeper 最核心的作用就是保證分布式系統(tǒng)的數(shù)據(jù)一致性,而無論是處理來自客戶端的會話請求時,還是集群 Leader 節(jié)點發(fā)生重新選舉時,都會產(chǎn)生數(shù)據(jù)不一致的情況。
?為了解決這個問題,ZooKeeper 采用了 ZAB 協(xié)議算法。
?
ZAB 協(xié)議算法(Zookeeper Atomic Broadcast ?,Zookeeper 原子廣播協(xié)議)是 ZooKeeper 專門設計用來解決集群最終一致性問題的算法,它的兩個核心功能點是崩潰恢復和原子廣播協(xié)議。
- 在整個 ZAB 協(xié)議的底層實現(xiàn)中,ZooKeeper 集群主要采用主從模式的系統(tǒng)架構方式來保證 ZooKeeper 集群系統(tǒng)的一致性。
當接收到來自客戶端的事務性會話請求后,系統(tǒng)集群采用主服務器來處理該條會話請求,經(jīng)過主服務器處理的結果會通過網(wǎng)絡發(fā)送給集群中其他從節(jié)點服務器進行數(shù)據(jù)同步操作。
?以 ZooKeeper 集群為例,這個操作過程可以概括為:
?
當 ZooKeeper 集群接收到來自客戶端的事務性的會話請求后,集群中的其他 Follow 角色服務器會將該請求轉(zhuǎn)發(fā)給 Leader 角色服務器進行處理。
當 Leader 節(jié)點服務器在處理完該條會話請求后,會將結果通過操作日志的方式同步給集群中的 Follow 角色服務器。
然后 Follow 角色服務器根據(jù)接收到的操作日志,在本地執(zhí)行相關的數(shù)據(jù)處理操作,最終完成整個 ZooKeeper 集群對客戶端會話的處理工作。
「崩潰恢復」
當集群中的 Leader 發(fā)生故障的時候,整個集群就會因為缺少 Leader 服務器而無法處理來自客戶端的事務性的會話請求。
?因此,為了解決這個問題。在 ZAB 協(xié)議中也設置了處理該問題的崩潰恢復機制。
?
崩潰恢復機制是保證 ZooKeeper 集群服務高可用的關鍵。觸發(fā) ZooKeeper 集群執(zhí)行崩潰恢復的事件是集群中的 Leader 節(jié)點服務器發(fā)生了異常而無法工作,于是 Follow 服務器會通過投票來決定是否選出新的 Leader 節(jié)點服務器。
?投票過程如下:
?
當崩潰恢復機制開始的時候,整個 ZooKeeper 集群的每臺 Follow 服務器會發(fā)起投票,并同步給集群中的其他 Follow 服務器。
在接收到來自集群中的其他 Follow 服務器的投票信息后,集群中的每個 Follow 服務器都會與自身的投票信息進行對比,如果判斷新的投票信息更合適,則采用新的投票信息作為自己的投票信息。在集群中的投票信息還沒有達到超過半數(shù)原則的情況下,再進行新一輪的投票,最終當整個 ZooKeeper 集群中的 Follow 服務器超過半數(shù)投出的結果相同的時候,就會產(chǎn)生新的 Leader 服務器。
?選票結構:
?
以 Fast Leader Election 選舉的實現(xiàn)方式來講,如下圖所示,一個選票的整體結果可以分為一下六個部分:

logicClock:用來記錄服務器的投票輪次。logicClock 會從 1 開始計數(shù),每當該臺服務經(jīng)過一輪投票后,logicClock 的數(shù)值就會加 1 。
state:用來標記當前服務器的狀態(tài)。在 ZooKeeper 集群中一臺服務器具有 LOOKING、FOLLOWING、LEADERING、OBSERVING 這四種狀態(tài)。
self_id:用來表示當前服務器的 ID 信息,該字段在 ZooKeeper 集群中主要用來作為服務器的身份標識符。self_zxid:當前服務器上所保存的數(shù)據(jù)的最大事務 ID ,從 0 開始計數(shù)。vote_id:投票要被推舉的服務器的唯一 ID 。vote_zxid:被推舉的服務器上所保存的數(shù)據(jù)的最大事務 ID ,從 0 開始計數(shù)。
當 ZooKeeper 集群需要重新選舉出新的 Leader 服務器的時候,就會根據(jù)上面介紹的投票信息內(nèi)容進行對比,以找出最適合的服務器。
?選票篩選
?
當一臺 Follow 服務器接收到網(wǎng)絡中的其他 Follow 服務器的投票信息后,是如何進行對比來更新自己的投票信息的。
Follow 服務器進行選票對比的過程,如下圖所示。

首先,會對比 logicClock 服務器的投票輪次,當 logicClock 相同時,表明兩張選票處于相同的投票階段,并進入下一階段,否則跳過。
接下來再對比vote_zxid被選舉的服務器 ID 信息,若接收到的外部投票信息中的 vote_zxid字段較大,則將自己的票中的vote_zxid與vote_myid更新為收到的票中的vote_zxid與vote_myid ,并廣播出去。
要是對比的結果相同,則繼續(xù)對比vote_myid被選舉服務器上所保存的最大事務 ID ,若外部投票的vote_myid 比較大,則將自己的票中的 vote_myid更新為收到的票中的vote_myid 。
經(jīng)過這些對比和替換后,最終該臺 Follow 服務器會產(chǎn)生新的投票信息,并在下一輪的投票中發(fā)送到 ZooKeeper 集群中。
「消息廣播」
在 Leader 節(jié)點服務器處理請求后,需要通知集群中的其他角色服務器進行數(shù)據(jù)同步。ZooKeeper 集群采用消息廣播的方式發(fā)送通知。
ZooKeeper 集群使用原子廣播協(xié)議進行消息發(fā)送,該協(xié)議的底層實現(xiàn)過程與二階段提交過程非常相似,如下圖所示。

當要在集群中的其他角色服務器進行數(shù)據(jù)同步的時候,Leader 服務器將該操作過程封裝成一個 Proposal 提交事務,并將其發(fā)送給集群中其他需要進行數(shù)據(jù)同步的服務器。
當這些服務器接收到 Leader 服務器的數(shù)據(jù)同步事務后,會將該條事務能否在本地正常執(zhí)行的結果反饋給 Leader 服務器,Leader 服務器在接收到其他 Follow 服務器的反饋信息后進行統(tǒng)計,判斷是否在集群中執(zhí)行本次事務操作。
這里請注意 ,與二階段提交過程不同(即需要集群中所有服務器都反饋可以執(zhí)行事務操作后,主服務器再次發(fā)送 commit 提交請求執(zhí)行數(shù)據(jù)變更) ,ZAB 協(xié)議算法省去了中斷的邏輯,當 ZooKeeper 集群中有超過一半的 Follow 服務器能夠正常執(zhí)行事務操作后,整個 ZooKeeper 集群就可以提交 Proposal 事務了。
日志清理「日志類型」
在 ZooKeeper 服務運行的時候,一般會產(chǎn)生數(shù)據(jù)快照和日志文件,數(shù)據(jù)快照用于集群服務中的數(shù)據(jù)同步,而數(shù)據(jù)日志則記錄了 ZooKeeper 服務運行的相關狀態(tài)信息。
?其中,數(shù)據(jù)日志是我們在生產(chǎn)環(huán)境中需要定期維護和管理的文件。
?
「清理方案」
如上面所介紹的,面對生產(chǎn)系統(tǒng)中產(chǎn)生的日志,一般的維護操作是備份和清理。
備份是為了之后對系統(tǒng)的運行情況進行排查和優(yōu)化,而清理主要因為隨著系統(tǒng)日志的增加,日志會逐漸占用系統(tǒng)的存儲空間,如果一直不進行清理,可能耗盡系統(tǒng)的磁盤存儲空間,并最終影響服務的運行。
「清理工具」
?Corntab
?
首先,我們介紹的是 Linux corntab ,它是 Linux 系統(tǒng)下的軟件,可以自動地按照我們設定的時間,周期性地執(zhí)行我們編寫的相關腳本。
crontab 定時腳本的方式相對靈活,可以按照我們的業(yè)務需求來設置處理日志的維護方式,比如這里我們希望定期清除 ZooKeeper 服務運行的日志,而不想清除數(shù)據(jù)快照的文件,則可以通過腳本設置,達到只對數(shù)據(jù)日志文件進行清理的目的。
?PurgeTxnLog
?
ZooKeeper 自身還提供了 PurgeTxnLog 工具類,用來清理 snapshot 數(shù)據(jù)快照文件和系統(tǒng)日志。
PurgeTxnLog 清理方式和我們上面介紹的方式十分相似,也是通過定時腳本執(zhí)行任務,唯一的不同是,上面提到在編寫日志清除 logsCleanWeek 的時候 ,我們使用的是原生 shell 腳本自己手動編寫的數(shù)據(jù)日志清理邏輯,而使用 PurgeTxnLog 則可以在編寫清除腳本的時候調(diào)用 ZooKeeper 為我們提供的工具類完成日志清理工作。
如下面的代碼所示,首先,我們在/usr/bin目錄下創(chuàng)建一個 PurgeLogsClean 腳本。注意這里的腳本也是一個 shell 文件。
在腳本中我們只需要編寫 PurgeTxnLog 類的調(diào)用程序,系統(tǒng)就會自動通過 PurgeTxnLog 工具類為我們完成對應日志文件的清理工作。
#!/bin/sh??
java?-cp?"$CLASSPATH"?org.apache.zookeeper.server.PurgeTxnLog?
echo?"清理完成"?
PurgeTxnLog 方式與 crontab 相比,使用起來更加容易而且也更加穩(wěn)定安全,不過 crontab 方式更加靈活,我們可以根據(jù)不同的業(yè)務需求編寫自己的清理邏輯。
實現(xiàn)分布式鎖分布式鎖的目的是保證在分布式部署的應用集群中,多個服務在請求同一個方法或者同一個業(yè)務操作的情況下,對應業(yè)務邏輯只能被一臺機器上的一個線程執(zhí)行,避免出現(xiàn)并發(fā)問題。
?實現(xiàn)分布式鎖目前有三種流行方案,即基于數(shù)據(jù)庫、Redis、ZooKeeper 的方案
?
「方案一:」
使用節(jié)點中的存儲數(shù)據(jù)區(qū)域,ZK中節(jié)點存儲數(shù)據(jù)的大小不能超過1M,但是只是存放一個標識是足夠的,線程獲得鎖時,先檢查該標識是否是無鎖標識,若是可修改為占用標識,使用完再恢復為無鎖標識
「方案二:」
使用子節(jié)點,每當有線程來請求鎖的時候,便在鎖的節(jié)點下創(chuàng)建一個子節(jié)點,子節(jié)點類型必須維護一個順序,對子節(jié)點的自增序號進行排序,默認總是最小的子節(jié)點對應的線程獲得鎖,釋放鎖時刪除對應子節(jié)點便可

「死鎖風險:」
兩種方案其實都是可行的,但是使用鎖的時候一定要去規(guī)避死鎖
方案一看上去是沒問題的,用的時候設置標識,用完清除標識,但是要是持有鎖的線程發(fā)生了意外,釋放鎖的代碼無法執(zhí)行,鎖就無法釋放,其他線程就會一直等待鎖,相關同步代碼便無法執(zhí)行
方案二也存在這個問題,但方案二可以利用ZK的臨時順序節(jié)點來解決這個問題,只要線程發(fā)生了異常導致程序中斷,就會丟失與ZK的連接,ZK檢測到該鏈接斷開,就會自動刪除該鏈接創(chuàng)建的臨時節(jié)點,這樣就可以達到即使占用鎖的線程程序發(fā)生意外,也能保證鎖正常釋放的目的
「避免羊群效應」
把鎖請求者按照后綴數(shù)字進行排隊,后綴數(shù)字小的鎖請求者先獲取鎖。
如果所有的鎖請求者都 watch 鎖持有者,當代表鎖請求者的 znode 被刪除以后,所有的鎖請求者都會通知到,但是只有一個鎖請求者能拿到鎖。這就是羊群效應。
?為了避免羊群效應,每個鎖請求者 watch 它前面的鎖請求者。
?
每次鎖被釋放,只會有一個鎖請求者 會被通知到。
這樣做還讓鎖的分配具有公平性,鎖定的分配遵循先到先得的原則。

「用 ZooKeeper 實現(xiàn)分布式鎖的算法流程,根節(jié)點為 /lock:」
客戶端連接 ZooKeeper,并
在/lock下創(chuàng)建臨時有序子節(jié)點,第一個客戶端對應的子節(jié)點為/lock/lock01/00000001,第二個為/lock/lock01/00000002;其他客戶端獲取
/lock01下的子節(jié)點列表,判斷自己創(chuàng)建的子節(jié)點是否為當前列表中序號最小的子節(jié)點;如果是則認為獲得鎖,執(zhí)行業(yè)務代碼,否則通過 watch 事件監(jiān)聽
/lock01的子節(jié)點變更消息,獲得變更通知后重復此步驟直至獲得鎖;完成業(yè)務流程后,刪除對應的子節(jié)點,釋放分布式鎖;
在實際開發(fā)中,可以應用 Apache Curator 來快速實現(xiàn)分布式鎖,Curator 是 Netflix 公司開源的一個 ZooKeeper 客戶端,對 ZooKeeper 原生 API 做了抽象和封裝。
實現(xiàn)分布式ID我們可以通過 ZooKeeper 自身的客戶端和服務器運行模式,來實現(xiàn)一個分布式網(wǎng)絡環(huán)境下的 ID 請求和分發(fā)過程。
?每個需要 ID 編碼的業(yè)務服務器可以看作是 ZooKeeper 的客戶端。ID 編碼生成器可以作為 ZooKeeper 的服務端。
?
客戶端通過發(fā)送請求到 ZooKeeper 服務器,來獲取編碼信息,服務端接收到請求后,發(fā)送 ID 編碼給客戶端。
「實現(xiàn)原理:」
可以利用 ZooKeeper 數(shù)據(jù)模型中的順序節(jié)點作為 ID 編碼。
客戶端通過調(diào)用 create 函數(shù)創(chuàng)建順序節(jié)點。服務器成功創(chuàng)建節(jié)點后,會響應客戶端請求,把創(chuàng)建好的節(jié)點信息發(fā)送給客戶端。
客戶端用數(shù)據(jù)節(jié)點名稱作為 ID 編碼,進行之后的本地業(yè)務操作。
利用 ZooKeeper 中的順序節(jié)點特性,很容易使我們創(chuàng)建的 ID 編碼具有有序的特性。并且我們也可以通過客戶端傳遞節(jié)點的名稱,根據(jù)不同的業(yè)務編碼區(qū)分不同的業(yè)務系統(tǒng),從而使編碼的擴展能力更強。
?雖然使用 ZooKeeper 的實現(xiàn)方式有這么多優(yōu)點,但也會有一些潛在的問題。
?
其中最主要的是,在定義編碼的規(guī)則上還是強烈依賴于程序員自身的能力和對業(yè)務的深入理解。
很容易出現(xiàn)因為考慮不周,造成設置的規(guī)則在運行一段時間后,無法滿足業(yè)務要求或者安全性不夠等問題。
實現(xiàn)負載均衡「常見負載均衡算法」
?輪詢法
?
輪詢法是最為簡單的負載均衡算法,當接收到來自網(wǎng)絡中的客戶端請求后,負載均衡服務器會按順序逐個分配給后端服務。
比如集群中有 3 臺服務器,分別是 server1、server2、server3,輪詢法會按照 sever1、server2、server3 這個順序依次分發(fā)會話請求給每個服務器。當?shù)谝淮屋喸兘Y束后,會重新開始下一輪的循環(huán)。
?隨機法
?
隨機算法是指負載均衡服務器在接收到來自客戶端的請求后,會根據(jù)一定的隨機算法選中后臺集群中的一臺服務器來處理這次會話請求。
不過,當集群中備選機器變的越來越多時,通過統(tǒng)計學我們可以知道每臺機器被抽中的概率基本相等,因此隨機算法的實際效果越來越趨近輪詢算法。
?原地址哈希法
?
原地址哈希算法的核心思想是根據(jù)客戶端的 IP 地址進行哈希計算,用計算結果進行取模后,根據(jù)最終結果選擇服務器地址列表中的一臺機器,處理該條會話請求。
采用這種算法后,當同一 IP 的客戶端再次訪問服務端后,負載均衡服務器最終選舉的還是上次處理該臺機器會話請求的服務器,也就是每次都會分配同一臺服務器給客戶端。
?加權輪詢法
?
加權輪詢的方式與輪詢算法的方式很相似,唯一的不同在于選擇機器的時候,不只是單純按照順序的方式選擇,還根據(jù)機器的配置和性能高低有所側重,配置性能好的機器往往首先分配。
?加權隨機法
?
加權隨機法和我們上面提到的隨機算法一樣,在采用隨機算法選舉服務器的時候,會考慮系統(tǒng)性能作為權值條件。
?最小連接數(shù)法
?
最小連接數(shù)算法是指,根據(jù)后臺處理客戶端的連接會話條數(shù),計算應該把新會話分配給哪一臺服務器。
一般認為,連接數(shù)越少的機器,在網(wǎng)絡帶寬和計算性能上都有很大優(yōu)勢,會作為最優(yōu)先分配的對象。
「利用 ZooKeeper 實現(xiàn) 負載均衡 算法」
?這里我們通過采用最小連接數(shù)算法,來確定究竟如何均衡地分配網(wǎng)絡會話請求給后臺客戶端。
?
如下圖所示,建立的 ZooKeeper 數(shù)據(jù)模型中 Severs 節(jié)點可以作為存儲服務器列表的父節(jié)點。
在它下面創(chuàng)建 servers_host1、servers_host2、servers_host3等臨時節(jié)點來存儲集群中的服務器運行狀態(tài)信息。

整個實現(xiàn)的過程如下圖所示。

首先,在接收到客戶端的請求后,通過 getData 方法獲取服務端 Severs 節(jié)點下的服務器列表,其中每個節(jié)點信息都存儲有當前服務器的連接數(shù)。
通過判斷選擇最少的連接數(shù)作為當前會話的處理服務器,并通過 setData 方法將該節(jié)點連接數(shù)加 1。
最后,當客戶端執(zhí)行完畢,再調(diào)用 setData 方法將該節(jié)點信息減 1。
我們定義當服務器接收到會話請求后。在 ZooKeeper 服務端增加連接數(shù)的 addBlance 方法。
我們通過 readData 方法獲取服務器最新的連接數(shù),之后將該連接數(shù)加 1,再通過 writeData 方法將新的連接數(shù)信息寫入到服務端對應節(jié)點信息中。
當服務器處理完該會話請求后,需要更新服務端相關節(jié)點的連接數(shù)。
具體的操作與 addBlance 方法基本一樣,只是對獲取的連接信息進行減一操作。
「這里注意:」
我們?nèi)粘S玫降呢撦d均衡器主要是選擇后臺處理的服務器,并給其分發(fā)請求。
?而通過 ZooKeeper 實現(xiàn)的服務器,只提供了服務器的篩選工作。
?
在請求分發(fā)的過程中,還是通過負載算法計算出要訪問的服務器,之后客戶端自己連接該服務器,完成請求操作。
開源框架使用案例「Dubbo與ZooKeeper」
Dubbo 是阿里巴巴開發(fā)的一套開源的技術框架,是一款高性能、輕量級的開源 Java RPC 框架。
「用ZooKeeper做注冊中心」
在整個 Dubbo 框架的實現(xiàn)過程中,注冊中心是其中最為關鍵的一點,它保證了整個 PRC 過程中服務對外的透明性。
而 Dubbo 的注冊中心也是通過 ZooKeeper 來實現(xiàn)的。
如下圖所示,在整個 Dubbo 服務的啟動過程中,服務提供者會在啟動時向 /dubbo/com.foo.BarService/providers目錄寫入自己的 URL 地址,這個操作可以看作是一個 ZooKeeper 客戶端在 ZooKeeper 服務器的數(shù)據(jù)模型上創(chuàng)建一個數(shù)據(jù)節(jié)點。
服務消費者在啟動時訂閱 /dubbo/com.foo.BarService/providers 目錄下的提供者 URL 地址,并向 /dubbo/com.foo.BarService/consumers 目錄寫入自己的 URL 地址。
該操作是通過 ZooKeeper 服務器在 /consumers 節(jié)點路徑下創(chuàng)建一個子數(shù)據(jù)節(jié)點,然后再在請求會話中發(fā)起對 /providers 節(jié)點的 watch 監(jiān)控

「Kafka與ZooKeeper」
「Zookeeper的作用」
由于 Broker 服務器采用分布式集群的方式工作,那么在服務的運行過程中,難免出現(xiàn)某臺機器因異常而關閉的狀況。
為了保證整個 Kafka 集群的可用性,需要在系統(tǒng)中監(jiān)控整個機器的運行情況。而 Kafka 可以通過 ZooKeeper 中的數(shù)據(jù)節(jié)點,將網(wǎng)絡中機器的運行統(tǒng)計存儲在數(shù)據(jù)模型中的 brokers 節(jié)點下。
在 Kafka 的 Topic 信息注冊中也需要使用到 ZooKeeper ,在 Kafka 中同一個Topic 消息容器可以分成多個不同片,而這些分區(qū)既可以存在于一臺 Broker 服務器中,也可以存在于不同的 Broker 服務器中。
而在 Kafka 集群中,每臺 Broker 服務器又相對獨立。
為了能夠讀取這些以分布式方式存儲的分區(qū)信息,Kafka 會將這些分區(qū)信息在 Broker 服務器中的對應關系存儲在 ZooKeeper 數(shù)據(jù)模型的 topic 節(jié)點上,每一個 topic 在 ZooKeeper 數(shù)據(jù)節(jié)點上都會以 /brokers/topics/[topic] 的形式存在。
參考資料《從Paxos到Zookeeper 分布式一致性原理與實踐》
