ZooKeeper 入門看這篇就夠了

  • 2019 年 12 月 23 日
  • 筆記

什麼是 ZooKeeper?

ZooKeeper 是一個分佈式的,開放源碼的分佈式應用程序協同服務。ZooKeeper 的設計目標是將那些複雜且容易出錯的分佈式一致性服務封裝起來,構成一個高效可靠的原語集,並以一系列簡單易用的接口提供給用戶使用。

ZooKeeper 發展歷史

ZooKeeper 最早起源於雅虎研究院的一個研究小組。在當時,研究人員發現,在雅虎內部很多大型系統基本都需要依賴一個類似的系統來進行分佈式協同,但是這些系統往往都存在分佈式單點問題。

所以,雅虎的開發人員就開發了一個通用的無單點問題的分佈式協調框架,這就是 ZooKeeper。ZooKeeper 之後在開源界被大量使用,下面列出了 3 個著名開源項目是如何使用 ZooKeeper:

  • Hadoop:使用 ZooKeeper 做 Namenode 的高可用。
  • HBase:保證集群中只有一個 master,保存 hbase:meta 表的位置,保存集群中的 RegionServer 列表。
  • Kafka:集群成員管理,controller 節點選舉。

ZooKeeper 應用場景

很多分佈式協調服務都可以用 ZooKeeper 來做,其中典型應用場景如下:

  • 配置管理(configuration management):如果我們做普通的 Java 應用,一般配置項就是一個本地的配置文件,如果是微服務系統,各個獨立服務都要使用集中化的配置管理,這個時候就需要 ZooKeeper。
  • DNS 服務
  • 組成員管理(group membership):比如上面講到的 HBase 其實就是用來做集群的組成員管理。
  • 各種分佈式鎖

ZooKeeper 適用於存儲和協同相關的關鍵數據,不適合用於大數據量存儲。如果要存 KV 或者大量的業務數據,還是要用數據庫或者其他 NoSql 來做。

為什麼 ZooKeeper 不適合大數據量存儲呢?主要有以下兩個原因:

  1. 設計方面:ZooKeeper 需要把所有的數據(它的 data tree)加載到內存中。這就決定了ZooKeeper 存儲的數據量受內存的限制。這一點 ZooKeeper 和 Redis 比較像。一般的數據庫系統例如 MySQL(使用 InnoDB 存儲引擎的話)可以存儲大於內存的數據,這是因為 InnoDB 是基於 B-Tree 的存儲引擎。B-tree 存儲引擎和 LSM 存儲引擎都可以存儲大於內存的數據量。
  2. 工程方面:ZooKeeper 的設計目標是為協同服務提供數據存儲,數據的高可用性和性能是最重要的系統指標,處理大數量不是 ZooKeeper 的首要目標。因此,ZooKeeper 不會對大數量存儲做太多工程上的優化。

ZooKeeper 服務的使用

要使用 ZooKeeper 服務,首先我們的應用要引入 ZooKeeper 的客戶端庫,然後我們客戶端庫和 ZooKeeper 集群來進行網絡通信來使用 ZooKeeper 的服務,本質上是 Client-Server 的架構,我們的應用作為一個客戶端來調用 ZooKeeper Server 端的服務。

ZooKeeper 數據模型

ZooKeeper 的數據模型是層次模型。層次模型常見於文件系統。層次模型和 key-value 模型是兩種主流的數據模型。ZooKeeper 使用文件系統模型主要基於以下兩點考慮:

  1. 文件系統的樹形結構便於表達數據之間的層次關係。
  2. 文件系統的樹形結構便於為不同的應用分配獨立的命名空間(namespace)。

ZooKeeper 的層次模型稱作 data tree。Data tree 的每個節點叫做 znode。不同於文件系統,每個節點都可以保存數據。每個節點都有一個版本(version),版本從 0 開始計數。

如上圖所示的 data tree 中有兩個子樹,一個用於應用 1(/app1)和另一個用於應用 2(/app2)。

應用 1 的子樹實現了一個簡單的組成員協議:每個客戶端進程 pi 創建一個 znode p_i 在 /app1 下,只要 /app1/p_i 存在就代表進程 pi 在正常運行。

data tree 接口

ZooKeeper 對外提供一個用來訪問 data tree的簡化文件系統 API:

  • 使用 UNIX 風格的路徑名來定位 znode,例如 /A/X 表示 znode A 的子節點 X。
  • znode 的數據只支持全量寫入和讀取,沒有像通用文件系統那樣支持部分寫入和讀取。
  • data tree 的所有 API 都是 wait-free 的,正在執行中的 API 調用不會影響其他 API 的完成。
  • data tree 的 API都是對文件系統的 wait-free 操作,不直接提供鎖這樣的分佈式協同機制。但是 data tree 的 API 非常強大,可以用來實現多種分佈式協同機制。

znode 分類

一個 znode 可以是持久性的,也可以是臨時性的,znode 節點也可以是順序性的。每一個順序性的 znode 關聯一個唯一的單調遞增整數,因此 ZooKeeper 主要有以下 4 種 znode:

  1. 持久性的 znode (PERSISTENT): ZooKeeper 宕機,或者 client 宕機,這個 znode 一旦創建就不會丟失。
  2. 臨時性的 znode (EPHEMERAL): ZooKeeper 宕機了,或者 client 在指定的 timeout 時間內沒有連接 server,都會被認為丟失。
  3. 持久順序性的 znode (PERSISTENT_SEQUENTIAL): znode 除了具備持久性 znode 的特點之外,znode 的名字具備順序性。
  4. 臨時順序性的 znode (EPHEMERAL_SEQUENTIAL): znode 除了具備臨時性 znode 的特點之外,znode 的名字具備順序性。

