萬字長文帶你入門Zookeeper!!!
導讀
- 文章首發於微信公眾號【碼猿技術專欄】,原創不易,謝謝支援。
- Zookeeper 相信大家都聽說過,最典型的使用就是作為服務註冊中心。今天陳某帶大家從零基礎入門 Zookeeper,看了本文,你將會對 Zookeeper 有了初步的了解和認識。
- 注意:本文基於 Zookeeper 的版本是 3.4.14,最新版本的在使用上會有一些出入,但是企業現在使用的大部分都是 3.4x 版本的。
Zookeeper 概述
- Zookeeper 是一個分散式協調服務的開源框架。主要用來解決分散式集群中應用系統的一致性問題,例如怎樣避免同時操作同一數據造成臟讀的問題。
- ZooKeeper 本質上是一個分散式的小文件存儲系統。提供基於類似於文件系 統的目錄樹方式的數據存儲,並且可以對樹中的節點進行有效管理。從而用來維護和監控你存儲的數據的狀態變化。通過監控這些數據狀態的變化,從而可以達 到基於數據的集群管理。諸如:
統一命名服務
、分散式配置管理
、分散式消息隊列
、分散式鎖
、分散式協調
等功能。
Zookeeper 特性
-
全局數據一致
:每個 server 保存一份相同的數據副本,client 無論連 接到哪個 server,展示的數據都是一致的,這是最重要的特徵;
-
可靠性
:如果消息被其中一台伺服器接受,那麼將被所有的伺服器接受。
-
順序性
:包括全局有序和偏序兩種:全局有序是指如果在一台伺服器上 消息 a 在消息 b 前發布,則在所有 Server 上消息 a 都將在消息 b 前被 發布;偏序是指如果一個消息 b 在消息 a 後被同一個發送者發布,a 必將排在 b 前面。
-
數據更新原子性
:一次數據更新要麼成功(半數以上節點成功),要麼失 敗,不存在中間狀態;
-
實時性
: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 path
和ls2 path
兩個命令。後者是前者的增強,不僅會返回節點列表還會返回當前節點的狀態資訊。
ls path
:
ls / ## 僅僅返回節點列表 [zookeeper, node1]
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 地址或用戶。
授予的許可權
- 授予的許可權包括
create
、delete
、read
、writer
、admin
。也就是增、刪、改、查、管理的許可權,簡寫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 授權模式案例
setAcl [path] ip:[ip]:[acl]
./zkCli.sh -server ip
- 設置
192.168.10.1
這個 ip 的增刪改查管理的許可權。
setAcl /node2 ip:192.168.10.1:crdwa
Auth 授權模式案例
- auth 授權模式需要有一個認證用戶,添加命令如下:
addauth digest [username]:[password]
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"−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");
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();
//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入門指南
。