1. 程式人生 > >萬字長文帶你入門Zookeeper!!!

萬字長文帶你入門Zookeeper!!!

導讀

  • 文章首發於微信公眾號【碼猿技術專欄】,原創不易,謝謝支援。
  • Zookeeper 相信大家都聽說過,最典型的使用就是作為服務註冊中心。今天陳某帶大家從零基礎入門 Zookeeper,看了本文,你將會對 Zookeeper 有了初步的瞭解和認識。
  • 注意:本文基於 Zookeeper 的版本是 3.4.14,最新版本的在使用上會有一些出入,但是企業現在使用的大部分都是 3.4x 版本的。

Zookeeper 概述

  • Zookeeper 是一個分散式協調服務的開源框架。主要用來解決分散式叢集中應用系統的一致性問題,例如怎樣避免同時操作同一資料造成髒讀的問題。
  • ZooKeeper 本質上是一個分散式的小檔案儲存系統。提供基於類似於檔案系 統的目錄樹方式的資料儲存,並且可以對樹中的節點進行有效管理。從而用來維護和監控你儲存的資料的狀態變化。通過監控這些資料狀態的變化,從而可以達 到基於資料的叢集管理。諸如:統一命名服務
    分散式配置管理分散式訊息佇列分散式鎖分散式協調等功能。

Zookeeper 特性

  1. 全域性資料一致:每個 server 儲存一份相同的資料副本,client 無論連 接到哪個 server,展示的資料都是一致的,這是最重要的特徵;

  2. 可靠性:如果訊息被其中一臺伺服器接受,那麼將被所有的伺服器接受。

  3. 順序性:包括全域性有序和偏序兩種:全域性有序是指如果在一臺伺服器上 訊息 a 在訊息 b 前釋出,則在所有 Server 上訊息 a 都將在訊息 b 前被 釋出;偏序是指如果一個訊息 b 在訊息 a 後被同一個傳送者釋出,a 必將排在 b 前面。

  4. 資料更新原子性:一次資料更新要麼成功(半數以上節點成功),要麼失 敗,不存在中間狀態;

  5. 實時性:Zookeeper 保證客戶端將在一個時間間隔範圍內獲得伺服器的更新資訊,或者伺服器失效的資訊。

Zookeeper 節點型別

  • Znode 有兩種,分別為臨時節點和永久節點。
    • 臨時節點:該節點的生命週期依賴於建立它們的會話。一旦會話結束,臨時節點將被自動刪除,當然可以也可以手動刪除。臨時節點不允許擁有子節點。
    • 永久節點:該節點的生命週期不依賴於會話,並且只有在客戶端顯示執行刪除操作的時候,他們才能被刪除。
  • 節點的型別在建立時即被確定,並且不能改變。
  • Znode 還有一個序列化的特性,如果建立的時候指定的話,該 Znode 的名字後面會自動追加一個不斷增加的序列號。序列號對於此節點的父節點來說是唯一的,這樣便會記錄每個子節點建立的先後順序。它的格式為"%10d"
    (10 位數字,沒有數值的數位用 0 補充,例如“0000000001”)。
  • 這樣便會存在四種類型的 Znode 節點,分類如下:
    • PERSISTENT:永久節點
    • EPHEMERAL:臨時節點
    • PERSISTENT_SEQUENTIAL:永久節點、序列化
    • EPHEMERAL_SEQUENTIAL:臨時節點、序列化

ZooKeeper Watcher

  • ZooKeeper 提供了分散式資料釋出/訂閱功能,一個典型的釋出/訂閱模型系統定義了一種一對多的訂閱關係,能讓多個訂閱者同時監聽某一個主題物件,當這個主題物件自身狀態變化時,會通知所有訂閱者,使他們能夠做出相應的處理。
  • 觸發事件種類很多,如:節點建立,節點刪除,節點改變,子節點改變等。
  • 總的來說可以概括 Watcher 為以下三個過程:客戶端向服務端註冊 Watcher、服務端事件發生觸發 Watcher、客戶端回撥 Watcher 得到觸發事件情況。

