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

  • 2020 年 4 月 11 日
  • 筆記

導讀

  • 文章首發於微信公眾號【碼猿技術專欄】,原創不易,謝謝支援。
  • 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入門指南