ZooKeeper 學習筆記

ZooKeeper 介紹

ZooKeeper(wikihomegithub) 是用於分佈式應用的開源的分佈式協調服務。經過暴露簡單的原語,分佈式應用能在之上構建更高層的服務,如同步、配置管理和組成員管理等。在設計上易於編程開發,而且數據模型使用了熟知的文件系統目錄樹結構 [doc ]。html

共識與 Paxos

在介紹 ZooKeeper 以前,有必要了解下 Paxos 和 Chubby。2006 年 Google 在 OSDI 發表關於 BigtableChubby 的兩篇會議論文,以後再在 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

Paxos 協議和 Paxos 系統

Google 的 Chubby 沒有開源,在雲計算和大數據技術的風口下,Yahoo 開源的 ZooKeeper 便在工業界流行起來。ZooKeeper 重要的時間線以下:node

  • 2007 年 11 月 ZooKeeper 1.0 在 SourceForge 上發佈 [ref ]
  • 2008 年 6 月開始從 SourceForge 遷移到 Apache [ref ],在 10 月 Zookeeper 3.0 發佈,併成爲 Hadoop 的子項目 [ref1 ref2 ]

關於 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

ZooKeeper 架構圖

讀和寫操做,以下圖所示 [Haloi2015 ]:算法

ZooKeeper 讀和寫操做

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 數據模型

ZooKeeper 使用

安裝與配置

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

  • X:表示的服務器編號;
  • YYY:表示服務器的ip地址;
  • A:表示服務器節點間的通訊端口,用於 follower 和 leader 節點的通訊;
  • B:表示選舉端口,表示選舉新 leader 時服務器間相互通訊的端口,當 leader 掛掉時,其他服務器會相互通訊,選擇出新的 leader。

若想在單臺主機上試驗集羣模式,能夠將 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]

客戶端 API

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)

監視點(watch)

ZooKeeper 提供了處理變化的重要機制一一監視點(watch)。經過監視點,客戶端能夠對指定的 znode 節點註冊一個通知請求,在發生變化時就會收到一個單次的通知。當應用程序註冊了一個監視點來接收通知,匹配該監視點條件的第一個事件會觸發監視點的通知,而且最多隻觸發一次。例如,當 znode 節點也被刪除,客戶端須要知道該變化,客戶端在 /z 節點執行 exists 操做並設置監視點標誌位,等待通知,客戶端會以回調函數的形式收到通知。

ZooKeeper 的 API 中的讀操做:getData、getChildren 和 exists,都可以選擇在讀取的 znode 節點上設置監視點。使用監視點機制,咱們須要實現 Watcher 接口類,該接口惟一方法爲 process:

void process(WatchedEvent event)

WatchedEvent 數據結構包括如下信息:

  • ZooKeeper會話狀態(KeeperState):Disconnected、SyncConnected、AuthFailed、ConnectedReadOnly 、SaslAuthenticated、Expired。
  • 事件類型(EventType):NodeCreated 、NodeDeleted 、NodeDataChanged、NodeChildrenChanged 和 None 。
  • 若事件類型不是 None,還包括 znode 路徑。

若收到 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 示例代碼

在 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

ZooInspector 是 ZooKeeper 3.3.0 開始官方提供的可視化查看和編輯 ZooKeeper 實例的工具 [ZOOKEEPER-678 ]。源碼位於目錄 src/contrib/zooinspector 下,GitHub 地址爲:link。能夠根據 README.txt 的說明運行使用。或者能夠直接用 ZOOKEEPER-678 下提供的可執行 jar 包。

ZooInspector

參考資料

  1. 官方文檔:ZooKeeper http://zookeeper.apache.org/d...
  2. 2010-11 許令波:分佈式服務框架 Zookeeper https://www.ibm.com/developer...
  3. ZooKeeper:分佈式過程協同技術詳解,Benjamin Reed & Flavio Junqueira,2013,豆瓣
  4. Apache ZooKeeper Essentials, Haloi 2015,豆瓣
  5. 從Paxos到Zookeeper,阿里倪超 2015,豆瓣
  6. 大數據日知錄:架構與算法,張俊林 2014,第5章 分佈式協調系統,豆瓣
  7. 2010,Patrick Hunt, Mahadev Konar, Flavio Paiva Junqueira, Benjamin Reed: ZooKeeper: Wait-free Coordination for Internet-scale Systems. USENIX ATC 2010,dblpmsausenix
相關文章
相關標籤/搜索