Watcher 機制特點

  • 一次性觸發 :事件發生觸發監聽,一個 watcher event 就會被髮送到設定監聽的客戶端,這種效果是一次性的,後續再次發生同樣的事件,不會再次觸發。

  • 事件封裝 :ZooKeeper 使用 WatchedEvent 物件來封裝服務端事件並傳遞。WatchedEvent 包含了每一個事件的三個基本屬性: 通知狀態(keeperState),事件型別(EventType)和節點路徑(path)。

  • event 非同步傳送 :watcher 的通知事件從服務端傳送到客戶端是非同步的。

  • 先註冊再觸發 :Zookeeper 中的 watch 機制,必須客戶端先去服務端註冊監聽,這樣事件傳送才會觸發監聽,通知給客戶端。

常用 Shell 命令

新增節點

create [-s] [-e] path data
  • -s:表示建立有序節點
  • -e:表示建立臨時節點
  • 建立持久化節點:
create /test 1234

## 子節點
create /test/node1 node1
  • 建立持久化有序節點:
## 完整的節點名稱是a0000000001
create /a a
Created /a0000000001

## 完整的節點名稱是b0000000002
create /b b
Created /b0000000002
  • 建立臨時節點:
create -e /a a
  • 建立臨時有序節點:
## 完整的節點名稱是a0000000001
create -e -s /a a
Created /a0000000001

更新節點

set [path] [data] [version]
  • path:節點路徑
  • data:資料
  • version:版本號
  • 修改節點資料:
set /test aaa

## 修改子節點
set /test/node1 bbb
  • 基於資料版本號修改,如果修改的節點的版本號(dataVersion)不正確,拒絕修改
set /test aaa 1

刪除節點

delete [path] [version]
  • path:節點路徑
  • version:版本號,版本號不正確拒絕刪除
  • 刪除節點
delete /test

## 版本號刪除
delete /test 2
  • 遞迴刪除,刪除某個節點及後代
rmr /test

檢視節點資料和狀態

  • 命令格式如下:
get path
  • 獲取節點詳情:
## 獲取節點詳情
get /node1

## 節點內容
aaa
cZxid = 0x6
ctime = Sun Apr 05 14:50:10 CST 2020
mZxid = 0x6
mtime = Sun Apr 05 14:50:10 CST 2020
pZxid = 0x7
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 3
numChildren = 1
  • 節點各個屬性對應的含義如下:
    • cZxid:資料節點建立時的事務 ID。
    • ctime:資料節點建立時間。
    • mZxid:資料節點最後一次更新時的事務 ID。
    • mtime:資料節點最後一次更新的時間。
    • pZxid:資料節點的子節點最後一次被修改時的事務 ID。
    • cversion:子節點的更改次數。
    • dataVersion:節點資料的更改次數。
    • aclVersion :節點 ACL 的更改次數。
    • ephemeralOwner:如果節點是臨時節點,則表示建立該節點的會話的 SessionID。如果節點是持久化節點,值為 0。
    • dataLength :節點資料內容的長度。
    • numChildren:資料節點當前的子節點的個數。

檢視節點狀態

stat path
  • stat命令和get命令相似,不過這個命令不會返回節點的資料,只返回節點的狀態屬性。
stat /node1

## 節點狀態資訊,沒有節點資料
cZxid = 0x6
ctime = Sun Apr 05 14:50:10 CST 2020
mZxid = 0x6
mtime = Sun Apr 05 14:50:10 CST 2020
pZxid = 0x7
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 3
numChildren = 1

檢視節點列表

  • 檢視節點列表有ls pathls2 path兩個命令。後者是前者的增強,不僅會返回節點列表還會返回當前節點的狀態資訊。
  • ls path
ls /

## 僅僅返回節點列表
[zookeeper, node1]
  • ls2 path
