详解分布式协调服务 ZooKeeper
這篇文章主要會(huì)介紹 Zookeeper 的實(shí)現(xiàn)原理以及常見的應(yīng)用
在 2006 年,Google 發(fā)表了一篇名為 The Chubby lock service for loosely-coupled distributed systems 的論文,其中描述了一個(gè)分布式鎖服務(wù) Chubby 的設(shè)計(jì)理念和實(shí)現(xiàn)原理;作為 Google 內(nèi)部的一個(gè)基礎(chǔ)服務(wù),雖然 Chubby 與 GFS、Bigtable 和 MapReduce 相比并沒有那么大的名氣,不過它在 Google 內(nèi)部也是非常重要的基礎(chǔ)設(shè)施。
相比于名不見經(jīng)傳的 Chubby,作者相信 Zookeeper 更被廣大開發(fā)者所熟知,作為非常出名的分布式協(xié)調(diào)服務(wù),Zookeeper 有非常多的應(yīng)用,包括發(fā)布訂閱、命名服務(wù)、分?jǐn)?shù)是協(xié)調(diào)和分布式鎖,這篇文章主要會(huì)介紹 Zookeeper 的實(shí)現(xiàn)原理以及常見的應(yīng)用,但是在具體介紹 Zookeeper 的功能和原理之前,我們會(huì)簡(jiǎn)單介紹一下分布式鎖服務(wù) Chubby 以及它與 Zookeeper 之間的異同。
Chubby
作為分布式鎖服務(wù),Chubby 的目的就是允許多個(gè)客戶端對(duì)它們的行為進(jìn)行同步,同時(shí)也能夠解決客戶端的環(huán)境相關(guān)信息的分發(fā)和粗粒度的同步問題,GFS 和 Bigtable 都使用了 Chubby 以解決主節(jié)點(diǎn)的選舉等問題。在網(wǎng)絡(luò)上你很難找到關(guān)于 Chubby 的相關(guān)資料,我們只能從 The Chubby lock service for loosely-coupled distributed systems 一文中窺見它的一些設(shè)計(jì)思路、技術(shù)架構(gòu)等信息。
雖然 Chubby 和 Zookeeper 有著比較相似的功能,但是它們的設(shè)計(jì)理念卻非常不同,Chubby 在論文的摘要中寫道:
We describe our experiences with the Chubby lock service, which is intended to provide coarse-grained locking as well as reliable (though low-volume) storage for a loosely-coupled distributed system.
從論文的摘要中我們可以看出 Chubby 首先被定義成一個(gè) 分布式的鎖服務(wù),它能夠?yàn)榉植际较到y(tǒng)提供 松耦合、粗粒度 的分布式鎖功能,然而我們并不能依賴于它來做一些重量的數(shù)據(jù)存儲(chǔ)。
Chubby 在設(shè)計(jì)時(shí)做了兩個(gè)重要的設(shè)計(jì)決定,一是提供完整、獨(dú)立的分布式鎖服務(wù)而非一個(gè)用于共識(shí)的庫或者服務(wù),另一個(gè)是選擇提供小文件的的讀寫功能,使得主節(jié)點(diǎn)能夠方便地發(fā)布自己的狀態(tài)信息。
系統(tǒng)架構(gòu)
Chubby 總共由兩部分組成,一部分是用于提供數(shù)據(jù)的讀寫接口并管理相關(guān)的配置數(shù)據(jù)的服務(wù)端,另一部分就是客戶端使用的 SDK,為了提高系統(tǒng)的穩(wěn)定性,每一個(gè) Chubby 單元都由一組服務(wù)器組成,它會(huì)使用 共識(shí)算法 從集群中選舉出主節(jié)點(diǎn)。
在一個(gè) Chubby Cell 中,只有 主節(jié)點(diǎn)會(huì)對(duì)外提供讀寫服務(wù),其他的節(jié)點(diǎn)其實(shí)都是當(dāng)前節(jié)點(diǎn)的副本(Replica),它們只是維護(hù)一個(gè)數(shù)據(jù)的拷貝并會(huì)在主節(jié)點(diǎn)更新時(shí)對(duì)它們持有的數(shù)據(jù)庫進(jìn)行更新;客戶端通過向副本發(fā)送請(qǐng)求獲取主節(jié)點(diǎn)的位置,一旦它獲取到了主節(jié)點(diǎn)的位置,就會(huì)向所有的讀寫請(qǐng)求發(fā)送給主節(jié)點(diǎn),直到其不再響應(yīng)為止。寫請(qǐng)求都會(huì)通過一致性協(xié)議傳播到所有的副本中,當(dāng)集群中的多數(shù)節(jié)點(diǎn)都同步了請(qǐng)求時(shí)就會(huì)認(rèn)為當(dāng)前的寫入已經(jīng)被確認(rèn)。
當(dāng)主節(jié)點(diǎn)宕機(jī)時(shí),副本會(huì)在其租約到期時(shí)重新進(jìn)行選舉,副本節(jié)點(diǎn)如果在宕機(jī)幾小時(shí)還沒有回復(fù),那么系統(tǒng)就會(huì)從資源池中選擇一個(gè)新的節(jié)點(diǎn)并在該節(jié)點(diǎn)上啟動(dòng) Chubby 服務(wù)并更新 DNS 表。
主節(jié)點(diǎn)會(huì)不停地輪訓(xùn) DNS 表獲取集群中最新的配置,每次 DNS 表更新時(shí),主節(jié)點(diǎn)都會(huì)將新的配置下發(fā)給 Chubby 集群中其他的副本節(jié)點(diǎn)。
Zookeeper
很多人都會(huì)說 Zookeeper 是 Chubby 的一個(gè)開源實(shí)現(xiàn),這其實(shí)是有問題的,它們兩者只不過都提供了具有層級(jí)結(jié)構(gòu)的命名空間:
Chubby 和 Zookeeper 從最根本的設(shè)計(jì)理念上就有著非常明顯的不同,在上文中我們已經(jīng)提到了 Chubby 被設(shè)計(jì)成一個(gè)分布式的鎖服務(wù),它能夠?yàn)榉植际较到y(tǒng)提供松耦合、粗粒度的分布式鎖功能,然而我們并不能依賴于它來做一些重量的數(shù)據(jù)存儲(chǔ),而 Zookeeper 的論文在摘要中介紹到,它是一個(gè)能夠?yàn)榉植际较到y(tǒng)提供協(xié)調(diào)功能的服務(wù):
In this paper, we describe ZooKeeper, a service for co- ordinating processes of distributed applications.
Zookeeper 的目的是為客戶端構(gòu)建復(fù)雜的協(xié)調(diào)功能提供簡(jiǎn)單、高效的核心 API,相比于 Chubby 對(duì)外提供已經(jīng)封裝好的更上層的功能,Zookeeper 提供了更抽象的接口以便于客戶端自行實(shí)現(xiàn)想要完成的功能。
Chubby 直接為用戶提供封裝好的鎖和解鎖的功能,內(nèi)部完成了鎖的實(shí)現(xiàn),只是將 API 直接暴露給用戶,而 Zookeeper 卻需要用戶自己實(shí)現(xiàn)分布式鎖;總的來說,使用 Zookeeper 往往需要客戶端做更多的事情,但是也享有更多的自由。
技術(shù)架構(gòu)
與 Chubby 集群中,多個(gè)節(jié)點(diǎn)只有一個(gè)能夠?qū)ν馓峁┓?wù)不同,Zookeeper 集群中所有的節(jié)點(diǎn)都可以對(duì)外提供服務(wù),但是集群中的節(jié)點(diǎn)也分為主從兩種節(jié)點(diǎn),所有的節(jié)點(diǎn)都能處理來自客戶端的讀請(qǐng)求,但是只有主節(jié)點(diǎn)才能處理寫入操作:
這里所說的 Zookeeper 集群主從節(jié)點(diǎn)實(shí)際上分別是 Leader 和 Follower 節(jié)點(diǎn)。
客戶端使用 Zookeeper 時(shí)會(huì)連接到集群中的任意節(jié)點(diǎn),所有的節(jié)點(diǎn)都能夠直接對(duì)外提供讀操作,但是寫操作都會(huì)被從節(jié)點(diǎn)路由到主節(jié)點(diǎn),由主節(jié)點(diǎn)進(jìn)行處理。
Zookeeper 在設(shè)計(jì)上提供了以下的兩個(gè)基本的順序保證,線性寫和先進(jìn)先出的客戶端順序:
其中線性寫是指所有更新 Zookeeper 狀態(tài)的請(qǐng)求都應(yīng)該按照既定的順序串行執(zhí)行;而先進(jìn)先出的客戶端順序是指,所有客戶端發(fā)出的請(qǐng)求會(huì)按照發(fā)出的順序執(zhí)行。
Zab 協(xié)議
在我們簡(jiǎn)單介紹 Zookeeper 的技術(shù)架構(gòu)之后,這一節(jié)將談及 Zookeeper 中的 Zab 協(xié)議,Zookeeper 的 Zab 協(xié)議是為了解決分布式一致性而設(shè)計(jì)出的一種協(xié)議,它的全稱是 Zookeeper 原子廣播協(xié)議,它能夠在發(fā)生崩潰時(shí)快速恢復(fù)服務(wù),達(dá)到高可用性。
如上一節(jié)提到的,客戶端在使用 Zookeeper 服務(wù)時(shí)會(huì)隨機(jī)連接到集群中的一個(gè)節(jié)點(diǎn),所有的讀請(qǐng)求都會(huì)由當(dāng)前節(jié)點(diǎn)處理,而寫請(qǐng)求會(huì)被路由給主節(jié)點(diǎn)并由主節(jié)點(diǎn)向其他節(jié)點(diǎn)廣播事務(wù),與 2PC 非常相似,如果在所有的節(jié)點(diǎn)中超過一半都返回成功,那么當(dāng)前寫請(qǐng)求就會(huì)被提交。
當(dāng)主節(jié)點(diǎn)崩潰時(shí),其他的 Replica 節(jié)點(diǎn)會(huì)進(jìn)入崩潰恢復(fù)模式并重新進(jìn)行選舉,Zab 協(xié)議必須確保提交已經(jīng)被 Leader 提交的事務(wù)提案,同時(shí)舍棄被跳過的提案,這也就是說當(dāng)前集群中最新 ZXID 最大的服務(wù)器會(huì)被選舉成為 Leader 節(jié)點(diǎn);但是在正式對(duì)外提供服務(wù)之前,新的 Leader 也需要先與 Follower 中的數(shù)據(jù)進(jìn)行同步,確保所有節(jié)點(diǎn)擁有完全相同的提案列表。
在上面提到 ZXID 其實(shí)就是 Zab 協(xié)議中設(shè)計(jì)的事務(wù)編號(hào),它是一個(gè) 64 位的整數(shù),其中最低的 32 位是一個(gè)計(jì)數(shù)器,每當(dāng)客戶端修改 Zookeeper 集群狀態(tài)時(shí),Leader 都會(huì)以當(dāng)前 ZXID 值作為提案的編號(hào)創(chuàng)建一個(gè)新的事務(wù),在這之后會(huì)將當(dāng)前計(jì)數(shù)器加一;ZXID 中高的 32 位表示當(dāng)前 Leader 的任期,每當(dāng)發(fā)生崩潰進(jìn)入恢復(fù)模式,集群的 Leader 重新選舉之后都會(huì)將 epoch 加一。
Zab 和 Paxos
Zab 和 Paxos 協(xié)議在實(shí)現(xiàn)上其實(shí)有非常多的相似點(diǎn),例如:
-
主節(jié)點(diǎn)會(huì)向所有的從節(jié)點(diǎn)發(fā)出提案;
-
主節(jié)點(diǎn)在接收到一組從節(jié)點(diǎn)中 50% 以上節(jié)點(diǎn)的確認(rèn)后,才會(huì)認(rèn)為當(dāng)前提案被提交了;
-
Zab 協(xié)議中的每一個(gè)提案都包含一個(gè) epoch 值,與 Paxos 中的 Ballot 非常相似;
因?yàn)樗鼈冇幸恍┫嗤奶攸c(diǎn),所以有的觀點(diǎn)會(huì)認(rèn)為 Zab 是 Paxos 的一個(gè)簡(jiǎn)化版本,但是 Zab 和 Paxos 在設(shè)計(jì)理念上就有著比較大的不同,兩者的主要區(qū)別就在于 Zab 主要是為構(gòu)建高可用的主備系統(tǒng)設(shè)計(jì)的,而 Paxos 能夠幫助工程師搭建具有一致性的狀態(tài)機(jī)系統(tǒng)。
作為一個(gè)一致性狀態(tài)機(jī)系統(tǒng),它能夠保證集群中任意一個(gè)狀態(tài)機(jī)副本都按照客戶端的請(qǐng)求執(zhí)行了相同順序的請(qǐng)求,即使來自客戶端請(qǐng)求是異步的并且不同客戶端的接收同一個(gè)請(qǐng)求的順序不同,集群中的這些副本就是會(huì)使用 Paxos 或者它的變種對(duì)提案達(dá)成一致;在集群運(yùn)行的過程中,如果主節(jié)點(diǎn)出現(xiàn)了錯(cuò)誤導(dǎo)致宕機(jī),其他的節(jié)點(diǎn)會(huì)重新開始進(jìn)行選舉并處理未提交的請(qǐng)求。
但是在類似 Zookeeper 的高可用主備系統(tǒng)中,所有的副本都需要對(duì)增量的狀態(tài)更新順序達(dá)成一致,這些狀態(tài)更新的變量都是由主節(jié)點(diǎn)創(chuàng)建并發(fā)送給其他的從節(jié)點(diǎn)的,每一個(gè)從節(jié)點(diǎn)都會(huì)嚴(yán)格按照順序逐一的執(zhí)行主節(jié)點(diǎn)生成的狀態(tài)更新請(qǐng)求,如果 Zookeeper 集群中的主節(jié)點(diǎn)發(fā)生了宕機(jī),新的主節(jié)點(diǎn)也必須嚴(yán)格按照順序?qū)φ?qǐng)求進(jìn)行恢復(fù)。
總的來說,使用狀態(tài)更新節(jié)點(diǎn)數(shù)據(jù)的主備系統(tǒng)相比根據(jù)客戶端請(qǐng)求改變狀態(tài)的狀態(tài)機(jī)系統(tǒng)對(duì)于請(qǐng)求的執(zhí)行順序有著更嚴(yán)格的要求。
實(shí)現(xiàn)原理
這一節(jié)會(huì)簡(jiǎn)單介紹 Zookeeper 的一些實(shí)現(xiàn)原理,重點(diǎn)會(huì)介紹以下幾個(gè)部分的內(nèi)容:文件系統(tǒng)、臨時(shí) / 持久節(jié)點(diǎn)和通知的實(shí)現(xiàn)原理。
文件系統(tǒng)
了解或者使用 Zookeeper 或者其他分布式協(xié)調(diào)服務(wù)的讀者對(duì)于使用類似文件系統(tǒng)的方式比較熟悉,與 Unix 中的文件系統(tǒng)份上相似的是,Zookeeper 中也使用文件系統(tǒng)組織系統(tǒng)中存儲(chǔ)的資源。
Zookeeper 中其實(shí)并沒有文件和文件夾的概念,它只有一個(gè) Znode 的概念,它既能作為容器存儲(chǔ)數(shù)據(jù),也可以持有其他的 Znode 形成父子關(guān)系。
Znode 其實(shí)有 PERSISTENT、PERSISTENT_SEQUENTIAL、EPHEMERAL 和 EPHEMERAL_SEQUENTIAL 四種類型,它們是臨時(shí)與持久、順序與非順序兩個(gè)不同的方向組合成的四種類型。
臨時(shí)節(jié)點(diǎn)是客戶端在連接 Zookeeper 時(shí)才會(huì)保持存在的節(jié)點(diǎn),一旦客戶端和服務(wù)端之間的連接中斷,當(dāng)前連接持有的所有節(jié)點(diǎn)都會(huì)被刪除,而持久的節(jié)點(diǎn)不會(huì)隨著會(huì)話連接的中斷而刪除,它們需要被客戶端主動(dòng)刪除;Zookeeper 中另一種節(jié)點(diǎn)的特性就是順序和非順序,如果我們使用 Zookeeper 創(chuàng)建了順序的節(jié)點(diǎn),那么所有節(jié)點(diǎn)就會(huì)在名字的末尾附加一個(gè)序列號(hào),序列號(hào)是一個(gè)由父節(jié)點(diǎn)維護(hù)的單調(diào)遞增計(jì)數(shù)器。
通知
常見的通知機(jī)制往往都有兩種,一種是客戶端使用『拉』的方式從服務(wù)端獲取最新的狀態(tài),這種方式獲取的狀態(tài)很有可能都是過期的,需要客戶端不斷地通過輪訓(xùn)的方式獲取服務(wù)端最新的狀態(tài),另一種方式就是在客戶端訂閱對(duì)應(yīng)節(jié)點(diǎn)后由服務(wù)端向所有訂閱者推送該節(jié)點(diǎn)的變化,相比于客戶端主動(dòng)獲取數(shù)據(jù)的方式,服務(wù)端主動(dòng)推送更能夠保證客戶端數(shù)據(jù)的實(shí)時(shí)性。
作為分布式協(xié)調(diào)工具的 Zookeeper 就實(shí)現(xiàn)了這種服務(wù)端主動(dòng)推送請(qǐng)求的機(jī)制,也就是 Watch,當(dāng)客戶端使用 getData 等接口獲取 Znode 狀態(tài)時(shí)傳入了一個(gè)用于處理節(jié)點(diǎn)變更的回調(diào),那么服務(wù)端就會(huì)主動(dòng)向客戶端推送節(jié)點(diǎn)的變更:
public byte[] getData(final String path, Watcher watcher, Stat stat)從這個(gè)方法中傳入的 Watcher 對(duì)象實(shí)現(xiàn)了相應(yīng)的 process 方法,每次對(duì)應(yīng)節(jié)點(diǎn)出現(xiàn)了狀態(tài)的改變,WatchManager 都會(huì)通過以下的方式調(diào)用傳入 Watcher 的方法:
Set<Watcher> triggerWatch(String path, EventType type, Set<Watcher> supress) {WatchedEvent e = new WatchedEvent(type, KeeperState.SyncConnected, path);Set<Watcher> watchers;synchronized (this) {watchers = watchTable.remove(path);}for (Watcher w : watchers) {w.process(e);}return watchers; }Zookeeper 中的所有數(shù)據(jù)其實(shí)都是由一個(gè)名為 DataTree 的數(shù)據(jù)結(jié)構(gòu)管理的,所有的讀寫數(shù)據(jù)的請(qǐng)求最終都會(huì)改變這顆樹的內(nèi)容,在發(fā)出讀請(qǐng)求時(shí)可能會(huì)傳入 Watcher 注冊(cè)一個(gè)回調(diào)函數(shù),而寫請(qǐng)求就可能會(huì)觸發(fā)相應(yīng)的回調(diào),由 WatchManager 通知客戶端數(shù)據(jù)的變化。
通知機(jī)制的實(shí)現(xiàn)其實(shí)還是比較簡(jiǎn)單的,通過讀請(qǐng)求設(shè)置 Watcher 監(jiān)聽事件,寫請(qǐng)求在觸發(fā)事件時(shí)就能將通知發(fā)送給指定的客戶端。
會(huì)話
在 Zookeeper 中一個(gè)非常重要的概念就是會(huì)話,客戶端與服務(wù)器之間的任何操作都與 Zookeeper 中會(huì)話的概念有關(guān),比如我們?cè)偕弦还?jié)中提到的臨時(shí)節(jié)點(diǎn)生命周期以及通知的機(jī)制等等,它們都是基于會(huì)話來實(shí)現(xiàn)的。
每當(dāng)客戶端與服務(wù)端建立連接時(shí),其實(shí)創(chuàng)建了一個(gè)新的會(huì)話,在每一個(gè)會(huì)話的生命周期中,Zookeeper 會(huì)在不同的會(huì)話狀態(tài)之間進(jìn)行切換,比如說:CONNECTING、CONNECTED、RECONNECTING、RECONNECTED 和 CLOSE 等。
作為 Zookeeper 中最重要的概念之一,每一個(gè) Session 都包含四個(gè)基本屬性,會(huì)話的唯一 ID、會(huì)話超時(shí)時(shí)間、下次會(huì)話的超時(shí)時(shí)間點(diǎn)和表示會(huì)話是否被關(guān)閉的標(biāo)記。
SessionTracker 是 Zookeeper 中的會(huì)話管理器,它負(fù)責(zé)所有會(huì)話的創(chuàng)建、管理以及清理工作,但是它本身只是一個(gè) Java 的接口,定義了一系列用于管理會(huì)話的相關(guān)接口:
public interface SessionTracker {public static interface Session {long getSessionId();int getTimeout();boolean isClosing();}public static interface SessionExpirer {void expire(Session session);long getServerId();}long createSession(int sessionTimeout);boolean trackSession(long id, int to);boolean commitSession(long id, int to);boolean touchSession(long sessionId, int sessionTimeout);void setSessionClosing(long sessionId);void shutdown();void removeSession(long sessionId); }與其他的長(zhǎng)連接一樣,Zookeeper 中的會(huì)話也需要客戶端與服務(wù)端之間進(jìn)行心跳檢測(cè),客戶端會(huì)在超時(shí)時(shí)間內(nèi)向服務(wù)端發(fā)送心跳請(qǐng)求來保證會(huì)話不會(huì)被服務(wù)端關(guān)閉,一旦服務(wù)端檢測(cè)到某一個(gè)會(huì)話長(zhǎng)時(shí)間沒有收到心跳包就會(huì)中斷當(dāng)前會(huì)話釋放服務(wù)器上的資源。
? ?應(yīng)用? ?
作為分布式協(xié)調(diào)服務(wù),Zookeeper 能夠?yàn)榧禾峁┓植际揭恢滦缘谋WC,我們可以通過 Zookeeper 提供的最基本的 API 組合成更高級(jí)的功能:
public class Zookeeper {public String create(final String path, byte data[], List<ACL> acl, CreateMode createMode)public void delete(final String path, int version) throws InterruptedException, KeeperExceptionpublic Stat exists(final String path, Watcher watcher) throws KeeperException, InterruptedExceptionpublic byte[] getData(final String path, Watcher watcher, Stat stat) throws KeeperException, InterruptedExceptionpublic Stat setData(final String path, byte data[], int version) throws KeeperException, InterruptedExceptionpublic void sync(final String path, VoidCallback cb, Object ctx) }在這一節(jié)中,我們將介紹如何在生產(chǎn)環(huán)境中使用 Zookeeper 實(shí)現(xiàn)發(fā)布訂閱、命名服務(wù)、分布式協(xié)調(diào)以及分布式鎖等功能。
發(fā)布訂閱
通過 Zookeeper 進(jìn)行數(shù)據(jù)的發(fā)布與訂閱其實(shí)可以說是它提供的最基本功能,它能夠允許多個(gè)客戶端同時(shí)訂閱某一個(gè)節(jié)點(diǎn)的變更并在變更發(fā)生時(shí)執(zhí)行我們預(yù)先設(shè)置好的回調(diào)函數(shù),在運(yùn)行時(shí)改變服務(wù)的配置和行為:
ZooKeeper zk = new ZooKeeper("localhost", 3000, null); zk.getData("/config", new Watcher() {public void process(WatchedEvent watchedEvent) {System.out.println(watchedEvent.toString());} }, null); zk.setData("/config", "draven".getBytes(), 0);// WatchedEvent state:SyncConnected type:NodeDataChanged path:/config發(fā)布與訂閱是 Zookeeper 提供的一個(gè)最基本的功能,它的使用非常的簡(jiǎn)單,我們可以在 getData 中傳入實(shí)現(xiàn) process 方法的 Watcher 對(duì)象,在每次改變節(jié)點(diǎn)的狀態(tài)時(shí),process 方法都會(huì)被調(diào)用,在這個(gè)方法中就可以對(duì)變更進(jìn)行響應(yīng)動(dòng)態(tài)修改一些行為。
通過 Zookeeper 這個(gè)中樞,每一個(gè)客戶端對(duì)節(jié)點(diǎn)狀態(tài)的改變都能夠推送給節(jié)點(diǎn)的訂閱者,在發(fā)布訂閱模型中,Zookeeper 的每一個(gè)節(jié)點(diǎn)都可以被理解成一個(gè)主題,每一個(gè)客戶端都可以向這個(gè)主題推送詳細(xì),同時(shí)也可以訂閱這個(gè)主題中的消息;只是 Zookeeper 引入了文件系統(tǒng)的父子層級(jí)的概念將發(fā)布訂閱功能實(shí)現(xiàn)得更加復(fù)雜。
public static enum EventType {None(-1),NodeCreated(1),NodeDeleted(2),NodeDataChanged(3),NodeChildrenChanged(4); }如果我們訂閱了一個(gè)節(jié)點(diǎn)的變更信息,那么該節(jié)點(diǎn)的子節(jié)點(diǎn)出現(xiàn)數(shù)量變更時(shí)就會(huì)調(diào)用 process 方法通知觀察者,這也意味著更復(fù)雜的實(shí)現(xiàn),同時(shí)和專門做發(fā)布訂閱的中間件相比也沒有性能優(yōu)勢(shì),在海量推送的應(yīng)用場(chǎng)景下,消息隊(duì)列更能勝任,而 Zookeeper 更適合做一些類似服務(wù)配置的動(dòng)態(tài)下發(fā)的工作。
命名服務(wù)
除了實(shí)現(xiàn)服務(wù)配置數(shù)據(jù)的發(fā)布與訂閱功能,Zookeeper 還能幫助分布式系統(tǒng)實(shí)現(xiàn)命名服務(wù),在每一個(gè)分布式系統(tǒng)中,客戶端應(yīng)用都有根據(jù)指定名字獲取資源、服務(wù)器地址的需求,在這時(shí)就要求整個(gè)集群中的全部服務(wù)有著唯一的名字。
在大型分布式系統(tǒng)中,有兩件事情非常常見,一是不同服務(wù)之間的可能擁有相同的名字,另一個(gè)是同一個(gè)服務(wù)可能會(huì)在集群中部署很多的節(jié)點(diǎn),Zookeeper 就可以通過文件系統(tǒng)和順序節(jié)點(diǎn)解決這兩個(gè)問題。
在上圖中,我們創(chuàng)建了兩個(gè)命名空間,/infrastructure 和 /business 分別代表架構(gòu)和業(yè)務(wù)部門,兩個(gè)部門中都擁有名為 metrics 的服務(wù),而業(yè)務(wù)部門的 metrics 服務(wù)也部署了兩個(gè)節(jié)點(diǎn),在這里使用了命名空間和順序節(jié)點(diǎn)解決唯一標(biāo)志符的問題。
ZooKeeper zk = new ZooKeeper("localhost", 3000, null); zk.create("/metrics", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL); zk.create("/metrics", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL); List children = zk.getChildren("/", null); System.out.println(children);// [metrics0000000001, metrics0000000002]使用上面的代碼就能在 Zookeeper 中創(chuàng)建兩個(gè)帶序號(hào)的 metrics 節(jié)點(diǎn),分別是 metrics0000000001 和 metrics0000000002,也就是說 Zookeeper 幫助我們保證了節(jié)點(diǎn)的唯一性,讓我們能通過唯一的 ID 查找到對(duì)應(yīng)服務(wù)的地址等信息。
協(xié)調(diào)分布式事務(wù)
Zookeeper 的另一個(gè)作用就是擔(dān)任分布式事務(wù)中的協(xié)調(diào)者角色,在之前介紹 分布式事務(wù) 的文章中我們?cè)?jīng)介紹過分布式事務(wù)本質(zhì)上都是通過 2PC 來實(shí)現(xiàn)的,在兩階段提交中就需要一個(gè)協(xié)調(diào)者負(fù)責(zé)協(xié)調(diào)分布式事務(wù)的執(zhí)行。
ZooKeeper zk = new ZooKeeper("localhost", 3000, null); String path = zk.create("/transfer/tx", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);List ops = Arrays.asList(Op.create(path + "/cohort", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL),Op.create(path + "/cohort", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL),Op.create(path + "/cohort", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL) ); zk.multi(ops);當(dāng)前節(jié)點(diǎn)作為協(xié)調(diào)者在每次發(fā)起分布式事務(wù)時(shí)都會(huì)創(chuàng)建一個(gè) /transfer/tx 的持久順序節(jié)點(diǎn),然后為幾個(gè)事務(wù)的參與者創(chuàng)建幾個(gè)空白的節(jié)點(diǎn),事務(wù)的參與者在收到事務(wù)時(shí)會(huì)向這些空白的節(jié)點(diǎn)中寫入信息并監(jiān)聽這些節(jié)點(diǎn)中的內(nèi)容。
所有的事務(wù)參與者會(huì)向當(dāng)前節(jié)點(diǎn)中寫入提交或者終止,一旦當(dāng)前的節(jié)點(diǎn)改變了事務(wù)的狀態(tài),其他節(jié)點(diǎn)就會(huì)得到通知,如果出現(xiàn)一個(gè)寫入終止的節(jié)點(diǎn),所有的節(jié)點(diǎn)就會(huì)回滾對(duì)分布式事務(wù)進(jìn)行回滾。
使用 Zookeeper 實(shí)現(xiàn)強(qiáng)一致性的分布式事務(wù)其實(shí)還是一件比較困難的事情,一方面是因?yàn)閺?qiáng)一致性的分布式事務(wù)本身就有一定的復(fù)雜性,另一方面就是 Zookeeper 為了給客戶端提供更多的自由,對(duì)外暴露的都是比較基礎(chǔ)的 API,對(duì)它們進(jìn)行組裝實(shí)現(xiàn)復(fù)雜的分布式事務(wù)還是比較麻煩的,對(duì)于如何使用 Zookeeper 實(shí)現(xiàn)分布式事務(wù),我們可以在 ZooKeeper Recipes and Solutions 一文中找到更為詳細(xì)的內(nèi)容。
分布式鎖
在數(shù)據(jù)庫中,鎖的概念其實(shí)是非常重要的,常見的關(guān)系型數(shù)據(jù)庫就會(huì)對(duì)排他鎖和共享鎖進(jìn)行支持,而 Zookeeper 提供的 API 也可以讓我們非常簡(jiǎn)單的實(shí)現(xiàn)分布式鎖。
ZooKeeper zk = new ZooKeeper("localhost", 3000, null); final String resource = "/resource";final String lockNumber = zk.create("/resource/lock-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);List<String> locks = zk.getChildren(resource, false, null); Collections.sort(locks);if (locks.get(0).equals(lockNumber.replace("/resource/", ""))) {System.out.println("Acquire Lock");zk.delete(lockNumber, 0); } else {zk.getChildren(resource, new Watcher() {public void process(WatchedEvent watchedEvent) {try {ZooKeeper zk = new ZooKeeper("localhost", 3000, null);List locks = zk.getChildren(resource, null, null);Collections.sort(locks);if (locks.get(0).equals(lockNumber.replace("/resource/", ""))) {System.out.println("Acquire Lock");zk.delete(lockNumber, 0);}} catch (Exception e) {}}}, null); }如果多個(gè)服務(wù)同時(shí)要對(duì)某個(gè)資源進(jìn)行修改,就可以使用上述的代碼來實(shí)現(xiàn)分布式鎖,假設(shè)集群中存在一個(gè)資源 /resource,幾個(gè)服務(wù)需要通過分布式鎖保證資源只能同時(shí)被一個(gè)節(jié)點(diǎn)使用,我們可以用創(chuàng)建臨時(shí)順序節(jié)點(diǎn)的方式實(shí)現(xiàn)分布式鎖;當(dāng)我們創(chuàng)建臨時(shí)節(jié)點(diǎn)后,通過 getChildren 獲取當(dāng)前等待鎖的全部節(jié)點(diǎn),如果當(dāng)前節(jié)點(diǎn)是所有節(jié)點(diǎn)中序號(hào)最小的就得到了當(dāng)前資源的使用權(quán)限,在對(duì)資源進(jìn)行處理后,就可以通過刪除 /resource/lock-00000000x 來釋放鎖,如果當(dāng)前節(jié)點(diǎn)不是最小值,就會(huì)注冊(cè)一個(gè) Watcher 等待 /resource 子節(jié)點(diǎn)的變化直到當(dāng)前節(jié)點(diǎn)的序列號(hào)成為最小值。
上述代碼在集群中爭(zhēng)奪同一資源的服務(wù)器特別多的情況下會(huì)出現(xiàn)羊群效應(yīng),每次子節(jié)點(diǎn)改變時(shí)都會(huì)通知當(dāng)前節(jié)點(diǎn),造成資源的浪費(fèi),我們其實(shí)可以將 getChildren 換成 getData,讓當(dāng)前節(jié)點(diǎn)只監(jiān)聽前一個(gè)節(jié)點(diǎn)的刪除事件:
Integer number = Integer.parseInt(lockNumber.replace("/resource/lock-", "")) + 1; String previousLock = "/resource/lock-" + String.format("%010d", number);zk.getData(previousLock, new Watcher() {public void process(WatchedEvent watchedEvent) {try {if (watchedEvent.getType() == Event.EventType.NodeDeleted) {System.out.println("Acquire Lock");ZooKeeper zk = new ZooKeeper("localhost", 3000, null);zk.delete(lockNumber, 0);}} catch (Exception e) {}} }, null);在新的分布式鎖實(shí)現(xiàn)中,我們減少了每一個(gè)服務(wù)需要關(guān)注的事情,只讓它們監(jiān)聽需要關(guān)心的數(shù)據(jù)變更,減少 Zookeeper 發(fā)送不必要的通知影響效率。
分布式鎖作為分布式系統(tǒng)中比較重要的一個(gè)工具,確實(shí)有著比較多的應(yīng)用,同時(shí)也有非常多的實(shí)現(xiàn)方式,除了 Zookeeper 之外,其他服務(wù)例如 Redis 和 etcd 也能夠?qū)崿F(xiàn)分布式鎖,為分布式系統(tǒng)的構(gòu)建提供支持,不過在這篇文章中就不展開介紹了。
? ?總結(jié)? ?
我們?cè)谶@篇文章中簡(jiǎn)單介紹了 Google 的分布式鎖服務(wù) Chubby 以及同樣能夠提供分布式鎖服務(wù)功能的 Zookeeper。
作為分布式協(xié)調(diào)服務(wù),Zookeeper 的應(yīng)用場(chǎng)景非常廣泛,不僅能夠用于服務(wù)配置的下發(fā)、命名服務(wù)、協(xié)調(diào)分布式事務(wù)以及分布式鎖,還能夠用來實(shí)現(xiàn)微服務(wù)治理中的服務(wù)注冊(cè)以及發(fā)現(xiàn)等功能,這些其實(shí)都源于 Zookeeper 能夠提供高可用的分布式協(xié)調(diào)服務(wù),能夠?yàn)榭蛻舳颂峁┓植际揭恢滦缘闹С?#xff0c;在后面的文章中作者也會(huì)介紹其他用于分布式協(xié)調(diào)的服務(wù)。
參考資料:
https://zookeeper.apache.org/doc/r3.4.4/recipes.html
https://static.googleusercontent.com/media/research.google.com/en//archive/chubby-osdi06.pdf
總結(jié)
以上是生活随笔為你收集整理的详解分布式协调服务 ZooKeeper的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: AI面试必备!你不可不知的10个深度学习
- 下一篇: 推荐系统遇上深度学习(二十二):Deep