ZooKeeper(wiki,home,github) 是用於分佈式應用的開源的分佈式協調服務。經過暴露簡單的原語,分佈式應用能在之上構建更高層的服務,如同步、配置管理和組成員管理等。在設計上易於編程開發,而且數據模型使用了熟知的文件系統目錄樹結構 [doc ]。html
在介紹 ZooKeeper 以前,有必要了解下 Paxos 和 Chubby。2006 年 Google 在 OSDI 發表關於 Bigtable 和 Chubby 的兩篇會議論文,以後再在 2007 年 PODC 會議上發表了論文「Paxos Made Live」,介紹 Chubby 底層實現的共識(consensus)協議 Multi-Paxos,該協議對 Lamport 的原始 Paxos 算法作了改進,提升了運行效率 [ref ]。Chubby 做爲鎖服務被 Google 應用在 GFS 和 Bigtable 中。受 Chubby 的影響,來自 Yahoo 研究院的 Benjamin Reed 和 Flavio Junqueira 等人開發了被業界稱爲開源版的 Chubby 的 ZooKeeper(內部實現事實上稍有不一樣 [ref ]),底層的共識協議爲 ZAB。Lamport 的 Paxos 算法出了名的難懂,如何讓算法更加可理解(understandable),便成了 Stanford 博士生 Diego Ongaro 的研究課題。Diego Ongaro 在 2014 年發表了介紹 Raft 算法的論文,「In search of an understandable consensus algorithm」。Raft 是可理解版的 Paxos,很快就成爲解決共識問題的流行協議之一。這些類 Paxos 協議和 Paxos 系統之間的關係,以下 [Ailijiang2016 ]:java
Google 的 Chubby 沒有開源,在雲計算和大數據技術的風口下,Yahoo 開源的 ZooKeeper 便在工業界流行起來。ZooKeeper 重要的時間線以下:node
關於 ZooKeeper 名字的來源,Flavio Junqueira 和 Benjamin Reed 在介紹 ZooKeeper 的書中有以下闡述:git
ZooKeeper 由雅虎研究院開發。咱們小組在進行 ZooKeeper 的開發一段時間以後,開始推薦給其餘小組,所以咱們須要爲咱們的項目起一個名字。與此同時,小組也一同致力於 Hadoop 項目,參與了不少動物命名的項目,其中有廣爲人知的 Apache Pig 項目( http://pig.apache.org)。咱們在討論各類各樣的名字時,一位團隊成員提到咱們不能再使用動物的名字了,由於咱們的主管以爲這樣下去會以爲咱們生活在動物園中。你們對此產生了共鳴,分佈式系統就像一個動物園,混亂且難以管理,而 ZooKeeper 就是將這一切變得可控。
ZooKeeper 服務由若干臺服務器構成,其中的一臺經過 ZAB 原子廣播協議選舉做爲主控服務器(leader),其餘的做爲從屬服務器(follower)。客戶端能夠經過 TCP 協議鏈接任意一臺服務器。若是客戶端是讀操做請求,則任意一個服務器均可以直接響應請求;若是是更新數據操做(寫數據或者更新數據)。則只能由主控服務器來協調更新操做;若是客戶端鏈接的是從屬服務器,則從屬服務器會將更新據請求轉發到主控服務器,由其完成更新操做。主控服務器將全部更新操做序列化,利用 ZAB 協議將數據更新請求通知全部從屬服務器,ZAB 保證更新操做。github
讀和寫操做,以下圖所示 [Haloi2015 ]:算法
ZooKeeper 的任意一臺服務器均可以響應客戶端的讀操做,這樣能夠提升吞吐量。Chubby 在這點上與 ZooKeeper 不一樣,全部讀/寫操做都由主控服務器完成,從屬服務器只是爲了提升整個協調系統的可用性,即主控服務器發生故障後可以在從屬服務器中快速選舉出新的主控服務器。在帶來高吞吐量優點的同時,ZooKeeper 這樣作也帶來潛在的問題:客戶端可能會讀到過時數據,由於即便主控服務器已經更新了某個內存數據,可是 ZAB 協議還未能將其廣播到從屬服務器。爲了解決這一問題,在 ZooKeeper 的接口 API 函數中提供了 sync 操做,應用能夠根據須要在讀數據前調用該操做,其含義是:接收到 sync 命令的從屬服務器從主控服務器同步狀態信息,保證二者徹底一致。這樣若是在讀操做前調用 sync 操做,則能夠保證客戶端必定能夠讀取到最新狀態的數據。apache
ZooKeeper 所提供的命名空間跟標準文件系統很類似。路徑中一系列元素是用斜槓(/)分隔的。每一個節點在 ZooKeper 命名空間中是用路徑來識別的。在 ZooKeeper 術語下,節點被稱爲 znode。默認每一個 znode 最大隻能存儲 1M 數據(能夠經過配置參數修改),這與 Chubby 同樣是出於避免應用將協調系統看成存儲系統來用。znode 只能使用絕對路徑,相對路徑不能被 ZooKeeper 識別。znode 命名能夠是任意 Unicode 字符。惟一的例外是,名稱"/zookeeper"。命名爲"/zookeeper"的 znode,由 ZooKeeper 系統自動生成,用配額(quota)管理。編程
ZooKeeper 安裝與啓動:數組
$ brew info zookeeper zookeeper: stable 3.4.10 (bottled), HEAD Centralized server for distributed coordination of services https://zookeeper.apache.org/ ... 省略 $ brew install zookeeper $ zkServer start # 啓動 $ zkServer stop # 終止 $ zkServer help ZooKeeper JMX enabled by default Using config: /usr/local/etc/zookeeper/zoo.cfg Usage: ./zkServer.sh {start|start-foreground|stop|restart|status|upgrade|print-cmd}
若不修改配置文件,默認是單機模式啓動。若要使用集羣模式,須要修改 /usr/local/etc/zookeeper/zoo.cfg
(默認路徑)。示例 zoo.cfg
[doc ]:服務器
tickTime=2000 dataDir=/var/lib/zookeeper clientPort=2181 initLimit=5 syncLimit=2 server.1=192.168.211.11:2888:3888 server.2=192.168.211.12:2888:3888 server.3=192.168.211.13:2888:3888
clientPort
:客戶端鏈接 Zookeeper 服務器的端口,Zookeeper 會監聽這個端口,接受客戶端的訪問請求。server.X=YYY:A:B
若想在單臺主機上試驗集羣模式,能夠將 YYY
都修改成 localhost
,而且讓兩個端口 A:B
也相互不一樣(好比:2888:3888, 2889:3889, 2890:3890),便可實現僞集羣模式。示例 zoo.cfg
以下 [doc ]:
server.1=localhost:2888:3888 server.2=localhost:2889:3889 server.3=localhost:2890:3890
zkCli 支持的所有命令:
$ zkCli help ZooKeeper -server host:port cmd args stat path [watch] set path data [version] ls path [watch] delquota [-n|-b] path ls2 path [watch] setAcl path acl setquota -n|-b val path history redo cmdno printwatches on|off delete path [version] sync path listquota path rmr path get path [watch] create [-s] [-e] path data acl addauth scheme auth quit getAcl path close connect host:port
Zookeeper 支持兩種類型節點:持久節點(persistent znode)和臨時節點(ephemeral znode)。持久節點不論客戶端會話狀況,一直存在,只有當客戶端顯式調用刪除操做纔會消失。而臨時節點則不一樣,會在客戶端會話結束或者發生故障的時候被 ZooKeeper 系統自動清除。另外,這兩種類型的節點均可以添加是不是順序(sequential)的特性,這樣就有了:持久順序節點和臨時順序節點。
(1) 持久節點(persistent znode)
使用 create
建立節點(默認持久節點),以及使用 get
查看該節點:
$ zkCli # 啓動客戶端 [zk: localhost:2181(CONNECTED) 1] create /zoo 'hello zookeeper' Created /zoo [zk: localhost:2181(CONNECTED) 2] get /zoo hello zookeeper cZxid = 0x8d ctime = Thu Nov 08 20:42:55 CST 2017 mZxid = 0x8d mtime = Thu Nov 08 20:42:55 CST 2017 pZxid = 0x8d cversion = 0 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 15 numChildren = 0
create
建立子節點,以及使用 ls
查看所有子節點:
[zk: localhost:2181(CONNECTED) 3] create /zoo/duck '' Created /zoo/duck [zk: localhost:2181(CONNECTED) 4] create /zoo/goat '' Created /zoo/goat [zk: localhost:2181(CONNECTED) 5] create /zoo/cow '' Created /zoo/cow [zk: localhost:2181(CONNECTED) 6] ls /zoo [cow, goat, duck]
delete
刪除節點,以及使用 rmr
遞歸刪除:
[zk: localhost:2181(CONNECTED) 7] delete /zoo/duck [zk: localhost:2181(CONNECTED) 8] ls /zoo [cow, goat] [zk: localhost:2181(CONNECTED) 9] delete /zoo Node not empty: /zoo [zk: localhost:2181(CONNECTED) 10] rmr /zoo [zk: localhost:2181(CONNECTED) 11] ls /zoo Node does not exist: /zoo
(2) 臨時節點(ephemeral znode)
和持久節點不一樣,臨時節點不能建立子節點:
$ zkCli # 啓動第1個客戶端 [zk: localhost:2181(CONNECTED) 0] create -e /node 'hello' Created /node [zk: localhost:2181(CONNECTED) 40] get /node hello cZxid = 0x97 ctime = Thu Nov 08 21:01:25 CST 2017 mZxid = 0x97 mtime = Thu Nov 08 21:01:25 CST 2017 pZxid = 0x97 cversion = 0 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x161092a0ff30000 dataLength = 5 numChildren = 0 [zk: localhost:2181(CONNECTED) 1] create /node/child '' Ephemerals cannot have children: /node/child
臨時節點在客戶端會話結束或者發生故障的時候被 ZooKeeper 系統自動清除。如今試驗下的針對臨時節點自動清除的監視:
$ zkCli # 啓動第2個客戶端 [zk: localhost:2181(CONNECTED) 0] create -e /node 'hello' Node already exists: /node [zk: localhost:2181(CONNECTED) 1] stat /node true cZxid = 0x97 ctime = Thu Nov 08 21:01:25 CST 2017 mZxid = 0x97 mtime = Thu Nov 08 21:01:25 CST 2017 pZxid = 0x97 cversion = 0 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x161092a0ff30000 dataLength = 5 numChildren = 0
若客戶端1,退出 quit
或崩潰,客戶端2將收到監視事件:
[zk: localhost:2181(CONNECTED) 2] WATCHER:: WatchedEvent state:SyncConnected type:NodeDeleted path:/node
(3) 順序節點(sequential znode)
順序節點在其建立時 ZooKeeper 會自動在 znode 名稱上附加上順序編號。順序編號,由父 znode 維護,而且單調遞增。順序編號,由 4 字節的有符號整數組成,並被格式化爲 0 填充的 10 位數字。
[zk: localhost:2181(CONNECTED) 1] create /test '' Created /test [zk: localhost:2181(CONNECTED) 2] create -s /test/seq '' Created /test/seq0000000000 [zk: localhost:2181(CONNECTED) 3] create -s /test/seq '' Created /test/seq0000000001 [zk: localhost:2181(CONNECTED) 4] create -s /test/seq '' Created /test/seq0000000002 [zk: localhost:2181(CONNECTED) 5] ls /test [seq0000000000, seq0000000001, seq0000000002]
ZooKeeper 提供的主要 znode 操做 API 以下表所示:
API 操做 | 描述 | CLI 命令 |
---|---|---|
create | 建立 znode | create |
delete | 刪除 znode | delete/rmr/delquota |
exists | 檢查 znode 是否存在 | stat |
getChildren | 讀取 znode 所有的子節點 | ls/ls2 |
getData | 讀取 znode 數據 | get/listquota |
setData | 設置 znode 數據 | set/setquota |
getACL | 讀取 znode 的 ACL | getACL |
setACL | 設置 znode 的 ACL | setACL |
sync | 同步 | sync |
Java 的 ZooKeeper 類實現了上述提供的 API。
Zookeeper 底層是 Java 實現,zkCli
命令行工具底層也是 Java 實現,對應的 Java 實現類爲 org.apache.zookeeper.ZooKeeperMain
[src1 src2 ]。ZooKeeper 3.5.x 下,CLI 命令與底層實現 API 對應關係:
命名 CLI | Java API (ZooKeeper 類) |
---|---|
addauth scheme auth | public void addAuthInfo(String scheme, byte[] auth) |
close | public void close() |
create [-s] [-e] path data acl | public String create(final String path, byte data[], List<ACL> acl, CreateMode createMode) |
delete path [version] | public void delete(String path, int version) |
delquota [-n|-b] path | public void delete(String path, int version) |
get path [watch] | public byte[] getData(String path, boolean watch, Stat stat) |
getAcl path | public List<ACL> getACL(final String path, Stat stat) |
listquota path | public byte[] getData(String path, boolean watch, Stat stat) |
ls path [watch] | public List<String> getChildren(String path, Watcher watcher, Stat stat) |
ls2 path [watch] | - |
quit | public void close() |
rmr path | public void delete(final String path, int version) |
set path data [version] | public Stat setData(String path, byte[] data, int version) |
setAcl path acl | public Stat setACL(final String path, List<ACL> acl, int aclVersion) |
setquota -n|-b val path | public Stat setData(String path, byte[] data, int version) |
stat path [watch] | public Stat exists(String path, boolean watch) |
sync path | public void sync(String path, AsyncCallback.VoidCallback cb, Object ctx) |
ZooKeeper 提供了處理變化的重要機制一一監視點(watch)。經過監視點,客戶端能夠對指定的 znode 節點註冊一個通知請求,在發生變化時就會收到一個單次的通知。當應用程序註冊了一個監視點來接收通知,匹配該監視點條件的第一個事件會觸發監視點的通知,而且最多隻觸發一次。例如,當 znode 節點也被刪除,客戶端須要知道該變化,客戶端在 /z 節點執行 exists 操做並設置監視點標誌位,等待通知,客戶端會以回調函數的形式收到通知。
ZooKeeper 的 API 中的讀操做:getData、getChildren 和 exists,都可以選擇在讀取的 znode 節點上設置監視點。使用監視點機制,咱們須要實現 Watcher 接口類,該接口惟一方法爲 process:
void process(WatchedEvent event)
WatchedEvent 數據結構包括如下信息:
若收到 WatchedEvent, 在 zkCli 中會輸出相似以下結果:
WatchedEvent state:SyncConnected type:NodeDeleted path:/node
監視點有兩種類型:數據監視點和子節點監視點。建立、刪除或設置一個 znode 節點的數據都會觸發數據監視點,exists 和 getData 這兩個操做能夠設置數據監視點。只有 getChildren 操做能夠設置子節點監視點,這種監視點只有在 znode 子節點建立或刪除時才被觸發。對於每種事件類型,咱們經過如下調用設置監視點:
NodeCreated
經過 exists 調用設置一個監視點。
NodeDeleted
經過 exists 或 getData 調用設置監視點。
NodeDataChanged
經過 exists 或getData 調用設置監視點。
NodeChildrenChanged
經過 getChildren 調用設置監視點。
在 Java 下使用 ZooKeeper 須要先添加以下 maven 依賴:
<dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.11</version> <type>pom</type> </dependency>
ZookeeperDemo
示例,展現了創建鏈接會話,以及對 znode 的建立、讀取、修改、刪除和設置監視點等操做:
import java.io.IOException; import org.apache.commons.lang3.time.DateFormatUtils; import org.apache.zookeeper.*; import org.apache.zookeeper.data.Stat; public class ZookeeperDemo { public static void main(String[] args) throws KeeperException, InterruptedException, IOException { // 建立服務器鏈接 ZooKeeper zk = new ZooKeeper("127.0.0.1:2181", 100, new Watcher() { // 監控全部被觸發的事件 public void process(WatchedEvent event) { System.out.printf("WatchedEvent state:%s type:%s path:%s\n", event.getState(), event.getType(), event.getPath()); } }); // 建立節點 zk.create("/zoo", "hello ZooKeeper".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); // 讀取節點數據 Stat stat = new Stat(); System.out.println(new String(zk.getData("/zoo", false, stat))); printStat(stat); // 建立子節點 zk.create("/zoo/duck", "hello duck".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); zk.create("/zoo/goat", "hello goat".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); zk.create("/zoo/cow", "hello cow".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); // 讀取子節點列表,並設置監視點 System.out.println(zk.getChildren("/zoo", true)); // 讀取子節點數據,並設置監視點 System.out.println(new String(zk.getData("/zoo/duck", true, null))); // 修改子節點數據 zk.setData("/zoo/duck", "hi duck".getBytes(), -1); // 讀取修改後的子節點數據 System.out.println(new String(zk.getData("/zoo/duck", true, null))); // 刪除子節點 zk.delete("/zoo/duck", -1); zk.delete("/zoo/goat", -1); zk.delete("/zoo/cow", -1); // 刪除父節點 zk.delete("/zoo", -1); // 關閉鏈接 zk.close(); } private static void printStat(Stat stat) { System.out.println("cZxid = 0x" + Long.toHexString(stat.getCzxid())); System.out.println("ctime = " + DateFormatUtils.format(stat.getCtime(), "yyyy-MM-dd HH:mm:ss")); System.out.println("mZxid = 0x" + Long.toHexString(stat.getMzxid())); System.out.println("mtime = " + DateFormatUtils.format(stat.getMtime(), "yyyy-MM-dd HH:mm:ss")); System.out.println("pZxid = 0x" + Long.toHexString(stat.getPzxid())); System.out.println("cversion = " + stat.getCversion()); System.out.println("dataVersion = " + stat.getVersion()); System.out.println("aclVersion = " + stat.getAversion()); System.out.println("ephemeralOwner = 0x" + Long.toHexString(stat.getEphemeralOwner())); System.out.println("dataLength = " + stat.getDataLength()); System.out.println("numChildren = " + stat.getNumChildren()); } }
輸出結果:
WatchedEvent state:SyncConnected type:None path:null hello ZooKeeper cZxid = 0x1e1 ctime = 2017-11-20 12:18:36 mZxid = 0x1e1 mtime = 2017-11-20 12:18:36 pZxid = 0x1e1 cversion = 0 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 15 numChildren = 0 [cow, goat, duck] hello duck WatchedEvent state:SyncConnected type:NodeDataChanged path:/zoo/duck hi duck WatchedEvent state:SyncConnected type:NodeDeleted path:/zoo/duck WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/zoo
ZooInspector 是 ZooKeeper 3.3.0 開始官方提供的可視化查看和編輯 ZooKeeper 實例的工具 [ZOOKEEPER-678 ]。源碼位於目錄 src/contrib/zooinspector
下,GitHub 地址爲:link。能夠根據 README.txt
的說明運行使用。或者能夠直接用 ZOOKEEPER-678 下提供的可執行 jar 包。