ls2 /

## 返回節點列表和當前節點的狀態資訊
[zookeeper, node1]
cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x0
mtime = Thu Jan 01 08:00:00 CST 1970
pZxid = 0x6
cversion = 2
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 2

監聽器 get path watch

  • 使用get path watch註冊的監聽器在節點內容發生改變時,向客戶端傳送通知,注意 Zookeeper 的觸發器是一次性的,觸發一次後會立即生效。
get /node1 watch

## 改變節點資料
set /node1 bbb

## 監聽到節點內容改變了
WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/node1

監聽器 stat path watch

  • stat path watch註冊的監聽器能夠在節點狀態發生改變時向客戶端發出通知。比如節點資料改變、節點被刪除等。
stat /node2 watch

## 刪除節點node2
delete /node2

## 監聽器監聽到了節點刪除
WATCHER::
WatchedEvent state:SyncConnected type:NodeDeleted path:/node2

監聽器 ls/ls2 path watch

  • 使用ls path watch或者ls2 path watch註冊的監聽器,能夠監聽到該節點下的子節點的增加刪除操作。
ls /node1 watch

## 建立子節點
create /node1/b b

## 監聽到了子節點的新增
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/node1

Zookeeper 的 ACL 許可權控制

  • zookeeper 類似檔案控制系統,client 可以建立,刪除,修改,檢視節點,那麼如何做到許可權控制的呢?zookeeper 的access control list 訪問控制列表可以做到這一點。
  • ACL 許可權控制,使用scheme:id:permission來標識。
    • 許可權模式(scheme):授權的策略
    • 授權物件(id):授權的物件
    • 許可權(permission):授予的許可權
  • 許可權控制是基於每個節點的,需要對每個節點設定許可權。
  • 每個節點支援設定多種許可權控制方案和多個許可權。
  • 子節點不會繼承父節點的許可權,客戶端無權訪問某節點,但可能可以訪問它的子節點。
  • 例如:根據 IP 地址進行授權,命令如下:
setACl /node1 ip:192.168.10.1:crdwa

許可權模式

  • 許可權模式即是採用何種方式授權。
  • world:只有一個使用者,anyone,表示登入 zookeeper 所有人(預設的模式)。
  • ip:對客戶端使用 IP 地址認證。
  • auth:使用已新增認證的使用者認證。
  • digest:使用使用者名稱:密碼方式認證。

授權物件

  • 給誰授權,授權物件的 ID 指的是許可權賦予的實體,例如 IP 地址或使用者。

授予的許可權

  • 授予的許可權包括createdeletereadwriteradmin。也就是增、刪、改、查、管理的許可權,簡寫cdrwa
  • 注意:以上 5 種許可權中,delete是指對子節點的刪除許可權,其他 4 種許可權是對自身節點的操作許可權。
  • create:簡寫c,可以建立子節點。
  • delete:簡寫d,可以刪除子節點(僅下一級節點)。
  • read:簡寫r,可以讀取節點資料以及顯示子節點列表。
  • write:簡寫w,可以更改節點資料。
  • admin:簡寫a,可以設定節點訪問控制列表許可權。

授權相關命令

  • getAcl [path]:讀取指定節點的 ACL 許可權。
  • setAcl [path] [acl]:設定 ACL
  • addauth <scheme> <auth>:新增認證使用者,和 auth,digest 授權模式相關。

world 授權模式案例

  • zookeeper 中預設的授權模式,針對登入 zookeeper 的任何使用者授予指定的許可權。命令如下:
setAcl [path] world:anyone:[permission]
  • path:節點
  • permission:授予的許可權,比如cdrwa
  • 去掉不能讀取節點資料的許可權:
## 獲取許可權列表(預設的)
getAcl /node2

'world,'anyone
: cdrwa

## 去掉讀取節點資料的的許可權,去掉r
setAcl /node2 world:anyone:cdwa

## 再次獲取許可權列表
getAcl /node2