安裝 ZooKeeper

到 https://archive.apache.org/dist/zookeeper/stable/ 下載 ZooKeeper,目前的最新版是 3.5.6。

把 apache-zookeeper-3.5.6-bin.tar.gz 解壓到一個本地目錄 (目錄名最好不要包含空格和中文)。我使用 /usr/local 目錄。

tar -zxvf apache-zookeeper-3.5.6-bin.tar.gz

把 conf 目錄下的 zoo_sample.cfg 重命名為 zoo.cfg,然後修改配置。

# 心跳檢查的時間 2秒  tickTime=2000  # 初始化時 連接到服務器端的間隔次數,總時間10*2=20秒  initLimit=10  # ZK Leader 和follower 之間通訊的次數,總時間5*2=10秒  syncLimit=5  # 存儲內存中數據快照的位置,如果不設置參數,更新事務日誌將被存儲到默認位置。  dataDir=/data/zookeeper  # ZK 服務器端的監聽端口  clientPort=2181

配置以下環境變量 vim /etc/profile

export ZOOKEEPER_HOME=/usr/local/apache-zookeeper-3.5.6-bin  export PATH=$PATH:$ZOOKEEPER_HOME/bin:$ZOOKEEPER_HOME/conf

啟動 Zookeeper

再安裝配置完成後,就可以啟動 Zookeeper,使用 zkServer.sh start 啟動 ZooKeeper 服務:

[root@wupx apache-zookeeper-3.5.6-bin]# zkServer.sh start  /usr/bin/java  ZooKeeper JMX enabled by default  Using config: /usr/local/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg  Starting zookeeper ... STARTED

檢查 ZooKeeper 日誌是否有出錯信息:

[root@wupx apache-zookeeper-3.5.6-bin]# cd logs/  [root@wupx logs]# grep -E -i "((exception)|(error))" *

因為返回沒有結果,說明沒有錯誤信息。

檢查 ZooKeeper 數據文件,這裡存放的 ZooKeeper 的事務日誌文件和快照日誌文件。

[root@wupx zookeeper]# cd /data/zookeeper/  [root@wupx zookeeper]# tree  .  ├── version-2  │   └── snapshot.0  └── zookeeper_server.pid  1 directory, 2 files

因為現在還沒有運行任何 ZooKeeper 命令,所以還沒有事務日誌文件。

最後會檢查 ZooKeeper 是否在 2181 端口上監聽。

netstat -an | ag 2181

執行後,我們可以看到 ZooKeeper 已經在 2181 這個端口上監聽了。

下面我們演示下如何使用 zkCli:

zkCli 使用

在執行 zkCli.sh 命令後,會出現很多消息,這些消息證明我們的 zkCli 和 ZooKeeper 的節點建立了有效連接。

2019-12-22 10:38:36,684 [myid:localhost:2181] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@959] - Socket connection established, initiating session, client: /127.0.0.1:54038, server: localhost/127.0.0.1:2181

使用 ls -R / 可以遞歸查找 ZooKeeper 的 znode 節點,使用 create /znode_name 可以創建 znode 節點,具體演示如下:

# 使用 ls -R 可以遞歸查找 ZooKeeper 的 znode 節點  [zk: localhost:2181(CONNECTED) 0] ls -R /  /  /zookeeper  /zookeeper/config  /zookeeper/quota  # 創建 znode /app1  [zk: localhost:2181(CONNECTED) 1] create /app1  Created /app1  [zk: localhost:2181(CONNECTED) 2] create /app2  Created /app2  [zk: localhost:2181(CONNECTED) 3] create /app1/p_1 1  Created /app1/p_1  [zk: localhost:2181(CONNECTED) 4] create /app1/p_2 2  Created /app1/p_2  [zk: localhost:2181(CONNECTED) 5] create /app1/p_3 3  Created /app1/p_3  [zk: localhost:2181(CONNECTED) 6] ls -R /  /  /app1  /app2  /zookeeper  /app1/p_1  /app1/p_2  /app1/p_3  /zookeeper/config  /zookeeper/quota

用 zkCli 實現鎖

分佈式鎖要求如果鎖的持有者宕了,鎖可以被釋放。ZooKeeper 的 ephemeral 節點恰好具備這樣的特性。

接下來我們來演示下,需要在兩個終端上分別啟動 zkCli,

在終端 1 上:

執行 zkCli.sh,再執行 create -e /lock 命令,來建立臨時 znode,加鎖的操作其實就是建立 znode 的過程,此時第一個客戶端加鎖成功。

接下來嘗試在第二個客戶端加鎖,在終端 2 上:

執行 zkCli.sh,再執行 create -e /lock 命令,會發現提示 Node already exists: /lock,提示 znode 已存在,znode 建立失敗,因此加鎖失敗,這時候我們來監控這個 znode,使用 stat -w /lock 來等待鎖被釋放。

這個時候我們退出第一個客戶端,在終端 1 上執行 quit 命令,會在客戶端 2 上收到一條 WATCHER 信息,具體如下:

WATCHER::  WatchedEvent state:SyncConnected type:NodeDeleted path:/lock

再收到這個事件後再次在客戶端 2 上執行加鎖,執行 create -e /lock,會顯示創建 znode 成功,即加鎖成功。

總結

這篇文章主要介紹了 ZooKeeper 的安裝配置,ZooKeeper 的基本概念和 zkCli 的使用,並用 zkCli 來實現一個鎖,為後面更加深入的學習打好基礎。

參考 https://zookeeper.apache.org/doc/current/zookeeperOver.html https://zookeeper.apache.org/doc/current/zookeeperStarted.html 《從Paxos到Zookeeper:分佈式一致性原理與實踐》