什么
1 zookeeper 與分布式系統(tǒng)
zookeeper 是一個(gè)中間件,為分布式系統(tǒng)提供協(xié)調(diào)(Coordination)服務(wù)瞒渠。是Google Chubby的開源實(shí)現(xiàn)嫩痰,Google的三篇論文總都提及了一個(gè)lock service -- Chubby,于是就有了Chubby的開源實(shí)現(xiàn) zookeeper。
1.1 什么是分布式系統(tǒng)
分布式系統(tǒng)
很多臺(tái)計(jì)算機(jī)組成一個(gè)整體杆逗,一個(gè)整體一致對(duì)外并且處理同一個(gè)請(qǐng)求。
內(nèi)部的每臺(tái)計(jì)算機(jī)都可以相互通信(rest/RPC)
客戶端到服務(wù)端的一次請(qǐng)求,到響應(yīng)結(jié)束會(huì)歷經(jīng)多臺(tái)計(jì)算機(jī)
如下圖所示,小慕是客戶端,訪問分布式文件系統(tǒng)(網(wǎng)盤),服務(wù)端在服務(wù)器A,服務(wù)器B是服務(wù)器A的備用機(jī)從而實(shí)現(xiàn)高可用,具體的文件保存在文件服務(wù)器中耳贬,為了防止文件丟失,一個(gè)文件會(huì)保存在多個(gè)文件服務(wù)器中。一次請(qǐng)求歷經(jīng)了3臺(tái)計(jì)算機(jī)。
下圖是一個(gè)電商網(wǎng)站下單請(qǐng)求響應(yīng)流程育叁。用戶在商品頁面下單后,會(huì)經(jīng)過商品服務(wù)查看商品庫存,再經(jīng)過訂單服務(wù)生成訂單,再經(jīng)過賬單服務(wù),最后返回到商品頁面。一個(gè)請(qǐng)求歷經(jīng)了4臺(tái)計(jì)算機(jī)挖帘。
1.2 什么是zookeeper
zookeeper 是一個(gè)中間件骄崩,為分布式系統(tǒng)提供協(xié)調(diào)服務(wù)站楚。我們可以把zookeeper看成是一個(gè)分布式數(shù)據(jù)庫:
- 一個(gè)具有文件系統(tǒng)特點(diǎn)的分布式數(shù)據(jù)庫
- 解決了數(shù)據(jù)一致性問題的分布式數(shù)據(jù)庫
- 具有發(fā)布訂閱功能的分布式數(shù)據(jù)庫
1.3 zookeeper的特性
- 一致性:數(shù)據(jù)一致性彻况,數(shù)據(jù)按照順序分批入庫
- 原子性:事務(wù)要么成功要么失敗
- 單一視圖:客戶端連接集群中的zk節(jié)點(diǎn),數(shù)據(jù)都是一致的
- 可靠性:每次對(duì)zk的操作狀態(tài)都會(huì)保存在服務(wù)端
- 實(shí)時(shí)性:客戶端可以讀取到zk服務(wù)端的最新數(shù)據(jù)
2 zookeeper 的安裝與集群配置
- 安裝
JDK
左权,配置JAVA_HOME
- 在官網(wǎng)下載zookeeper壓縮包,上傳到Linux機(jī)器 /opt 目錄
- 解壓,
tar -zxvf zookeeper3.4.10.tar.gz
cp -r /opt/zookeeper3.4.10.tar.gz /myzookeeper
- 修改zoo.cfg文件
- 啟動(dòng)zookeeper服務(wù)端突想,
zkServer.sh start
- 檢查是否啟動(dòng)成功,
ps -ef | grep zookeeper
檢查進(jìn)程,echo ruok | nc 127.0.0.1:2181
返回imok
查看官方文檔或zookeeper docs目錄的index.html夷蚊,Started Guide快速使用中介紹了單機(jī)安裝和一些zk的基本概念箱歧,Programmer's Guide詳細(xì)介紹了zk的數(shù)據(jù)模型,節(jié)點(diǎn)類型,會(huì)話病毡,Watch事件,ACL權(quán)限控制等。
zookeeper 目錄結(jié)構(gòu)
bin:主要的一些運(yùn)行命令膨更,zkCli.sh 是啟動(dòng)zk客戶端旦棉,zkServer.sh是啟動(dòng)zk服務(wù)端
conf:配置文件童本,我們需要修改zoo_sample.cfg
contrib:附加的一些功能
dist-maven:保存mvn編譯結(jié)果的目錄泵额,包括jar亡资,sources.jar母谎,pom.xml
docs:zk幫助文檔咬扇,可以打開index.html查看梭灿,與官網(wǎng)文檔相同
lib:開發(fā)時(shí)使用的jar包,
recipes:案例demo代碼,包括election休溶,lock孽尽,queue
src:zk源碼
zoo.cfg 配置
復(fù)制 conf 目錄下的zoo_sample.cfg熏挎,重命名為zoo.cfg哼勇。該配置文件中有以下幾個(gè)屬性:
tickTime:用于計(jì)算的時(shí)間單元帝璧,單位是毫秒。比如session超時(shí)設(shè)置為 N逐虚,則超時(shí)時(shí)間為N * tickTime
initLimit:用于集群聋溜,初始化連接時(shí)間叭爱。follower服務(wù)器啟動(dòng)過程中撮躁,需要連接并同步Leader節(jié)點(diǎn)的所有最新數(shù)據(jù),不能超過initLimit买雾,以tickTime的倍數(shù)來表示
syncLimit:用于集群把曼,限制了follower服務(wù)器與Leader服務(wù)器之間請(qǐng)求和應(yīng)答的時(shí)限(心跳機(jī)制)杨帽;如果A發(fā)出心跳包在syncLimit之后沒有收到B的響應(yīng),就認(rèn)為這個(gè)B已經(jīng)不在線了
dataDir:zookeeper存儲(chǔ)的數(shù)據(jù)文件目錄嗤军。dataDir=/usr/local/zookeeper/dataDir
dataLogDir:日志目錄注盈,如果不配置則使用dataDir。dataLogDir==/usr/local/zookeeper/dataLogDir
clientPort:客戶端連接服務(wù)器的端口叙赚,默認(rèn)2181
zk 的常用命令
zkServer.sh start
啟動(dòng)zk服務(wù)老客,在windows中是zkServer.cmd
,不需要start命令震叮。
zkServer.sh stop
停止zk服務(wù)
zkServer.sh status
查看zookeeper狀態(tài)胧砰,返回zk的配置文件,客戶端連接端口苇瓣,服務(wù)器類型Mode為Leader或Follower
zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/apache-zookeeper-3.5.5-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: leader
echo ruok | nc 127.0.0.1:2181
返回imok說明zkServer啟動(dòng)成功
jps
查看啟動(dòng)的java進(jìn)程尉间,zk進(jìn)程名稱為QuorumPeerMain
zkCli.sh
啟動(dòng)zk客戶端,集群狀態(tài)需要制定zk服務(wù)器zkCli.sh -server 192.168.100.1:2181
zookeeper服務(wù)啟動(dòng)日志如下所示击罪,主要包括以下5部分內(nèi)容:
- 以單機(jī)模式啟動(dòng)
running in standalone mode
哲嘲, - 讀取配置文件zoo.cfg
Reading configuration from: E:\zookeeper-3.4.10\bin\..\conf\zoo.cfg
, - 開始啟動(dòng)服務(wù)
Starting server
媳禁, - 顯示zk環(huán)境信息眠副,包括zk的版本號(hào) version,主機(jī)名稱 hostname损话,java 版本侦啸,java_home,classpath丧枪,操作系統(tǒng)光涂,用戶名稱等
- 顯示配置信息,包括
tickTime set to 2000
拧烦,綁定端口binding to port 0.0.0.0/0.0.0.0:2181
E:\zookeeper-3.4.10\bin>zkServer.cmd
E:\zookeeper-3.4.10\bin>call "C:\Program Files\Java\jdk1.8.0_51"\bin\java "-Dzookeeper.log.dir=E:\zookeeper-3.4.10\bin\.." "-Dzookeeper.root.logger=INFO,CONSOLE" -cp "E:\zookeeper-3.4.10\bin\..\build\classes;E:\zookeeper-3.4.10\bin\..\build\lib\*;E:\zookeeper-3.4.10\bin\..\*;E:\zookeeper-3.4.10\bin\..\lib\*;E:\zookeeper-3.4.10\bin\..\conf" org.apache.zookeeper.server.quorum.QuorumPeerMain "E:\zookeeper-3.4.10\bin\..\conf\zoo.cfg"
2020-02-04 11:31:42,927 [myid:] - INFO [main:QuorumPeerConfig@134] - Reading configuration from: E:\zookeeper-3.4.10\bin\..\conf\zoo.cfg
2020-02-04 11:31:42,938 [myid:] - INFO [main:DatadirCleanupManager@78] - autopurge.snapRetainCount set to 3
2020-02-04 11:31:42,939 [myid:] - INFO [main:DatadirCleanupManager@79] - autopurge.purgeInterval set to 0
2020-02-04 11:31:42,940 [myid:] - INFO [main:DatadirCleanupManager@101] - Purge task is not scheduled.
2020-02-04 11:31:42,943 [myid:] - WARN [main:QuorumPeerMain@113] - Either no config or no quorum defined in config, running in standalone mode
2020-02-04 11:31:43,037 [myid:] - INFO [main:QuorumPeerConfig@134] - Reading configuration from: E:\zookeeper-3.4.10\bin\..\conf\zoo.cfg
2020-02-04 11:31:43,039 [myid:] - INFO [main:ZooKeeperServerMain@96] - Starting server
2020-02-04 11:31:52,125 [myid:] - INFO [main:Environment@100] - Server environment:zookeeper.version=3.4.10-39d3a4f269333c922ed3db283be479f9deacaa0f, built on 03/23/2017 10:13 GMT
2020-02-04 11:31:52,125 [myid:] - INFO [main:Environment@100] - Server environment:host.name=DESKTOP-HSRU97J
2020-02-04 11:31:52,128 [myid:] - INFO [main:Environment@100] - Server environment:java.version=1.8.0_51
2020-02-04 11:31:52,129 [myid:] - INFO [main:Environment@100] - Server environment:java.vendor=Oracle Corporation
2020-02-04 11:31:52,130 [myid:] - INFO [main:Environment@100] - Server environment:java.home=C:\Program Files\Java\jdk1.8.0_51\jre
2020-02-04 11:31:52,130 [myid:] - INFO [main:Environment@100] - Server environment:java.class.path=E:\zookeeper-3.4.10\bin\..\build\classes;E:\zookeeper-3.4.10\bin\..\build\lib\*;E:\zookeeper-3.4.10\bin\..\zookeeper-3.4.10.jar;E:\zookeeper-3.4.10\bin\..\lib\jline-0.9.94.jar;E:\zookeeper-3.4.10\bin\..\lib\log4j-1.2.16.jar;E:\zookeeper-3.4.10\bin\..\lib\netty-3.10.5.Final.jar;E:\zookeeper-3.4.10\bin\..\lib\slf4j-api-1.6.1.jar;E:\zookeeper-3.4.10\bin\..\lib\slf4j-log4j12-1.6.1.jar;E:\zookeeper-3.4.10\bin\..\conf
2020-02-04 11:31:52,131 [myid:] - INFO [main:Environment@100] - Server environment:java.library.path=C:\Program Files\Java\jdk1.8.0_51\bin;C:\WINDOWS\Sun\Java\bin;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\Program Files\Java\jdk1.8.0_51\bin;C:\Program Files\Java\jdk1.8.0_51\jre\bin;E:\Program Files (x86)\apache-maven-3.3.9\bin;C:\WINDOWS\System32\OpenSSH\;E:\Program Files (x86)\Git\cmd;C:\Users\Administrator\AppData\Local\Microsoft\WindowsApps;C:\Users\Administrator\AppData\Local\GitHubDesktop\bin;%USERPROFILE%\AppData\Local\Microsoft\WindowsApps;;.
2020-02-04 11:31:52,132 [myid:] - INFO [main:Environment@100] - Server environment:java.io.tmpdir=C:\Users\ADMINI~1\AppData\Local\Temp\
2020-02-04 11:31:52,133 [myid:] - INFO [main:Environment@100] - Server environment:java.compiler=<NA>
2020-02-04 11:31:52,138 [myid:] - INFO [main:Environment@100] - Server environment:os.name=Windows 8.1
2020-02-04 11:31:52,139 [myid:] - INFO [main:Environment@100] - Server environment:os.arch=amd64
2020-02-04 11:31:52,141 [myid:] - INFO [main:Environment@100] - Server environment:os.version=6.3
2020-02-04 11:31:52,144 [myid:] - INFO [main:Environment@100] - Server environment:user.name=Administrator
2020-02-04 11:31:52,145 [myid:] - INFO [main:Environment@100] - Server environment:user.home=C:\Users\Administrator
2020-02-04 11:31:52,146 [myid:] - INFO [main:Environment@100] - Server environment:user.dir=E:\zookeeper-3.4.10\bin
2020-02-04 11:31:52,158 [myid:] - INFO [main:ZooKeeperServer@829] - tickTime set to 2000
2020-02-04 11:31:52,158 [myid:] - INFO [main:ZooKeeperServer@838] - minSessionTimeout set to -1
2020-02-04 11:31:52,160 [myid:] - INFO [main:ZooKeeperServer@847] - maxSessionTimeout set to -1
2020-02-04 11:31:52,301 [myid:] - INFO [main:NIOServerCnxnFactory@89] - binding to port 0.0.0.0/0.0.0.0:2181
2020-02-04 11:32:01,829 [myid:] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /0:0:0:0:0:0:0:1:60100
2020-02-04 11:32:01,852 [myid:] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:ZooKeeperServer@942] - Client attempting to establish new session at /0:0:0:0:0:0:0:1:60100
2020-02-04 11:32:01,862 [myid:] - INFO [SyncThread:0:FileTxnLog@203] - Creating new log file: log.7a8
2020-02-04 11:32:02,218 [myid:] - INFO [SyncThread:0:ZooKeeperServer@687] - Established session 0x1700e411a110000 with negotiated timeout 30000 for client /0:0:0:0:0:0:0:1:60100
3 zookeeper基本數(shù)據(jù)模型
zookeeper數(shù)據(jù)模型是一個(gè)樹形結(jié)構(gòu)忘闻,類似于linux文件結(jié)構(gòu)。如下圖所示恋博,zk根目錄是 / 齐佳,是一個(gè)樹形結(jié)構(gòu)
zk的數(shù)據(jù)模型可以理解為linux的文件目錄:/usr/local/...
每一個(gè)節(jié)點(diǎn)都稱為znode,znode可以有子節(jié)點(diǎn)债沮,也可以有數(shù)據(jù)炼吴。
每個(gè)節(jié)點(diǎn)分為臨時(shí)節(jié)點(diǎn)和永久節(jié)點(diǎn),臨時(shí)節(jié)點(diǎn)在客戶端斷開后消失
每個(gè)節(jié)點(diǎn)znode都有自己的版本號(hào)疫衩,可以通過命令行來顯示節(jié)點(diǎn)信息
每當(dāng)節(jié)點(diǎn)數(shù)據(jù)發(fā)生變化硅蹦,那么該節(jié)點(diǎn)的版本號(hào)會(huì)加1(樂觀鎖參考文檔1)
刪除/修改過時(shí)節(jié)點(diǎn)時(shí),因?yàn)榘姹咎?hào)不匹配,則會(huì)修改失斖邸(樂觀鎖參考文檔1)
每個(gè)節(jié)點(diǎn)znode存儲(chǔ)的數(shù)據(jù)不宜過大涮瞻,幾k即可
節(jié)點(diǎn)可以設(shè)置權(quán)限控制列表acl,可以通過權(quán)限設(shè)置來限制用戶的訪問
3.1 zk 數(shù)據(jù)模型基本操作
客戶端連接
使用命令zkCli.sh
啟動(dòng)客戶端假褪,啟動(dòng)成功信息如下署咽,表示連接到了 localhost:2181,連接狀態(tài)是CONNECTED生音,后面的數(shù)字 0 表示運(yùn)行的命令數(shù)
[zk: localhost:2181(CONNECTED) 0]
輸入help
命令宁否,查看zk客戶端的常用命令如下,
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
znode結(jié)構(gòu)
Znode由三部分組成:path缀遍,data家淤,Stat
[zk: ] get /zookeeper # 節(jié)點(diǎn)路徑path
# 節(jié)點(diǎn)保存的數(shù)據(jù)data,此節(jié)點(diǎn)數(shù)據(jù)為空
# 下面是Stat信息
cZxid = 0x0 # 節(jié)點(diǎn)創(chuàng)建操作的zxid,create
ctime = Thu Jan 01 08:00:00 CST 1970 # 創(chuàng)建節(jié)點(diǎn)時(shí)間
mZxid = 0x0 # 節(jié)點(diǎn)最新修改操作的zxid瑟由,modify
mtime = Thu Jan 01 08:00:00 CST 1970 # 修改節(jié)點(diǎn)時(shí)間
pZxid = 0x0 # 子節(jié)點(diǎn)最后更新的zxid
cversion = -1 # 子節(jié)點(diǎn)的修改次數(shù),每次修改子節(jié)點(diǎn)version會(huì)加1, children
dataVersion = 0 # 當(dāng)前節(jié)點(diǎn)保存的數(shù)據(jù)的修改次數(shù)冤寿,每次修改數(shù)據(jù)version會(huì)加1
aclVersion = 0 # 用戶控制權(quán)限的修改次數(shù)歹苦,每次修改權(quán)限version會(huì)加1
ephemeralOwner = 0x0 # 如果是臨時(shí)節(jié)點(diǎn)表示該節(jié)點(diǎn)的session id;非臨時(shí)節(jié)點(diǎn)則為0
dataLength = 0 # 節(jié)點(diǎn)保存的數(shù)據(jù)的大小
numChildren = 1 # 子節(jié)點(diǎn)的數(shù)量
3.2 zookeeper的應(yīng)用場景
- 統(tǒng)一配置文件管理督怜,即只需要部署一臺(tái)服務(wù)器殴瘦,則可以把相同的配置文件同步更新到其他所有服務(wù)器,此操作在云計(jì)算中應(yīng)用特別多号杠。<a href="#watchconfig">查看詳細(xì) </a>
-
服務(wù)注冊(cè)和發(fā)現(xiàn)蚪腋,類似消息隊(duì)列MQ,dubbo發(fā)布者會(huì)把數(shù)據(jù)存到znode中姨蟋,訂閱者會(huì)讀取這個(gè)數(shù)據(jù)屉凯。如下圖所示,發(fā)布者發(fā)布數(shù)據(jù)眼溶,訂閱者根據(jù)數(shù)據(jù)的變化進(jìn)行操作悠砚。利用 Znode 和 Watcher,可以實(shí)現(xiàn)分布式服務(wù)的注冊(cè)和發(fā)現(xiàn)堂飞,最著名的應(yīng)用就是阿里的分布式 RPC 框架 Dubbo灌旧。
-
提供分布式鎖,分布式環(huán)境中不同進(jìn)程之間會(huì)爭奪資源绰筛,類似多線程中的鎖枢泰。下圖中多個(gè)服務(wù)器中的進(jìn)程要操作網(wǎng)盤中的文件,為了避免沖突铝噩,需要分布式鎖衡蚂。雅虎研究員設(shè)計(jì) ZooKeeper 的初衷。利用 ZooKeeper 的臨時(shí)順序節(jié)點(diǎn),可以輕松實(shí)現(xiàn)分布式鎖讳窟。
- 集群管理让歼,集群中保證數(shù)據(jù)的強(qiáng)一致性。無論客戶端讀取哪一臺(tái)機(jī)器的數(shù)據(jù)丽啡,都會(huì)得到一致的數(shù)據(jù)谋右,因?yàn)閦ookeeper會(huì)將數(shù)據(jù)從主節(jié)點(diǎn)同步到其他節(jié)點(diǎn)
- 此外,Kafka补箍、HBase改执、Hadoop 也都依靠 ZooKeeper 同步節(jié)點(diǎn)信息,實(shí)現(xiàn)高可用坑雅。
4 zookeeper 基本特性與客戶端操作
4.1 session的基本原理
- 客戶端與服務(wù)端之間的的連接稱之為Session(會(huì)話)
- 每個(gè)Session會(huì)話都可以設(shè)置一個(gè)超時(shí)時(shí)間辈挂,超時(shí)后Session會(huì)被銷毀
- 心跳停止,則session過期
- Session過期裹粤,則臨時(shí)節(jié)點(diǎn)znode會(huì)被拋棄
- 心跳機(jī)制终蒂,客戶端向服務(wù)端的ping包請(qǐng)求,為了向服務(wù)端表示客戶端在線
4.2 常用命令行操作
zookeeper節(jié)點(diǎn)znode有許多狀態(tài)信息(Stat)遥诉,其中有兩個(gè)重要概念zxid
和version numbers
拇泣。
zxid
下面的命令中經(jīng)常出現(xiàn)Zxid
,對(duì)ZooKeeper節(jié)點(diǎn)和子節(jié)點(diǎn)創(chuàng)建矮锈、更新數(shù)據(jù)(查詢不會(huì)修改zxid)都會(huì)收到一個(gè)zxid (ZooKeeper Transaction Id)形式的標(biāo)記霉翔。這將向ZooKeeper公開所有更改的總順序。每次更改都有一個(gè)惟一的zxid苞笨,如果zxid1小于zxid2债朵,則說明zxid1發(fā)生在zxid2之前。
zookeeper中的操作分為事務(wù)性操作(create瀑凝,set序芦,delete
),會(huì)使得zxid
加1猜丹,并且將該操作記錄持久化到日志中芝加;而非事務(wù)性操作(get,exist
)不會(huì)修改zxid
射窒。
Version Numbers
對(duì)節(jié)點(diǎn)的每次更改都會(huì)使得該節(jié)點(diǎn)的版本號(hào)version加 1藏杖。總共有三個(gè)version:
-
version
:對(duì)znode的數(shù)據(jù)的更改次數(shù) -
cversion
:對(duì)znode的子節(jié)點(diǎn)的更改次數(shù) -
aclversion
:對(duì)znode的ACL的更改次數(shù)
常用命令
ls
:顯示指定目錄(節(jié)點(diǎn))下的子節(jié)點(diǎn)
ls2
:ls2是顯示指定目錄(節(jié)點(diǎn))下的子節(jié)點(diǎn)脉顿,指定目錄的狀態(tài)信息蝌麸。等同于ls
+stat
命令
[zk: localhost:2181(CONNECTED) 2] ls /zookeeper
[quota]
[zk: localhost:2181(CONNECTED) 4] ls2 /zookeeper
[quota]
cZxid = 0x0 # 節(jié)點(diǎn)創(chuàng)建操作的zxid,create
ctime = Thu Jan 01 08:00:00 CST 1970 # 創(chuàng)建節(jié)點(diǎn)時(shí)間
mZxid = 0x0 # 節(jié)點(diǎn)最新修改操作的zxid艾疟,modify
mtime = Thu Jan 01 08:00:00 CST 1970 # 修改節(jié)點(diǎn)時(shí)間
pZxid = 0x0 # 子節(jié)點(diǎn)最后更新的zxid
cversion = -1 # 子節(jié)點(diǎn)的修改次數(shù)来吩,每次修改子節(jié)點(diǎn)version會(huì)加1, children
dataVersion = 0 # 當(dāng)前節(jié)點(diǎn)保存的數(shù)據(jù)的修改次數(shù)敢辩,每次修改數(shù)據(jù)version會(huì)加1
aclVersion = 0 # 用戶控制權(quán)限的修改次數(shù),每次修改權(quán)限version會(huì)加1
ephemeralOwner = 0x0 # 臨時(shí)節(jié)點(diǎn)擁有者,如果是臨時(shí)節(jié)點(diǎn)表示該節(jié)點(diǎn)的會(huì)話id弟疆;非臨時(shí)節(jié)點(diǎn)則為0
dataLength = 0 # 節(jié)點(diǎn)保存的數(shù)據(jù)的大小
numChildren = 1 # 子節(jié)點(diǎn)的數(shù)量
stat
:顯示指定節(jié)點(diǎn)的狀態(tài)信息戚长,與get
命令的區(qū)別是不顯示保存的數(shù)據(jù)信息
create [-s] [-e] path data acl
:創(chuàng)建節(jié)點(diǎn),-e Ephemeral表示臨時(shí)節(jié)點(diǎn)怠苔,-s sequence表示順序節(jié)點(diǎn)同廉,data必填,否則無法創(chuàng)建柑司,不支持遞歸創(chuàng)建
get
:獲取指定節(jié)點(diǎn)保存的信息和狀態(tài)信息
[zk:] create -e /imooc/tmp imooc-data2
Created /imooc/tmp
[zk: localhost:2181(CONNECTED) 9] get /imooc
imooc-data # 節(jié)點(diǎn)保存的數(shù)據(jù)信息
cZxid = 0x7ab
ctime = Tue Feb 04 16:11:01 CST 2020
pZxid = 0x7ac # 子節(jié)點(diǎn)最新操作的zxid
cversion = 1 # 子節(jié)點(diǎn)的修改次數(shù)
ephemeralOwner = 0x0 # 非臨時(shí)節(jié)點(diǎn),所以為0
numChildren = 1 # 創(chuàng)建了1個(gè)子節(jié)點(diǎn),所以為1
[zk: localhost:2181(CONNECTED) 10] get /imooc/tmp
imooc-data2
cZxid = 0x7ac # 節(jié)點(diǎn)創(chuàng)建操作的zxid,與父節(jié)點(diǎn)的pZxid相同
ephemeralOwner = 0x1700e411a110001 # 臨時(shí)節(jié)點(diǎn),表示會(huì)話id
問題:創(chuàng)建臨時(shí)節(jié)點(diǎn)后迫肖,停止客戶端,該臨時(shí)節(jié)點(diǎn)會(huì)立即消失嗎攒驰?
使用客戶端A創(chuàng)建臨時(shí)節(jié)點(diǎn)ephNode蟆湖,客戶端B可以查看該臨時(shí)節(jié)點(diǎn),強(qiáng)行終止客戶端A(不能使用quit命令退出)玻粪,發(fā)現(xiàn)客戶端B仍然能夠查看該臨時(shí)節(jié)點(diǎn)隅津,因?yàn)樾奶嬖诔瑫r(shí)時(shí)間,在超時(shí)范圍內(nèi)劲室,zk認(rèn)為該客戶端仍然正常饥瓷。
當(dāng)心跳超時(shí)后,session會(huì)話過期痹籍,臨時(shí)節(jié)點(diǎn)ephNode 也會(huì)被拋棄,此時(shí)使用客戶端B就查看不到該臨時(shí)節(jié)點(diǎn)了晦鞋。查看zoo.cfg
文件蹲缠,syncLimit
屬性就是心跳超時(shí)時(shí)間
# create -s 表示創(chuàng)建序列自增節(jié)點(diǎn),設(shè)置的節(jié)點(diǎn)名稱后會(huì)添加自增數(shù)
[zk:] create -s /imooc/seq seq-data
Created /imooc/seq0000000005
[zk:] create -s /imooc/seque seq-data
Created /imooc/seque0000000006
set path data [version]
:設(shè)置節(jié)點(diǎn)的數(shù)據(jù) ,version表示修改指定dataversion
的數(shù)據(jù)悠垛,如果參數(shù)version與節(jié)點(diǎn)的dataversion
不一致线定,則修改失敗,這是為了避免多個(gè)客戶端同時(shí)修改數(shù)據(jù)競爭產(chǎn)生的問題确买。
[zk: localhost:2181(CONNECTED) 3] get /imooc
imooc-data
mZxid = 0x7ab
dataVersion = 0
[zk: localhost:2181(CONNECTED) 4] set /imooc new-data
mZxid = 0x7c0 # 修改了節(jié)點(diǎn)數(shù)據(jù),記錄修改操作的zxid
dataVersion = 1 # 修改節(jié)點(diǎn)數(shù)據(jù)的次數(shù)
#修改節(jié)點(diǎn)指定版本的數(shù)據(jù)
[zk: localhost:2181(CONNECTED) 6] set /imooc 123 1
dataVersion = 2
# 當(dāng)節(jié)點(diǎn)dataVersion與參數(shù)1不相等時(shí),則修改失敗.樂觀鎖
[zk: localhost:2181(CONNECTED) 7] set /imooc 123 1
version No is not valid : /imooc
delete path [version]
:刪除節(jié)點(diǎn)斤讥,version需要與節(jié)點(diǎn)dataversion
一致,否則刪除失敗
4.3 watcher機(jī)制
客戶端可以在節(jié)點(diǎn)znode上設(shè)置一個(gè)watch事件湾趾,對(duì)該znode的更改將觸發(fā)該watch事件芭商,并清除該watch事件。當(dāng)一個(gè)watch事件觸發(fā)時(shí)搀缠,zookeeper會(huì)向客戶端發(fā)送一個(gè)通知铛楣。watcher機(jī)制的特點(diǎn)如下所示:
針對(duì)每個(gè)節(jié)點(diǎn)znode的操作,都會(huì)有一個(gè)監(jiān)督者
watcher
當(dāng)監(jiān)控的某個(gè)節(jié)點(diǎn)znode發(fā)生了變化艺普,則觸發(fā)watcher事件(類似觸發(fā)器)
watcher是一次性的簸州,觸發(fā)后以及銷毀
節(jié)點(diǎn)znode自己鉴竭、子孫節(jié)點(diǎn)的創(chuàng)建、刪除岸浑、數(shù)據(jù)修改都能觸發(fā)當(dāng)前節(jié)點(diǎn)的watcher搏存。節(jié)點(diǎn)沒有創(chuàng)建之前也能添加watcher
-
不同類型的操作,觸發(fā)不同的watcher事件矢洲,包括節(jié)點(diǎn)創(chuàng)建璧眠、刪除、數(shù)據(jù)修改事件
創(chuàng)建自身節(jié)點(diǎn)觸發(fā):NodeCreated
修改自身節(jié)點(diǎn)數(shù)據(jù)觸發(fā):NodeDataChanged
刪除自身節(jié)點(diǎn)觸發(fā):NodeDeleted
創(chuàng)建兵钮、刪除子節(jié)點(diǎn)都會(huì)觸發(fā):NodeChildrenChanged
修改子節(jié)點(diǎn)數(shù)據(jù)不會(huì)觸發(fā)watch事件
stat path [watch]:
獲取節(jié)點(diǎn)狀態(tài)信息蛆橡,給節(jié)點(diǎn)添加一次性的watch事件
get path [watch]:
獲取節(jié)點(diǎn)狀態(tài)信息和數(shù)據(jù)信息,給節(jié)點(diǎn)添加一次性的watch事件
ls2 path [watch]:
獲取節(jié)點(diǎn)狀態(tài)信息和子節(jié)點(diǎn)掘譬,給節(jié)點(diǎn)添加一次性的watch事件
下面代碼演示給節(jié)點(diǎn)mywatch添加watch事件泰演,創(chuàng)建、刪除葱轩、數(shù)據(jù)修改節(jié)點(diǎn)mywatch自己會(huì)觸發(fā)哪些類型的watch事件:
# 給不存在的節(jié)點(diǎn)mywatch添加watch事件
[zk: localhost:2181(CONNECTED) 14] stat /mywatch watch
Node does not exist: /mywatch
# 創(chuàng)建節(jié)點(diǎn)mywatch睦焕,觸發(fā)watch事件WatchedEvent,類型是NodeCreated
[zk: localhost:2181(CONNECTED) 15] create /mywatch 123
WATCHER::
Created /mywatch
WatchedEvent state:SyncConnected type:NodeCreated path:/mywatch
# 因?yàn)閣atch事件是一次性的靴拱,所以我們重新添加watch事件
[zk: localhost:2181(CONNECTED) 19] get /mywatch watch
123
# 修改節(jié)點(diǎn)數(shù)據(jù)垃喊,觸發(fā)WatchedEvent,類型是NodeDataChanged
[zk: localhost:2181(CONNECTED) 20] set /mywatch 456
WATCHER::ctime = Thu Feb 06 12:22:27 CST 2020
WatchedEvent state:SyncConnected type:NodeDataChanged path:/mywatchmZxid = 0x7c9
# 再次添加watch事件
[zk: localhost:2181(CONNECTED) 21] get /mywatch watch
456
# 刪除節(jié)點(diǎn)mywatch,觸發(fā)WatchedEvent,類型是NodeDeleted
[zk: localhost:2181(CONNECTED) 22] delete /mywatch
WATCHER::
[zk: localhost:2181(CONNECTED) 23]
WatchedEvent state:SyncConnected type:NodeDeleted path:/mywatch
創(chuàng)建袜炕、刪除子節(jié)點(diǎn)會(huì)觸發(fā)NodeChildrenChanged事件本谜,但修改子節(jié)點(diǎn)數(shù)據(jù)不會(huì)觸發(fā)watch事件
# 創(chuàng)建節(jié)點(diǎn)mywatch
[zk: localhost:2181(CONNECTED) 31] create /mywatch 123
Created /mywatch
# 給節(jié)點(diǎn)添加watch事件
[zk: localhost:2181(CONNECTED) 33] ls /mywatch watch
[]
# 創(chuàng)建mywatch的子節(jié)點(diǎn),觸發(fā)WatchedEvent,類型是NodeChildrenChanged
[zk: localhost:2181(CONNECTED) 34] create /mywatch/cnode 666
WATCHER::Created /mywatch/cnode # 這里是創(chuàng)建的節(jié)點(diǎn)路徑
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/mywatch # 這里是觸發(fā)watch事件的節(jié)點(diǎn)自身
watcher是當(dāng)前客戶端加在節(jié)點(diǎn)znode上的觸發(fā)器,
watcher使用場景
- 統(tǒng)一配置文件管理偎窘。sqlConfig節(jié)點(diǎn)保存json數(shù)據(jù)乌助,即對(duì)配置文件的操作和文件路徑,某個(gè)客戶端對(duì)sqlConfig節(jié)點(diǎn)添加了watch事件陌知,當(dāng)節(jié)點(diǎn)數(shù)據(jù)更新后他托,所有客戶端都能監(jiān)聽到,然后根據(jù)節(jié)點(diǎn)數(shù)據(jù)更新本地配置信息仆葡。<a href="#watchconfig">第7.2章節(jié) </a>詳細(xì)介紹了利用Watch實(shí)現(xiàn)統(tǒng)一配置文件管理赏参。
4.4 ACL權(quán)限控制
ACL(access control lists)
- 針對(duì)節(jié)點(diǎn)可以設(shè)置相關(guān)讀寫權(quán)限,保障數(shù)據(jù)安全性
- 權(quán)限permissions可以指定不同的權(quán)限范圍和角色
- zk的acl通過[schema: id : permissions] 來構(gòu)成權(quán)限列表沿盅;
- schema:權(quán)限機(jī)制把篓,有五種類型:world,auth腰涧,digest纸俭,ip,super
- permissions:創(chuàng)建南窗、刪除揍很、讀郎楼、寫權(quán)限
- id:用戶,permissions:權(quán)限組合字符串
身份認(rèn)證的5種類型schema
world:默認(rèn)方式窒悔,相當(dāng)于全世界都能訪問呜袁,只有一個(gè)id anyone world:anyone:[permissions]
;
auth:代表節(jié)點(diǎn)授權(quán)的用戶 auth:username:password:cdrwa
digest:即用戶名:密碼這種方式認(rèn)證简珠,這也是業(yè)務(wù)系統(tǒng)中最常用的阶界,digest:username:BSE64(SHA1(password)):[permissions]
ip:指定的ip地址才可以訪問,ip:182.168.1.1:[permissions]
super:超級(jí)管理員聋庵,擁有所有權(quán)限膘融,需要修改zkServer.sh
文件
permissions
權(quán)限字符串縮寫crdwa
- create:創(chuàng)建子節(jié)點(diǎn)
- read:獲取節(jié)點(diǎn) / 子節(jié)點(diǎn)信息和數(shù)據(jù)
- delete:刪除子節(jié)點(diǎn)
- write:設(shè)置節(jié)點(diǎn)數(shù)據(jù)
- admin:管理權(quán)限,設(shè)置節(jié)點(diǎn)ACL的權(quán)限
訪問
addauth digest user:pwd
來添加當(dāng)前上下文中的授權(quán)用戶祭玉,auth
和digest
兩種授權(quán)方式均可以通過addauth digest user:pwd
命令(明文密碼)訪問氧映。
登錄后設(shè)置權(quán)限可省略u(píng)sername和password
# 未添加授權(quán)addauth, 設(shè)置ACL失敗
[zk: localhost:2181(CONNECTED) 7] setAcl /myacl auth:mao:mao:crdwa
Acl is not valid : /myacl
# 添加授權(quán)用戶mao:mao
[zk: localhost:2181(CONNECTED) 8] addauth digest mao:mao
[zk: localhost:2181(CONNECTED) 9] setAcl /myacl auth:mao:mao:crwa
aclVersion = 1
[zk: localhost:2181(CONNECTED) 12] setAcl /myacl auth:mao:123456:crdwa
aclVersion = 2
# ACL列表仍然只有 mao:mao, 沒有mao:123456
[zk: localhost:2181(CONNECTED) 13] getAcl /myacl
'digest,'mao:LVVsVUii7a7fmrx8wQgjm3ljkTA=
: crwa
# 省略u(píng)sername和password, 使用當(dāng)前授權(quán)的用戶, 修改權(quán)限為crdwa
[zk: localhost:2181(CONNECTED) 14] setAcl /myacl auth:::crdwa
aclVersion = 3
# 修改成功
[zk: localhost:2181(CONNECTED) 29] getAcl /myacl
'digest,'mao:LVVsVUii7a7fmrx8wQgjm3ljkTA=
: cdrwa
ACL命令行
-
getAcl:
獲取某個(gè)節(jié)點(diǎn)的acl權(quán)限信息,getAcl /imooc/myauth
-
setAcl:
設(shè)置某個(gè)節(jié)點(diǎn)的acl權(quán)限信息脱货,setAcl /imooc/myauth auth:mao:mao:cdrwa
-
addauth:
來添加當(dāng)前上下文中的授權(quán)用戶岛都,addauth digest mao:maos
# 修改myauth節(jié)點(diǎn)的acl權(quán)限為crwa,即無法刪除子節(jié)點(diǎn)
[zk: localhost:2181(CONNECTED) 26] setAcl /imooc/myauth world:anyone:crwa
aclVersion = 1
# 創(chuàng)建myauth的子節(jié)點(diǎn)test
[zk: localhost:2181(CONNECTED) 27] create /imooc/myauth/test 222
Created /imooc/myauth/test
# 刪除myauth子節(jié)點(diǎn)test,發(fā)生權(quán)限錯(cuò)誤
[zk: localhost:2181(CONNECTED) 28] delete /imooc/myauth/test
Authentication is not valid : /imooc/myauth/test
# 添加當(dāng)前上下文中的授權(quán)用戶,相當(dāng)于登錄振峻,否則下面的setAcl命令會(huì)失敗
[zk: localhost:2181(CONNECTED) 31] addauth digest mao:mao
# 使用用戶mao設(shè)置acl權(quán)限, 當(dāng)用戶名密碼不是當(dāng)前用戶mao:mao時(shí)不生效
# 和下行命令等價(jià) setAcl /imooc/myauth auth:::cdrwa
[zk: localhost:2181(CONNECTED) 32] setAcl /imooc/myauth auth:mao:mao:cdrwa
aclVersion = 2
# 查看myauth權(quán)限
[zk: localhost:2181(CONNECTED) 33] getAcl /imooc/myauth
'digest,'mao:LVVsVUii7a7fmrx8wQgjm3ljkTA=
: cdrwa
[zk: localhost:2181(CONNECTED) 34]
# 使用digest設(shè)置acl權(quán)限
[zk: localhost:2181(CONNECTED) 34] setAcl /imooc/myauth digest:mao:LVVsVUii7a7fmrx8wQgjm3ljkTA=:cdra
aclVersion = 3
# 查看myauth權(quán)限,發(fā)現(xiàn)已修改
[zk: localhost:2181(CONNECTED) 35] getAcl /imooc/myauth
'digest,'mao:LVVsVUii7a7fmrx8wQgjm3ljkTA=
: cdra
# 修改節(jié)點(diǎn)數(shù)據(jù),提示權(quán)限不合法
[zk: localhost:2181(CONNECTED) 36] set /imooc/myauth 222
Authentication is not valid : /imooc/myauth
super 權(quán)限設(shè)置
修改zkServer.sh文件臼疫,添加系統(tǒng)屬性“-Dzookeeper.DigestAuthenticationProvider.superDigest=username:BASE64(SHA1(password))”。zk會(huì)讀取該屬性并設(shè)置為super用戶扣孟,源碼如下圖所示
4.5 四字命令
四字命令是在Linux中使用(zkCli無法使用)來zookeeper服務(wù)的當(dāng)前狀態(tài)及相關(guān)信息的烫堤,四字命令的
zk可以通過它自身提供的簡寫命令來和服務(wù)器交互,需要使用到nc
命令凤价,需要使用yum install nc
安裝塔逃,命令格式為 echo [commond] | nc [ip] [port]
- stat:查看zk的狀態(tài)信息和Mode類型
- ruok:查看當(dāng)前zkServer是否啟動(dòng),若啟動(dòng)成功則返回 imok
- dump:列出未經(jīng)處理的會(huì)話和臨時(shí)節(jié)點(diǎn)
- conf:查看服務(wù)配置
- cons:展示連接到服務(wù)器的客戶端信息
- envi:環(huán)境變量
- mntr:監(jiān)控zk健康信息
- wchs:展示watch的詳細(xì)信息
- wchc:通過session列出服務(wù)器watch的詳細(xì)信息料仗,
- wchp:通過路徑列出服務(wù)器 watch的詳細(xì)信息
5 zookeeper集群安裝
6 zookeeper JavaAPI開發(fā)客戶端
-
依賴
使用zookeeper 原生JavaAPI開發(fā)需要引入相應(yīng)的jar包,依賴
pom.xml
如下所示:
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.7</version>
</dependency>
6.1 會(huì)話連接與恢復(fù)(源碼)
建立客戶端與zk服務(wù)端的session連接伏蚊,需要以下三步:
- 需要?jiǎng)?chuàng)建zk對(duì)象立轧,傳入Watcher對(duì)象
- 啟動(dòng)
sendThread
線程與zk服務(wù)端建立連接 - 啟動(dòng)
eventThread
線程,不斷檢查連接是否建立成功躏吊;若成功氛改,則觸發(fā)watch事件,使用Watcher發(fā)送watch通知比伏。
// ZKConnect是Watcher接口的實(shí)現(xiàn)類胜卤,用于發(fā)送watch通知,即調(diào)用Watch.process()方法
ZooKeeper zk = new ZooKeeper(zkServerPath, timeout, new ZKConnect());
客戶端連接zk服務(wù)端代碼如下所示:
// 實(shí)現(xiàn)Watcher接口,用于通知客戶端是否連接成功
public class ZKConnect implements Watcher {
final static Logger log = LoggerFactory.getLogger(ZKConnect.class);
public static final String zkServerPath = "localhost:2181";
// public static final String zkServerPath = "192.168.1.111:2181,192.168.1.111:2182,192.168.1.111:2183";
public static final Integer timeout = 5000;
public static void main(String[] args) throws Exception {
/**
* 客戶端和zk服務(wù)端鏈接是一個(gè)異步的過程
* 當(dāng)連接成功后后赁项,客戶端會(huì)收的一個(gè)watch通知葛躏,即調(diào)用Watch.process()方法
*
* 參數(shù):
* connectString:連接服務(wù)器的ip字符串澈段,多個(gè)ip用逗號(hào)分隔
* sessionTimeout:超時(shí)時(shí)間,心跳收不到了舰攒,那就超時(shí)
* watcher:通知事件败富,如果有對(duì)應(yīng)的事件觸發(fā),則會(huì)收到一個(gè)通知摩窃;如果不需要兽叮,那就設(shè)置為null
* sessionId:會(huì)話的id
* sessionPasswd:會(huì)話密碼 當(dāng)會(huì)話丟失后,可以依據(jù) sessionId 和 sessionPasswd 重新獲取會(huì)話
*/
ZooKeeper zk = new ZooKeeper(zkServerPath, timeout, new ZKConnect());
log.warn("客戶端開始連接zookeeper服務(wù)器...");
log.warn("連接狀態(tài):{}", zk.getState());
// 等待連接線程執(zhí)行完畢
new Thread().sleep(2000);
log.warn("連接狀態(tài):{}", zk.getState());
}
// 連接成功后使用watch事件進(jìn)行通知
@Override
public void process(WatchedEvent event) {
log.warn("接受到watch通知:{}", event);
}
}
會(huì)話恢復(fù)
將之前創(chuàng)建的zk連接會(huì)話的sessionId
和sessionPasswd
保存猾愿,然后利用其創(chuàng)建新的
zk對(duì)象即可恢復(fù)會(huì)話鹦聪,查看完整代碼
ZooKeeper zk = new ZooKeeper(zkServerPath, timeout, new ZKConnectSessionWatcher());
long sessionId = zk.getSessionId();
byte[] sessionPassword = zk.getSessionPasswd();
log.warn("客戶端開始連接zookeeper服務(wù)器...");
log.warn("連接狀態(tài):{}", zk.getState());
new Thread().sleep(1000);
log.warn("連接狀態(tài):{}", zk.getState());
new Thread().sleep(200);
// 開始會(huì)話重連,使用之前保存的sessionId和password創(chuàng)建新的連接
ZooKeeper zkSession = new ZooKeeper(zkServerPath,
timeout,
new ZKConnectSessionWatcher(),
sessionId,
sessionPassword);
6.2 節(jié)點(diǎn)增刪改查
創(chuàng)建節(jié)點(diǎn)有同步、異步兩種形式蒂秘,是重載的create
方法:
同步創(chuàng)建有返回值泽本,成功返回節(jié)點(diǎn)路徑,失敗拋出異常
KeeperException
異步創(chuàng)建無返回值材彪,成功調(diào)用參數(shù)中的回調(diào)方法
StringCallback.processResult()
观挎,方法內(nèi)容可以自己實(shí)現(xiàn),也可以根據(jù)ctx
執(zhí)行不同的操作都不支持節(jié)點(diǎn)的遞歸創(chuàng)建
// 同步創(chuàng)建段化,path,data,acl與命令create一致, createmode是-s序列 -e臨時(shí)節(jié)點(diǎn)的結(jié)合體
public String create(final String path, byte data[], List<ACL> acl, CreateMode createMode) throws KeeperException, InterruptedException
// 異步創(chuàng)建嘁捷,StringCallback是創(chuàng)建成功后的回調(diào)函數(shù), ctx是成功后的返回信息,一般為json
public void create(final String path, byte data[], List<ACL> acl, CreateMode createMode, StringCallback cb, Object ctx)
同步創(chuàng)建節(jié)點(diǎn)
// 如果創(chuàng)建失敗會(huì)拋出異常KeeperException
@Test
public void createNode() throws KeeperException, InterruptedException {
/**
* 同步或者異步創(chuàng)建節(jié)點(diǎn),都不支持子節(jié)點(diǎn)的遞歸創(chuàng)建显熏,異步有一個(gè)callback函數(shù)
* 參數(shù):
* path:創(chuàng)建的路徑
* data:存儲(chǔ)的數(shù)據(jù)的byte[]
* acl:控制權(quán)限策略
* Ids.OPEN_ACL_UNSAFE --> world:anyone:cdrwa
* CREATOR_ALL_ACL --> auth:user:password:cdrwa
* createMode:節(jié)點(diǎn)類型, 是一個(gè)枚舉
* PERSISTENT:持久節(jié)點(diǎn)
* PERSISTENT_SEQUENTIAL:持久順序節(jié)點(diǎn)
* EPHEMERAL:臨時(shí)節(jié)點(diǎn)
* EPHEMERAL_SEQUENTIAL:臨時(shí)順序節(jié)點(diǎn)
*/
String result = zookeeper.create("/testnode", "123".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
log.warn("創(chuàng)建" + result + "成功");
}
異步創(chuàng)建節(jié)點(diǎn)
@Test
public void createNodeAsync() throws InterruptedException {
String ctx = "{'create':'success'}";
// 因?yàn)槭钱惒?創(chuàng)建成功后調(diào)用StringCallback.processResult()
zookeeper.create("/testnode3/abc", "123".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT, new AsyncCallback.StringCallback() {
@Override
public void processResult(int rc, String path, Object ctx, String name) {
System.out.println("創(chuàng)建節(jié)點(diǎn): " + path);
System.out.println((String)ctx);
}
}, ctx);
Thread.sleep(2000);
}
設(shè)置節(jié)點(diǎn)數(shù)據(jù)
// 版本號(hào)錯(cuò)誤會(huì)拋出KeeperException: Badversion for /node
@Test
public void setData() throws KeeperException, InterruptedException {
/**
* 參數(shù):
* path:節(jié)點(diǎn)路徑
* data:數(shù)據(jù)
* version:數(shù)據(jù)版本
* 返回值Stat等價(jià)于stat命令, 返回節(jié)點(diǎn)狀態(tài)信息
*/
Stat status = zookeeper.setData("/testnode", "666".getBytes(), 1);
System.out.println(status.getVersion());
}
刪除節(jié)點(diǎn)
@Test
public void deleteNodeAsync() throws KeeperException, InterruptedException {
String result = "{'delete':'success'}";
zookeeper.delete("/testnode", 2, new AsyncCallback.VoidCallback() {
@Override
public void processResult(int rc, String path, Object ctx) {
System.out.println("刪除節(jié)點(diǎn)" + path);
System.out.println((String)ctx);
}
}, result);
}
查詢節(jié)點(diǎn)數(shù)據(jù)
/**
* 獲取節(jié)點(diǎn)數(shù)據(jù), 等價(jià)于命令 get path [watch]
* Stat保存節(jié)點(diǎn)狀態(tài)信息, data保存節(jié)點(diǎn)數(shù)據(jù)
* watch=false表示不添加監(jiān)聽,為true表示添加監(jiān)聽,監(jiān)聽事件在watch的`process`中觸發(fā)
*/
@Test
public void getNodeData() throws KeeperException, InterruptedException {
Stat status = new Stat();
byte[] data = zookeeper.getData("/imooc", false, status);
System.out.println("節(jié)點(diǎn)數(shù)據(jù):" + new String(data));
}
獲取子節(jié)點(diǎn)列表
/**
* 獲取子節(jié)點(diǎn)列表
* stat用于獲得當(dāng)前節(jié)點(diǎn)狀態(tài)信息
*/
@Test
public void getChildrenNode() throws KeeperException, InterruptedException {
Stat status = new Stat();
List<String> children = zookeeper.getChildren("/imooc", false, status);
children.forEach(e -> System.out.println(e));
}
判斷節(jié)點(diǎn)是否存在
@Test
public void nodeExist() throws KeeperException, InterruptedException {
Stat status = zookeeper.exists("/imooc", false);
if(status == null) {
System.out.println("當(dāng)前節(jié)點(diǎn)不存在");
}else {
System.out.println("當(dāng)前節(jié)點(diǎn)存在雄嚣,dataVersion:" + status.getVersion());
}
}
6.3 watch與acl
7 Apache Curator
Apache Curator也是一款開源的zookeeper客戶端Java API,企業(yè)常用于操作zookeeper喘蟆。API簡單易用营密,提供常用的工具類,提供了分布式鎖解決方案臀突,并且解決了原生API的三個(gè)問題:
- 超時(shí)重連贸营,需要手動(dòng)重連
- watch注冊(cè)后,一次觸發(fā)就會(huì)失效
- 不支持遞歸創(chuàng)建節(jié)點(diǎn)
7.1 使用Curator操作zk
- 引入依賴橙弱,
pom.xml
需要引入Curator依賴
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.0</version>
</dependency>
- 創(chuàng)建客戶端歧寺,設(shè)置重試策略
/**
* 同步創(chuàng)建zk示例,原生api是異步的
*
* curator鏈接zookeeper的策略:ExponentialBackoffRetry
* baseSleepTimeMs:初始sleep的時(shí)間
* maxRetries:最大重試次數(shù)
* maxSleepMs:最大重試時(shí)間
*/
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
/**
* 獲取zk客戶端棘脐,需要傳入zk地址斜筐,超時(shí)時(shí)間,重試策略蛀缝,命名空間
* namespace,該客戶端即所有增刪改查操作的節(jié)點(diǎn)路徑前面都會(huì)加上 /workspace
*/
client = CuratorFrameworkFactory.builder()
.connectString(zkServerPath)
.sessionTimeoutMs(10000).retryPolicy(retryPolicy)
.namespace("workspace").build();
client.start();
- 檢查客戶端連接狀態(tài)顷链,是否啟動(dòng),測試關(guān)閉客戶端
/**
* 獲取客戶端的連接狀態(tài), 關(guān)閉會(huì)話連接
* 用于替代過時(shí)方法isStarted()
*/
@Test
public void getzkStatus() throws InterruptedException {
boolean isZkCuratorStarted = client.getState() == CuratorFrameworkState.STARTED;
System.out.println("當(dāng)前客戶的狀態(tài):" + (isZkCuratorStarted ? "連接中" : "已關(guān)閉"));
Thread.sleep(3000);
client.close();
boolean isZkCuratorStarted2 = client.getState() == CuratorFrameworkState.STARTED;
System.out.println("當(dāng)前客戶的狀態(tài):" + (isZkCuratorStarted2 ? "連接中" : "已關(guān)閉"));
}
- 操作節(jié)點(diǎn)
- 創(chuàng)建節(jié)點(diǎn)屈梁、
- 刪除節(jié)點(diǎn)嗤练、
- 設(shè)置節(jié)點(diǎn)數(shù)據(jù)榛了、
- 獲取節(jié)點(diǎn)數(shù)據(jù)和狀態(tài)信息、
- 獲取子節(jié)點(diǎn)列表潭苞,
- 判斷節(jié)點(diǎn)是否存在
/**
* 創(chuàng)建節(jié)點(diǎn)
* <p>
* client的命名空間是/workspace, 即所有增刪改查的操作的節(jié)點(diǎn)路徑前面都會(huì)加上 /workspace
* 會(huì)在第一次創(chuàng)建節(jié)點(diǎn)時(shí)自動(dòng)創(chuàng)建父節(jié)點(diǎn)/workspace
* 如果節(jié)點(diǎn)已存在拋出異常KeeperException$NodeExistsException: KeeperErrorCode = NodeExists for /workspace/curator/imooc
*/
@Test
public void createNode() throws Exception {
byte[] data = "abc".getBytes();
String nodePath = "/curator/imooc";
String path = client.create().creatingParentsIfNeeded() // 如果父節(jié)點(diǎn)不存在,創(chuàng)建父節(jié)點(diǎn)
.withMode(CreateMode.PERSISTENT) // 設(shè)置節(jié)點(diǎn)-s -t 臨時(shí),序列界定啊
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE) // 設(shè)置用戶控制權(quán)限
.forPath(nodePath, data);
System.out.println(path + "節(jié)點(diǎn)創(chuàng)建成功");
}
/**
* 設(shè)置節(jié)點(diǎn)數(shù)據(jù), 可設(shè)置dataVersion
* @throws Exception
*/
@Test
public void setData() throws Exception {
byte[] data = "123".getBytes();
String nodePath = "/curator/imooc";
Stat stat = client.setData()
// .withVersion(2) // 設(shè)置版本號(hào),可省略, 若版本號(hào)錯(cuò)誤拋出異常
.forPath(nodePath, data);
System.out.println("dataVersion" + stat.getVersion());
}
/**
* 刪除節(jié)點(diǎn), 版本號(hào)可省略
* 如果節(jié)點(diǎn)不存在會(huì)拋出異常KeeperException$NoNodeException: KeeperErrorCode = NoNode for /workspace/curator
*/
@Test
public void deleteNode() throws Exception {
String nodePath = "/curator";
client.delete()
.guaranteed() // 如果刪除失敗,那么后端會(huì)繼續(xù)刪除,直至成功
.deletingChildrenIfNeeded() // 如果存在子節(jié)點(diǎn),就刪
// .withVersion(2)
.forPath(nodePath);
}
/**
* 獲取節(jié)點(diǎn)數(shù)據(jù)和狀態(tài)信息
* 狀態(tài)信息保存在Stat中, 數(shù)據(jù)保存在data中
*
* @throws Exception
*/
@Test
public void getNode() throws Exception {
String nodePath = "/curator/imooc";
Stat stat = new Stat();
byte[] data = client.getData()
.storingStatIn(stat) // 保存節(jié)點(diǎn)狀態(tài)信息
.forPath(nodePath);
System.out.println(new String(data));
System.out.println(stat.toString());
}
/**
* 獲取所有子節(jié)點(diǎn)名稱
*
* @throws Exception
*/
@Test
public void getChildrenNode() throws Exception {
List<String> nodes = client.getChildren().forPath("/curator");
nodes.forEach((n) -> System.out.println(n));
}
/**
* 判斷節(jié)點(diǎn)是否存在
*/
@Test
public void nodeExist() throws Exception {
Stat stat = client.checkExists().forPath("/aaa");
if (stat == null) {
System.out.println("節(jié)點(diǎn)不存在");
} else {
System.out.println("節(jié)點(diǎn)存在" + stat);
}
}
7.3 設(shè)置Watch事件
-
設(shè)置一次失效的
watcher
事件/** * 對(duì)節(jié)點(diǎn)設(shè)置watcher, 觸發(fā)一次后失效 */ @Test public void setWatcher() throws Exception { CountDownLatch latch = new CountDownLatch(2); client.getData().usingWatcher(new CuratorWatcher() { @Override public void process(WatchedEvent watchedEvent) throws Exception { System.out.println(watchedEvent.getPath() + "觸發(fā)watcher事件: " + watchedEvent.getType()); // 只會(huì)執(zhí)行一次 latch.countDown(); } }).forPath("/curator/imooc"); // 等待操作cmd客戶端(set /workspace/curator/imooc 666), 觸發(fā)監(jiān)聽器, 回調(diào)process方法 // 修改節(jié)點(diǎn)數(shù)據(jù), 觸發(fā)一次后失效, 所以程序永遠(yuǎn)不會(huì)結(jié)束 latch.await(); }
一次注冊(cè)N次監(jiān)聽的
wacher
事件忽冻,不區(qū)分watch事件類型,不監(jiān)聽子節(jié)點(diǎn)NodeChildrenChanged事件
/**
* 利用nodeCache和Listener設(shè)置watch事件
* 一次注冊(cè),N次監(jiān)聽
* 缺點(diǎn)是多種類型Watch事件(NodeCreated, NodeDataChanged,NodeDeleted)都被稱為NodeChanged, 但是不監(jiān)聽NodeChildrenChanged事件
* @throws Exception
*/
@Test
public void setWatcherByNodeCache() throws Exception {
// 這次的監(jiān)聽器一直有效, 所以設(shè)置為5
CountDownLatch latch = new CountDownLatch(5);
NodeCache nodeCache = new NodeCache(client, "/curator/imooc");
nodeCache.start(true); // true啟動(dòng)時(shí)緩存當(dāng)前節(jié)點(diǎn), false啟動(dòng)時(shí)不緩存節(jié)點(diǎn)
if (nodeCache.getCurrentData() == null) {
System.out.println("節(jié)點(diǎn)初始化數(shù)據(jù)為空");
} else {
String data = new String(nodeCache.getCurrentData().getData());
System.out.println("節(jié)點(diǎn)初始化數(shù)據(jù)為: " + data);
}
// 添加監(jiān)聽器, 等待節(jié)點(diǎn)被修改觸發(fā)監(jiān)聽器,執(zhí)行nodeChanged方法
nodeCache.getListenable().addListener(new NodeCacheListener() {
@Override
public void nodeChanged() throws Exception {
// 節(jié)點(diǎn)刪除或不存在
if(nodeCache.getCurrentData() == null) {
System.out.println("節(jié)點(diǎn)不存在");
}
// nodeCache.getCurrentData()是獲取節(jié)點(diǎn)對(duì)象ChildData,
// ChildData可以獲取節(jié)點(diǎn)路徑,數(shù)據(jù),狀態(tài)信息stat
String data = new String(nodeCache.getCurrentData().getData());
System.out.println("節(jié)點(diǎn)" + nodeCache.getCurrentData().getPath() + " 數(shù)據(jù)為:" + data );
latch.countDown();
}
});
// 使主程序不結(jié)束, 等待cmd客戶端修改節(jié)點(diǎn)觸發(fā)監(jiān)聽器(set /workspace/curator/imooc 777)
// 一次注冊(cè),N次監(jiān)聽, 修改節(jié)點(diǎn)數(shù)據(jù)5次, 觸發(fā)10次watch事件后程序結(jié)束
latch.await();
}
- 設(shè)置區(qū)分事件類型Watch事件此疹,一次注冊(cè)僧诚,N次監(jiān)聽,區(qū)分事件類型蝗碎。因?yàn)?code>PathChildrenCache監(jiān)聽子節(jié)點(diǎn)湖笨,所以我們一般都設(shè)置為目標(biāo)節(jié)點(diǎn)的父節(jié)點(diǎn),然后在回調(diào)函數(shù)中篩選出目標(biāo)節(jié)點(diǎn)蹦骑。
/**
* 監(jiān)聽節(jié)點(diǎn), 需要異步初始化PathChildrenCache觸發(fā)監(jiān)聽
*
* @throws Exception
*/
@Test
public void setWatchsByPathChildrenCache() throws Exception {
// 設(shè)置需要監(jiān)聽的節(jié)點(diǎn)
String nodePath = "/curator/imooc";
// PathChildrenCache是監(jiān)聽所有子節(jié)點(diǎn), 所以設(shè)置為"/curator/imooc"的父節(jié)點(diǎn)/curator
PathChildrenCache childCache = new PathChildrenCache(client, "/curator", true);
/*
* StartMode: 初始化方式
* POST_INITIALIZED_EVENT:異步初始化慈省,初始化之后會(huì)觸發(fā)事件
* NORMAL:異步初始化
* BUILD_INITIAL_CACHE:同步初始化
*/
childCache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
// 注意這里獲取的是子節(jié)點(diǎn)的數(shù)據(jù),不是名稱
List<ChildData> childDataList = childCache.getCurrentData();
for (ChildData data : childDataList) {
System.out.println(new String(data.getData()));
}
childCache.getListenable().addListener(new PathChildrenCacheListener() {
/**
* @param curatorFramework 就是client, 可以根據(jù)監(jiān)聽事件操作節(jié)點(diǎn), 比如監(jiān)聽到a節(jié)點(diǎn)修改了數(shù)據(jù), 那b節(jié)點(diǎn)就刪除client.delete().forPath("/b")
* @param event 監(jiān)聽事件, 可以得到事件類型,節(jié)點(diǎn)名稱,節(jié)點(diǎn)數(shù)據(jù)等
*/
@Override
public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent event) throws Exception {
String path = event.getData().getPath();
// 由于PathChildrenCache監(jiān)聽/curator的所有子節(jié)點(diǎn),而我們只關(guān)心/curator/imooc, 所以使用衛(wèi)語句進(jìn)行排除
if (!nodePath.equals(path)) {
return;
}
processEvent(event);
}
});
}
private void processEvent(PathChildrenCacheEvent event) {
ChildData node = event.getData();
switch (event.getType()) {
case INITIALIZED:
System.out.println("子節(jié)點(diǎn)初始化完成...");
break;
case CHILD_ADDED: // 如果子節(jié)點(diǎn)已經(jīng)創(chuàng)建, 則在啟動(dòng)時(shí)會(huì)觸發(fā)該事件
System.out.println("創(chuàng)建子節(jié)點(diǎn):" + node.getPath());
System.out.println("子節(jié)點(diǎn)數(shù)據(jù):" + new String(node.getData()));
break;
case CHILD_UPDATED:
System.out.println("修改子節(jié)點(diǎn):" + node.getPath());
System.out.println("修改子節(jié)點(diǎn)數(shù)據(jù):" + new String(node.getData()));
break;
case CHILD_REMOVED:
System.out.println("刪除子節(jié)點(diǎn):" + node.getPath());
break;
default:
System.out.println("觸發(fā)Watch事件,類型為:" + event.getType());
}
}
7.2 <a name="watchconfig">統(tǒng)一配置文件管理</a>
統(tǒng)一配置文件管理的原理是利用watch
事件,比如為了同步redis配置文件到redis集群
- 在zk上創(chuàng)建
redisConfig
節(jié)點(diǎn) - 所有redis集群機(jī)器上都啟動(dòng)zk的 Java 客戶端眠菇,并對(duì)
redisConfig
節(jié)點(diǎn)設(shè)置watch
事件 - 運(yùn)維人員使用命令行修改redisConfig節(jié)點(diǎn)的數(shù)據(jù)边败,
set /workspace/conf/redis-config {"type":"update","url":"ftp://192.168.10.123/config/redis.xml"}
- 所有zk客戶端監(jiān)聽到
DataChanged
事件,查看節(jié)點(diǎn)數(shù)據(jù)捎废,解析數(shù)據(jù)后可知需要對(duì)redis配置文件的操作為update
笑窜,配置文件地址為url
。 - 根據(jù)文件地址
url
下載redis配置文件登疗,替換原有的配置文件排截,重啟服務(wù)即可。
查看客戶端代碼
/**
* 統(tǒng)一配置文件管理的原理是利用watch事件辐益,比如為了同步redis配置文件到redis集群
*
* 每臺(tái)機(jī)器上都執(zhí)行該類的main方法, 即啟動(dòng)zk客戶端.
*/
public class Client1 {
public static CuratorFramework client = null;
public static final String zkServerPath = "localhost:2181";
static {
RetryPolicy retryPolicy = new RetryNTimes(3, 5000);
client = CuratorFrameworkFactory.builder()
.connectString(zkServerPath)
.sessionTimeoutMs(10000).retryPolicy(retryPolicy)
.namespace("workspace").build();
client.start();
}
public final static String CONFIG_NODE = "/conf/redis-config";
public final static String CONFIG_NODE_PATH = "/conf";
public static CountDownLatch countDown = new CountDownLatch(10);
public static void main(String[] args) throws Exception {
System.out.println("client1 啟動(dòng)成功...");
final PathChildrenCache childrenCache = new PathChildrenCache(client, CONFIG_NODE_PATH, true);
childrenCache.start(StartMode.BUILD_INITIAL_CACHE);
// 添加監(jiān)聽事件
childrenCache.getListenable().addListener(new PathChildrenCacheListener() {
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
if (event.getData() == null) {
return;
}
String path = event.getData().getPath();
// 只對(duì)/conf/redis-config節(jié)點(diǎn)的變化進(jìn)行處理
if (!CONFIG_NODE.equals(path)) {
return;
}
processEvent(event);
}
});
countDown.await();
client.close();
}
public static void processEvent(PathChildrenCacheEvent event) throws InterruptedException {
// 只監(jiān)聽/redic-config節(jié)點(diǎn)變化的事件, 不監(jiān)聽創(chuàng)建断傲、刪除節(jié)點(diǎn)事件
if (!PathChildrenCacheEvent.Type.CHILD_UPDATED.equals(event.getType())) {
System.out.println(event.getType());
return;
}
// 讀取節(jié)點(diǎn)數(shù)據(jù)
String jsonConfig = new String(event.getData().getData());
System.out.println("節(jié)點(diǎn)" + CONFIG_NODE_PATH + "的數(shù)據(jù)為: " + jsonConfig);
if (jsonConfig.isEmpty()) {
System.out.println("配置文件json為空, 請(qǐng)重新輸入");
}
JSONObject obj = JSON.parseObject(jsonConfig);//將json字符串轉(zhuǎn)換為json對(duì)象
String type = obj.getString("type");
String url = obj.getString("url");
// 判斷操作類型,修改配置文件
switch (type) {
case "add":
System.out.println("監(jiān)聽到新增的配置,文件路徑為<"+ url + ">, 準(zhǔn)備下載...");
Thread.sleep(500);
System.out.println("下載成功智政,已將配置文件添加到項(xiàng)目中");
break;
case "update":
System.out.println("監(jiān)聽到新增的配置认罩,文件路徑為<"+ url + ">, 準(zhǔn)備下載...");
Thread.sleep(500);
System.out.println("下載成功,已將配置文件替換到項(xiàng)目中");
break;
case "delete":
System.out.println("監(jiān)聽到需要?jiǎng)h除配置");
Thread.sleep(100);
System.out.println("成功刪除項(xiàng)目中原配置文件");
break;
default:
System.out.println("無法識(shí)別操作類型:" + type);
}
// TODO 視情況統(tǒng)一重啟服務(wù)
}
}
7.3 acl權(quán)限操作與認(rèn)證授權(quán)
- 創(chuàng)建節(jié)點(diǎn)時(shí)設(shè)置ACL權(quán)限
/**
* 創(chuàng)建節(jié)點(diǎn)時(shí)設(shè)置acl權(quán)限
* 創(chuàng)建成功后使用命令行查看節(jié)點(diǎn)權(quán)限
* getAcl /workspace/curator/imooc/myacl
* 'digest,'imooc1:ee8R/pr2P4sGnQYNGyw2M5S5IMU=
* : cdrwa
* 'digest,'imooc2:Ux2+KXVIAs1OI24TQ/0A9Yh0/QU=
* : rw
*
* @throws Exception
*/
@Test
public void createAcl() throws Exception {
String nodePath = "/curator/imooc/myacl";
List<ACL> acls = new ArrayList<>();
Id imooc1 = new Id("digest", getDigestUserPwd("imooc1:123456"));
Id imooc2 = new Id("digest", getDigestUserPwd("imooc2:666666"));
// 用戶imooc1擁有所有權(quán)限, imooc2擁有讀寫權(quán)限
acls.add(new ACL(ZooDefs.Perms.ALL, imooc1));
acls.add(new ACL(ZooDefs.Perms.READ | ZooDefs.Perms.WRITE, imooc2));
// 創(chuàng)建節(jié)點(diǎn)
client.create()
.creatingParentsIfNeeded()
// 設(shè)置節(jié)點(diǎn)的acl權(quán)限, false表示不對(duì)父節(jié)點(diǎn)/curator/imooc/生效, 當(dāng)父節(jié)點(diǎn)已存在時(shí),true也不生效
.withACL(acls, false)
.forPath(nodePath, "123".getBytes());
}
public static String getDigestUserPwd(String id) {
String digest = "";
try {
digest = DigestAuthenticationProvider.generateDigest(id);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return digest;
}
執(zhí)行完上面的代碼续捂,創(chuàng)建節(jié)點(diǎn)成功后垦垂,使用命令行查看acl權(quán)限。
# imooc1擁有全部權(quán)限, imooc2擁有讀寫權(quán)限疾忍,與代碼設(shè)置一直
[zk: localhost:2181(CONNECTED) 19] getAcl /workspace/curator/imooc/myacl
'digest,'imooc1:ee8R/pr2P4sGnQYNGyw2M5S5IMU=
: cdrwa
'digest,'imooc2:Ux2+KXVIAs1OI24TQ/0A9Yh0/QU=
: rw
# 不登錄操作節(jié)點(diǎn), 提示權(quán)限不合法
[zk: localhost:2181(CONNECTED) 20] set /workspace/curator/imooc/myacl 000
Authentication is not valid : /workspace/curator/imooc/myacl
- 修改具有ACL權(quán)限控制節(jié)點(diǎn)的數(shù)據(jù)
- 使用用戶登錄并創(chuàng)建客戶端
- 修改節(jié)點(diǎn)數(shù)據(jù)
- 重新設(shè)置節(jié)點(diǎn)ACL權(quán)限(需要用戶具有admin權(quán)限)
/**
* 獲取權(quán)限限制的節(jié)點(diǎn)數(shù)據(jù), 重新設(shè)置節(jié)點(diǎn)ACL
* @throws Exception
*/
@Test
public void getDataAndSetAcl() throws Exception {
String nodePath = "/curator/imooc/myacl";
// 上個(gè)方法中設(shè)置了myacl的權(quán)限, client沒有登錄, 所以無法修改數(shù)據(jù), 會(huì)拋出異常KeeperException$NoAuthException
//client.setData().forPath(nodePath, "aaa".getBytes());
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
// 使用用戶imooc1登錄客戶端, imooc1具有管理權(quán)限
CuratorFramework authClient = CuratorFrameworkFactory.builder()
.connectString(zkServerPath)
.authorization("digest", "imooc1:123456".getBytes())
.sessionTimeoutMs(10000).retryPolicy(retryPolicy)
.namespace("workspace").build();
authClient.start();
authClient.setData().forPath(nodePath, "aaa".getBytes());
/*
* 修改數(shù)據(jù)后使用以下兩個(gè)命令在Cli查看數(shù)據(jù)是否修改成功
* addauth digest imooc2:666666
* get /workspace/curator/imooc/myacl
*/
// 設(shè)置節(jié)點(diǎn)的ACL權(quán)限,imooc2有了刪除權(quán)限. 注意這里是重新設(shè)置, 而不是添加權(quán)限
List<ACL> acls = new ArrayList<>();
Id imooc1 = new Id("digest", getDigestUserPwd("imooc1:123456"));
Id imooc2 = new Id("digest", getDigestUserPwd("imooc2:666666"));
acls.add(new ACL(ZooDefs.Perms.ALL, imooc1));
acls.add(new ACL(ZooDefs.Perms.READ | ZooDefs.Perms.WRITE | ZooDefs.Perms.DELETE, imooc2));
authClient.setACL().withACL(acls).forPath(nodePath);
// Cli使用 getAcl /workspace/curator/imooc/myacl 查看節(jié)點(diǎn)權(quán)限
}
8 zookeeper 實(shí)現(xiàn)原理
8.1 為dubbo提供動(dòng)態(tài)的服務(wù)注冊(cè)和發(fā)現(xiàn)
dubbo無法動(dòng)態(tài)注冊(cè)和發(fā)現(xiàn)
比如項(xiàng)目中有多個(gè)訂單服務(wù),每個(gè)服務(wù)都是一臺(tái)機(jī)器床三,每個(gè)客戶端(這是Order請(qǐng)求的客戶端一罩,不是zk客戶端)都有一份服務(wù)提供者列表。
高并發(fā)時(shí)需要添加多臺(tái)機(jī)器或服務(wù)down掉了撇簿,服務(wù)的提供者發(fā)生了變化聂渊,結(jié)果客戶端并不知道差购。
要想得到最新的服務(wù)提供者的URL列表,必須得手工更新配置文件才行汉嗽,確實(shí)很不方便欲逃。
這就是客戶端和服務(wù)提供者的緊耦合,想解除這個(gè)耦合饼暑,非得增加一個(gè)中間層不可稳析。
zookeeper注冊(cè)中心
所以應(yīng)該有個(gè)注冊(cè)中心,首先給這些服務(wù)命名(例如orderService)弓叛,其次那些新增OrderService 都可以在這里注冊(cè)一下彰居,客戶端就到這里來查詢,只需要給出名稱orderService撰筷,注冊(cè)中心就可以給出一個(gè)可以使用的url陈惰, 再也不怕服務(wù)提供者的動(dòng)態(tài)增減了。
zookeeper就可以充當(dāng)上文中的注冊(cè)中心毕籽,創(chuàng)建節(jié)點(diǎn)/orderService
抬闯,提供訂單服務(wù)的機(jī)器需要啟動(dòng)一個(gè)zk客戶端,注冊(cè)一個(gè)node1
節(jié)點(diǎn)关筒,節(jié)點(diǎn)數(shù)據(jù)保存服務(wù)的url
/orderService 表達(dá)了一個(gè)服務(wù)的概念溶握, 下面的每個(gè)節(jié)點(diǎn)表示了一個(gè)服務(wù)的實(shí)例。 例如/orderService/node2表示的orderService 的第二個(gè)實(shí)例平委, 每個(gè)節(jié)點(diǎn)上可以記錄下該實(shí)例的url , 這樣就可以查詢了奈虾。
當(dāng)然這個(gè)注冊(cè)中心必須得能和各個(gè)服務(wù)實(shí)例通信,如果某個(gè)服務(wù)實(shí)例不幸down掉了廉赔,那它在樹結(jié)構(gòu)中對(duì)于的節(jié)點(diǎn)也必須刪除肉微,這樣客戶端就查詢不到了。
注冊(cè)中心zookeeper就是和各個(gè)服務(wù)實(shí)例
node
之間建立Session蜡塌,讓各個(gè)服務(wù)實(shí)例的zk客戶端定時(shí)發(fā)送心跳碉纳,如果過了特定時(shí)間收不到心跳,就認(rèn)為這個(gè)服務(wù)實(shí)例node掛掉了馏艾,Session 過期劳曹, 把它從樹形結(jié)構(gòu)中刪除。
8.2 用于實(shí)現(xiàn)分布式鎖
同一個(gè)進(jìn)程中琅摩,多個(gè)線程訪問共享資源铁孵,可以使用Java提供的synchronized等鎖就可以實(shí)現(xiàn)安全訪問,但是在分布式系統(tǒng)中房资,程序都跑在不同機(jī)器的不同進(jìn)程中蜕劝,多個(gè)系統(tǒng)(進(jìn)程)訪問共享資源,就需要一個(gè)分布式鎖
和synchronized一樣,保證一個(gè)資源只能同時(shí)被一個(gè)節(jié)點(diǎn)搶到即可岖沛。誰能搶先在zookeeper創(chuàng)建一個(gè)/distribute_lock
的節(jié)點(diǎn)就表示搶到這個(gè)鎖了暑始,然后讀寫資源,讀寫完以后就把/distribute_lock
節(jié)點(diǎn)刪除婴削,其他進(jìn)程再來搶廊镜。
這樣存在一個(gè)缺點(diǎn),某個(gè)系統(tǒng)可能會(huì)多次搶到唉俗,不太公平嗤朴。
可以讓這些系統(tǒng)在注冊(cè)中心zookeeper的/distribute_lock下都創(chuàng)建順序節(jié)點(diǎn),會(huì)自動(dòng)給每個(gè)節(jié)點(diǎn)一個(gè)編號(hào)互躬,會(huì)是這個(gè)樣子:
然后各個(gè)系統(tǒng)去檢查自己的編號(hào)播赁,誰的編號(hào)小就認(rèn)為誰持有了鎖, 例如系統(tǒng)1吼渡。
系統(tǒng)1持有了鎖容为,就可以對(duì)共享資源進(jìn)行操作了, 操作完成以后process_01這個(gè)節(jié)點(diǎn)刪除寺酪, 再創(chuàng)建一個(gè)新的節(jié)點(diǎn)(編號(hào)變成process_04了):
其他系統(tǒng)一看坎背,編號(hào)為01的刪除了,再看看誰是最小的吧寄雀,是process_02得滤,那就認(rèn)為系統(tǒng)2持有了鎖,可以對(duì)共享資源操作了盒犹。 操作完成以后也要把process_02節(jié)點(diǎn)刪除懂更,創(chuàng)建新的節(jié)點(diǎn)。這時(shí)候process_03就是最小的了急膀,可以持有鎖了沮协。
8.3 zookeeper 高可用
服務(wù)注冊(cè)于發(fā)現(xiàn)和分布式鎖的例子,都加入了一個(gè)中間層zookeeper卓嫂,但是引入了一個(gè)重要的問題:
? 如果zookeeper掛掉慷暂,所有服務(wù)都依賴于zookeeper,那么就無法注冊(cè)服務(wù)和發(fā)現(xiàn)服務(wù)晨雳,也無法獲取分布式鎖了行瑞,所以必須保證注冊(cè)中心zookeeper的高可用。
為了實(shí)現(xiàn)高可用餐禁,zookeeper維護(hù)了一個(gè)集群血久,一主多從結(jié)構(gòu),如下圖所示:
zookeeper會(huì)從Server集群選舉出一個(gè)Leader
節(jié)點(diǎn)(這里的節(jié)點(diǎn)是指服務(wù)器帮非,不是 Znode)氧吐,用于接收寫/讀請(qǐng)求绷旗。更新數(shù)據(jù)時(shí),首先更新到Leader
副砍,再同步到follwer
。
Server集群其他均為follwer
庄岖,用于接收讀請(qǐng)求豁翎,直接從當(dāng)前follower Server讀取。但是又出現(xiàn)了主從數(shù)據(jù)一致性問題隅忿。
如何保證zookeeper主從節(jié)點(diǎn)(Leader和follower)的數(shù)據(jù)一致性呢心剥?
為了保證主從節(jié)點(diǎn)的數(shù)據(jù)一致性,ZooKeeper 采用了 ZAB 協(xié)議(Zookeeper Atomic Broadcast)背桐,這種協(xié)議非常類似于一致性算法 Paxos 和 Raft优烧。 ZAB 協(xié)議所定義的三種節(jié)點(diǎn)狀態(tài):
Looking:選舉狀態(tài)。
Leading:Leader 節(jié)點(diǎn)(主節(jié)點(diǎn))所處狀態(tài)链峭。
Following:Follower 節(jié)點(diǎn)(從節(jié)點(diǎn))所處的狀態(tài)畦娄。
zk客戶端會(huì)隨機(jī)的鏈接到 zookeeper 集群中的一個(gè)Leader
或follower
節(jié)點(diǎn),如果是讀請(qǐng)求弊仪,就直接從當(dāng)前節(jié)點(diǎn)中讀取數(shù)據(jù)熙卡;如果是寫請(qǐng)求,那么節(jié)點(diǎn)就會(huì)向 Leader
提交事務(wù)励饵,Leader
接收到事務(wù)提交驳癌,會(huì)廣播該事務(wù),只要超過半數(shù)節(jié)點(diǎn)寫入成功役听,該事務(wù)就會(huì)被提交颓鲜,每一個(gè)事務(wù)都會(huì)使用zxid
持久化到日志中,用于zk崩潰時(shí)恢復(fù)節(jié)點(diǎn)典予。
另外甜滨,Zookeeper是一個(gè)樹形結(jié)構(gòu),具有順序性很多操作都要先檢查才能確定是否可以執(zhí)行熙参,比如P1的事務(wù)t1可能是創(chuàng)建節(jié)點(diǎn)"/a"艳吠,t2可能是創(chuàng)建節(jié)點(diǎn)"/a/b",只有先創(chuàng)建了父節(jié)點(diǎn)"/a"孽椰,才能創(chuàng)建子節(jié)點(diǎn)"/a/b"昭娩。
為了實(shí)現(xiàn)這一點(diǎn),Zab協(xié)議要保證同一個(gè)Leader
發(fā)起的事務(wù)要按順序被執(zhí)行黍匾,同時(shí)還要保證只有先前Leader
的事務(wù)被執(zhí)行之后栏渺,新選舉出來的Leader
才能再次發(fā)起事務(wù)。
8.4 Zookeeper 的崩潰恢復(fù)
如果主節(jié)點(diǎn)Leader宕機(jī)锐涯,那么如何恢復(fù)服務(wù)呢磕诊?
1. 領(lǐng)導(dǎo)選舉Leader election
選舉階段,此時(shí)集群中的節(jié)點(diǎn)處于 Looking 狀態(tài)。它們會(huì)各自向其他節(jié)點(diǎn)發(fā)起投票霎终,投票當(dāng)中包含自己的服務(wù)器 ID 和最新事務(wù) ID(ZXID)滞磺。
接下來,節(jié)點(diǎn)會(huì)用自身的 ZXID 和從其他節(jié)點(diǎn)接收到的 ZXID 做比較莱褒,如果發(fā)現(xiàn)別人家的 ZXID 比自己大击困,也就是數(shù)據(jù)比自己新,那么就重新發(fā)起投票广凸,投票給目前已知最大的 ZXID 所屬節(jié)點(diǎn)阅茶。
每次投票后,服務(wù)器都會(huì)統(tǒng)計(jì)投票數(shù)量谅海,判斷是否有某個(gè)節(jié)點(diǎn)得到半數(shù)以上的投票脸哀。如果存在這樣的節(jié)點(diǎn),該節(jié)點(diǎn)將會(huì)成為準(zhǔn) Leader扭吁,狀態(tài)變?yōu)?Leading撞蜂。其他節(jié)點(diǎn)的狀態(tài)變?yōu)?Following。
這就相當(dāng)于侥袜,一群武林高手經(jīng)過激烈的競爭谅摄,選出了武林盟主。
2. 發(fā)現(xiàn) Discovery
發(fā)現(xiàn)階段系馆,用于在從節(jié)點(diǎn)中發(fā)現(xiàn)最新的 ZXID 和事務(wù)日志送漠。或許有人會(huì)問:既然 Leader 被選為主節(jié)點(diǎn)由蘑,已經(jīng)是集群里數(shù)據(jù)最新的了闽寡,為什么還要從節(jié)點(diǎn)中尋找最新事務(wù)呢?
這是為了防止某些意外情況尼酿,比如因網(wǎng)絡(luò)原因在上一階段產(chǎn)生多個(gè) Leader 的情況爷狈。
所以這一階段,Leader 集思廣益裳擎,接收所有 Follower 發(fā)來各自的最新 epoch 值涎永。Leader 從中選出最大的 epoch,基于此值加 1鹿响,生成新的 epoch 分發(fā)給各個(gè) Follower羡微。
各個(gè) Follower 收到全新的 epoch 后,返回 ACK 給 Leader惶我,帶上各自最大的 ZXID 和歷史事務(wù)日志妈倔。Leader 選出最大的 ZXID,并更新自身歷史日志绸贡。
3. 同步 Synchronization
同步階段盯蝴,把 Leader 剛才收集得到的最新歷史事務(wù)日志毅哗,同步給集群中所有的 Follower。只有當(dāng)半數(shù) Follower 同步成功捧挺,這個(gè)準(zhǔn) Leader 才能成為正式的 Leader虑绵。
自此,故障恢復(fù)正式完成闽烙。
8.5 zookeeper 數(shù)據(jù)寫入過程
寫入數(shù)據(jù)就涉及到了 ZAB協(xié)議的 BroadCast (廣播)階段蒸殿,簡單來說,就是 Zookeeper 常規(guī)情況下更新數(shù)據(jù)的時(shí)候鸣峭,由 Leader 廣播到所有的 Follower。詳細(xì)過程如下:
zk客戶端發(fā)出寫入數(shù)據(jù)請(qǐng)求給任意Follower酥艳。
Follower 把寫入數(shù)據(jù)請(qǐng)求轉(zhuǎn)發(fā)給 Leader摊溶。
Leader 采用二階段提交方式,先發(fā)送廣播給 Follower充石。
Follower 接到 Propose 消息莫换,寫入日志成功后,返回 ACK 消息給 Leader骤铃。
Leader 接到半數(shù)以上 ACK 消息拉岁,返回成功給客戶端,并且廣播 Commit 請(qǐng)求給 Follower惰爬。
9 zookeeper 分布式鎖
9.1 Curator與Spring的結(jié)合
見參考文檔2
9.2 什么是分布式鎖
9.2 實(shí)現(xiàn)分布式鎖
分布式一致性算法
集群中有兩個(gè)數(shù)據(jù)庫A和B喊暖,為了保證一致性,所以A和B需要同步數(shù)據(jù)撕瞧。當(dāng)User更新了數(shù)據(jù)庫A的數(shù)據(jù)value后陵叽,User從數(shù)據(jù)庫B讀取數(shù)據(jù)value,此時(shí)會(huì)出現(xiàn)三種情況:
- 強(qiáng)一致性丛版,value==2巩掺。強(qiáng)一致性需要讓同步過程非常快(很難實(shí)現(xiàn))页畦;或者利用分布式鎖胖替,在讀取數(shù)據(jù)庫B前阻塞住,等待同步完成后釋放鎖
- 弱一致性豫缨,value==1 独令。數(shù)據(jù)更新后,如果能容忍后續(xù)的訪問只能訪問到部分或者全部訪問不到好芭,則是弱一致性记焊。最終一致性就屬于弱一致性。
- 最終一致性栓撞,最終value==2遍膜。一段時(shí)間后碗硬,節(jié)點(diǎn)間的數(shù)據(jù)會(huì)最終達(dá)到一致狀態(tài),但不保證在任意時(shí)刻任意節(jié)點(diǎn)上的同一份數(shù)據(jù)都是相同的
更多一致性問題參考文章強(qiáng)一致性瓢颅、順序一致性恩尾、弱一致性和共識(shí)。
待補(bǔ)充
后面根據(jù)極客時(shí)間《zookeeper實(shí)戰(zhàn)與源碼解析》(8小時(shí)視頻)補(bǔ)充筆記挽懦,包括
- 實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)翰意,
- 解析paxos和raft,對(duì)比Chubby信柿,使用etcd冀偶,
- 存儲(chǔ)結(jié)構(gòu),存儲(chǔ)源碼渔嚷,
- 客戶端服務(wù)端通信源碼进鸠,
- 節(jié)點(diǎn)選舉,ZAB
根據(jù)博客等逐步更新一下內(nèi)容:
- CAP理論
- 服務(wù)端同步原理形病,
- 客戶端響應(yīng)原理客年,
- 可視化客戶端工具ZooInspector和exhibitor
- zookeeper異步初始化的源碼分析,eventThread漠吻,sendThread
總結(jié):
啥都不懂公眾號(hào), 觀其大略有印象;
快速入門看周陽, 短小生動(dòng)門檻低;
想要開發(fā)看慕課, 制作精良能實(shí)戰(zhàn);
深入理解看極客, 大牛源碼說原理.
依次耗時(shí)更長量瓜,學(xué)習(xí)曲線更陡峭,但是也更深入
推薦閱讀
- 什么是zookeeper - 碼農(nóng)翻身途乃,講了zookeeper誕生是為了解決哪些問題绍傲,即zk的作用
- 分布式一致性算法 - 碼農(nóng)翻身
- 強(qiáng)一致性、順序一致性耍共、弱一致性和共識(shí)
- 什么是zookeeper - 程序員小灰
- 如何用zookeeper實(shí)現(xiàn)分布式鎖 - 程序員小灰
- zookeeper 面試題 - 附答案唧取,用于檢查學(xué)習(xí)成果和復(fù)習(xí)
- 觀察者模式,zookeeper是一個(gè)基于觀察者模式設(shè)計(jì)的分布式服務(wù)管理框架
參考文檔
- ZooKeeper分布式專題與Dubbo微服務(wù)入門 - 慕課網(wǎng)
- zookeeper 代碼倉庫 - github
- 深入淺出理解Zookeeper - 周陽
- Zookeeper實(shí)戰(zhàn)與源碼剖析 - 極客時(shí)間
- zookeeper源碼解讀
- 什么是zookeeper - 碼農(nóng)翻身
- 分布式一致性算法 - 碼農(nóng)翻身
- 強(qiáng)一致性划提、順序一致性枫弟、弱一致性和共識(shí)
- 什么是zookeeper - 程序員小灰
- 如何用zookeeper實(shí)現(xiàn)分布式鎖 - 程序員小灰
- Java中的樂觀鎖
- zookeeper 面試題 - 附答案
- Java 異步實(shí)現(xiàn)的幾種方式