'world,'anyone
: cdwa

## 獲取節點資料,沒有許可權,失敗
get /node2

Authentication is not valid : /node2

IP 授權模式案例

  • 針對登入使用者的 ip 進行限制許可權。命令如下:
setAcl [path] ip:[ip]:[acl]
  • 遠端登入 zookeeper 的命令如下:
./zkCli.sh -server ip
  • 設定192.168.10.1這個 ip 的增刪改查管理的許可權。
setAcl /node2 ip:192.168.10.1:crdwa

Auth 授權模式案例

  • auth 授權模式需要有一個認證使用者,新增命令如下:
addauth digest [username]:[password]
  • 設定 auth 授權模式命令如下:
setAcl [path] auth:[user]:[acl]
  • chenmou這個賬戶新增 cdrwa 許可權:
## 新增一個認證賬戶
addauth digest chenmou:123456

## 新增許可權
setAcl /node2 auth:chenmou:crdwa

多種模式授權

  • zookeeper 中同一個節點可以使用多種授權模式,多種授權模式用,分隔。
## 建立節點
create /node3

## 新增認證使用者
addauth chenmou:123456

## 新增多種授權模式
setAcl /node3 ip:192.178.10.1:crdwa,auth:chenmou:crdwa

ACL 超級管理員

  • zookeeper 的許可權管理模式有一種叫做super,該模式提供一個超管可以方便的訪問任何許可權的節點。
  • 假設這個超管是super:admin,需要先為超管生成密碼的密文:
echo -n super:admin | openssl dgst  -binary -sha1 |openssl base64

## 執行完生成了祕鑰
xQJmxLMiHGwaqBvst5y6rkB6HQs=
  • 開啟zookeeper目錄下/bin/zkServer.sh,找到如下一行:
nohup JAVA&quot;−Dzookeeper.log.dir=JAVA"−Dzookeeper.log.dir={ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}"
  • 在後面新增一行指令碼,如下:
"-Dzookeeper.DigestAuthenticationProvider.superDigest=super:xQJmxLMiHGwaqBvst5y6rkB6HQs="
  • 此時完整的指令碼如下:
nohup "$JAVA" "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" "-Dzookeeper.DigestAuthenticationProvider.superDigest=super:xQJmxLMiHGwaqBvst5y6rkB6HQs=" \
    -cp "$CLASSPATH" $JVMFLAGS $ZOOMAIN "$ZOOCFG" > "$_ZOO_DAEMON_OUT" 2>&1 < /dev/null &
  • 重啟 zookeeper
  • 重啟完成之後此時超管即配置完成,如果需要使用,則使用如下命令:
 addauth digest super:admin

Curator 客戶端

  • Curator 是 Netflix 公司開源的一個 Zookeeper 客戶端,與 Zookeeper 提供的原生客戶端相比,Curator 的抽象層次更高,簡化了 Zookeeper 客戶端的開發量。

新增依賴

	<dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>4.0.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.zookeeper</groupId>
                    <artifactId>zookeeper</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.4.10</version>
        </dependency>

建立連線

  • 客戶端建立與 Zookeeper 的連線,這裡僅僅演示單機版本的連線,如下:
//建立CuratorFramework,用來操作api
CuratorFramework  client = CuratorFrameworkFactory.builder()
    //ip地址+埠號,如果是叢集,逗號分隔
    .connectString("120.26.101.207:2181")
    //會話超時時間
    .sessionTimeoutMs(5000)
    //超時重試策略,RetryOneTime:超時重連僅僅一次
    .retryPolicy(new RetryOneTime(3000))
    //名稱空間,父節點,如果不指定是在根節點下
    .namespace("node4")
    .build();
//啟動
client.start();

重連策略

  • 會話連線策略,即是當客戶端與 Zookeeper 斷開連線之後,客戶端重新連線 Zookeeper 時使用的策略,比如重新連線一次。
  • RetryOneTime:N 秒後重連一次,僅僅一次,演示如下:
.retryPolicy(new RetryOneTime(3000))
  • RetryNTimes:每 n 秒重連一次,重連 m 次。演示如下:
//每三秒重連一次,重連3次。arg1:多長時間後重連,單位毫秒,arg2:總共重連幾次
.retryPolicy(new RetryNTimes(3000,3))
  • RetryUntilElapsed:設定了最大等待時間,如果超過這個最大等待時間將會不再連線。
//每三秒重連一次,等待時間超過10秒不再重連。arg1:總等待時間,arg2:多長時間重連,單位毫秒
.retryPolicy(new RetryUntilElapsed(10000,3000))

新增節點

  • 新增節點
client.create()
    //指定節點的型別。PERSISTENT:持久化節點,PERSISTENT_SEQUENTIAL:持久化有序節點,EPHEMERAL:臨時節點,EPHEMERAL_SEQUENTIAL臨時有序節點
    .withMode(CreateMode.PERSISTENT)
    //指定許可權列表,OPEN_ACL_UNSAFE:world:anyone:crdwa
    .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
    //寫入節點資料,arg1:節點名稱 arg2:節點資料
    .forPath("/a", "a".getBytes());
  • 自定義許可權列表:withACL(acls)方法中可以設定自定義的許可權列表,程式碼如下:
//自定義許可權列表
List<ACL> acls=new ArrayList<>();
//指定授權模式和授權物件 arg1:授權模式,arg2授權物件
Id id=new Id("ip","127.0.0.1");
//指定授予的許可權,ZooDefs.Perms.ALL:crdwa
acls.add(new ACL(ZooDefs.Perms.ALL,id));
client.create()
    .withMode(CreateMode.PERSISTENT)
    //指定自定義許可權列表
    .withACL(acls)
    .forPath("/b", "b".getBytes());
  • 遞迴建立節點:creatingParentsIfNeeded()方法對於建立多層節點,如果其中一個節點不存在的話會自動建立
//遞迴建立節點
client.create()
    //遞迴方法,如果節點不存在,那麼建立該節點
    .creatingParentsIfNeeded()
    .withMode(CreateMode.PERSISTENT)
    .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
    //test節點和b節點不存在,遞迴創建出來
    .forPath("/test/a", "a".getBytes());
  • 非同步建立節點:inBackground()方法可以非同步回撥建立節點,建立完成後會自動回撥實現的方法
 //非同步建立節點
client.create()
    .withMode(CreateMode.PERSISTENT)
    .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
    //非同步建立
    .inBackground(new BackgroundCallback() {
        /**
        * @param curatorFramework 客戶端物件
        * @param curatorEvent 事件物件
        */
        @Override
        public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
            //列印事件型別
            System.out.println(curatorEvent.getType());
            }
    })
    .forPath("/test1", "a".getBytes());

更新節點資料

  • 更新節點,當節點不存在會報錯,程式碼如下:
client.setData()
      .forPath("/a","a".getBytes());
  • 攜帶版本號更新節點,當版本錯誤拒絕更新
client.setData()
    //指定版本號更新,如果版本號錯誤則拒絕更新
    .withVersion(1)
    .forPath("/a","a".getBytes());
  • 非同步更新節點資料:
client.setData()
        //非同步更新
        .inBackground(new BackgroundCallback() {
        //回撥方法
        @Override
        public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
                   }
        })
		.forPath("/a","a".getBytes());

刪除節點

  • 刪除當前節點,如果有子節點則拒絕刪除
client.delete()
    //刪除節點,如果是該節點包含子節點,那麼不能刪除
    .forPath("/a");
  • 指定版本號刪除,如果版本錯誤則拒絕刪除
client.delete()
    //指定版本號刪除
    .withVersion(1)
    //刪除節點,如果是該節點包含子節點,那麼不能刪除
    .forPath("/a");
  • 如果當前節點包含子節點則一併刪除,使用deletingChildrenIfNeeded()方法
client.delete()
    //如果刪除的節點包含子節點則一起刪除
    .deletingChildrenIfNeeded()
    //刪除節點,如果是該節點包含子節點,那麼不能刪除
    .forPath("/a");
  • 非同步刪除節點,使用inBackground()
client.delete()
	.deletingChildrenIfNeeded()
	//非同步刪除節點
    .inBackground(new BackgroundCallback() {
        @Override
        public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
        //回撥監聽
        }
       })
    //刪除節點,如果是該節點包含子節點,那麼不能刪除
    .forPath("/a");

獲取節點資料

  • 同步獲取節點資料
byte[] bytes = client.getData().forPath("/node1");
System.out.println(new String(bytes));
  • 獲取節點狀態和資料
//儲存節點狀態
Stat stat=new Stat();
byte[] bytes = client.getData()
	//獲取節點狀態儲存在stat物件中
    .storingStatIn(stat)
    .forPath("/node1");
System.out.println(new String(bytes));
//獲取節點資料的長度
System.out.println(stat.getDataLength());
  • 非同步獲取節點資料
client.getData()
    //非同步獲取節點資料,回撥監聽
     .inBackground((curatorFramework, curatorEvent) -> {
          //節點資料
          System.out.println(new String(curatorEvent.getData()));
      })
     .forPath("/node1");

獲取子節點

  • 同步獲取全部子節點
 List<String> strs = client.getChildren().forPath("/");
        for (String str:strs) {
            System.out.println(str);
        }
  • 非同步獲取全部子節點
client.getChildren()
//非同步獲取
.inBackground((curatorFramework, curatorEvent) -> {
        List<String> strs = curatorEvent.getChildren();
        for (String str:strs) {
              System.out.println(str);
        }
  })
.forPath("/");

檢視節點是否存在

  • 同步檢視
//如果節點不存在,stat為null
Stat stat = client.checkExists().forPath("/node");
  • 非同步檢視
//如果節點不存在,stat為null
client.checkExists()
    .inBackground((curatorFramework, curatorEvent) -> {
    //如果為null則不存在
    System.out.println(curatorEvent.getStat());
    })
    .forPath("/node");

Watcher API

  • curator 提供了兩種 watcher 來監聽節點的變化
    • NodeCache:監聽一個特定的節點,監聽新增和修改
    • PathChildrenCache:監聽一個節點的子節點,當一個子節點增加、刪除、更新時,path Cache 會改變他的狀態,會包含最新的子節點的資料和狀態。
  • NodeCache 演示:
//arg1:連線物件 arg2:監聽的節點路徑,/namespace/path
final NodeCache nodeCache = new NodeCache(client, "/w1");
//啟動監聽
nodeCache.start();
//新增監聽器
nodeCache.getListenable().addListener(() -> {
    //節點路徑
    System.out.println(nodeCache.getCurrentData().getPath());
    //節點資料
    System.out.println(new String(nodeCache.getCurrentData().getData()));
});
//睡眠100秒
Thread.sleep(1000000);
//關閉監聽
nodeCache.close();
  • PathChildrenCache演示:
//arg1:連線物件 arg2:節點路徑  arg3:是否能夠獲取節點資料
PathChildrenCache cache=new PathChildrenCache(client,"/w1", true);
cache.start();
cache.getListenable().addListener((curatorFramework, pathChildrenCacheEvent) -> {
	//節點路徑
	System.out.println(pathChildrenCacheEvent.getData().getPath());
	//節點狀態
	System.out.println(pathChildrenCacheEvent.getData().getStat());
	//節點資料
	System.out.println(new String(pathChildrenCacheEvent.getData().getData()));
});
cache.close();

小福利

  • 是不是覺得文章太長看得頭暈腦脹,為此陳某特地將本篇文章製作成 PDF 文字,需要回去仔細研究的朋友,老規矩,關注微信公眾號【碼猿技術專欄】回覆關鍵詞ZK入門指南