在ZooKeeper中,節點也稱為znode。由于對于程式員來說,對zk的操作主要是對znode的操作,是以,有必要對znode進行深入的了解。
ZooKeeper采用了類似檔案系統的的資料模型,其節點構成了一個具有層級關系的樹狀結構。例如,圖1展示了zk節點的層級樹狀結構。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiNxADOzIzMxEjMwcDM2EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
圖1:ZooKeeper的層級樹狀結構
圖1中,根節點 / 包含了兩個位元組點 /module1,/module2,而節點 /module1 又包含了三個位元組點 /module1/app1,/module1/app2,/module1/app3。在zk中,節點以絕對路徑表示,不存在相對路徑,且路徑最後不能以 / 結尾(根節點除外)。
類型
根據節點的存活時間,可以對節點劃分為持久節點和臨時節點。節點的類型在建立時就被确定下來,并且不能改變。
持久節點的存活時間不依賴于用戶端會話,隻有用戶端在顯式執行删除節點操作時,節點才消失。
臨時節點的存活時間依賴于用戶端會話,當會話結束,臨時節點将會被自動删除(當然也可以手動删除臨時節點)。利用臨時節點的這一特性,我們可以使用臨時節點來進行叢集管理,包括發現服務的上下線等。
ZooKeeper規定,臨時節點不能擁有子節點。
持久節點
使用指令create可以建立一個持久節點。
create /module1 module1
這樣,便建立了一個持久節點/module1,且其資料為”module1”。
臨時節點
使用create指令,并加上-e參數,可以建立一個臨時節點。
create -e /module1/app1 app1
這樣,便建立了一個臨時節點 /module1/app1,資料為”app1”。關閉會話,然後輸入指令:
get /module1/app1
可以看到有以下提示,說明臨時節點已經被删除。
Node does not exist: /module1/app1
順序節點
ZooKeeper中還提供了一種順序節點的節點類型。每次建立順序節點時,zk都會在路徑後面自動添加上10位的數字(計數器),例如 < path >0000000001,< path >0000000002,……這個計數器可以保證在同一個父節點下是唯一的。在zk内部使用了4個位元組的有符号整形來表示這個計數器,也就是說當計數器的大小超過2147483647時,将會發生溢出。
順序節點為節點的一種特性,也就是,持久節點和臨時節點都可以設定為順序節點。這樣一來,znode一共有4種類型:持久的、臨時的,持久順序的,臨時順序的。
使用指令create加上-s參數,可以建立順序節點,例如,
create -s /module1/app app
輸出:
Created /module1/app0000000001
便建立了一個持久順序節點 /module1/app0000000001。如果再執行此指令,則會生成節點 /module1/app0000000002。
如果在create -s再添加-e參數,則可以建立一個臨時順序節點。
節點的資料
在建立節點時,可以指定節點中存儲的資料。ZooKeeper保證讀和寫都是原子操作,且每次讀寫操作都是對資料的完整讀取或完整寫入,并不提供對資料進行部分讀取或者寫入的操作。
以下指令建立一個節點/module1/app2,且其存儲的資料為app2。
create /module1/app2 app2
ZooKeeper雖然提供了在節點存儲資料的功能,但它并不将自己定位為一個通用的資料庫,也就是說,你不應該在節點存儲過多的資料。Zk規定節點的資料大小不能超過1M,但實際上我們在znode的資料量應該盡可能小,因為資料過大會導緻zk的性能明顯下降。如果确實需要存儲大量的資料,一般解決方法是在另外的分布式資料庫(例如redis)中儲存這部分資料,然後在znode中我們隻保留這個資料庫中儲存位置的索引即可。
節點的屬性
每個znode都包含了一系列的屬性,通過指令get,我們可以獲得節點的屬性。
get /module1/app2
app2
cZxid = 0x20000000e
ctime = Thu Jun 30 20:41:55 HKT 2016
mZxid = 0x20000000e
mtime = Thu Jun 30 20:41:55 HKT 2016
pZxid = 0x20000000e
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 4
numChildren = 0
版本号
對于每個znode來說,均存在三個版本号:
-
dataVersion
資料版本号,每次對節點進行set操作,dataVersion的值都會增加1(即使設定的是相同的資料)。
-
cversion
子節點的版本号。當znode的子節點有變化時,cversion 的值就會增加1。
-
aclVersion
ACL的版本号,關于znode的ACL(Access Control List,通路控制),可以參考 參考資料1 有關ACL的描述。
以資料版本号來說明zk中版本号的作用。每一個znode都有一個資料版本号,它随着每次資料變化而自增。ZooKeeper提供的一些API例如setData和delete根據版本号有條件地執行。多個用戶端對同一個znode進行操作時,版本号的使用就會顯得尤為重要。例如,假設用戶端C1對znode /config寫入一些配置資訊,如果另一個用戶端C2同時更新了這個znode,此時C1的版本号已經過期,C1調用setData一定不會成功。這正是版本機制有效避免了資料更新時出現的先後順序問題。在這個例子中,C1在寫入資料時使用的版本号無法比對,使得操作失敗。圖2描述了這個情況。
圖2:使用版本号來阻止并行操作的不一緻性
事務ID
對于zk來說,每次的變化都會産生一個唯一的事務id,zxid(ZooKeeper Transaction Id)。通過zxid,可以确定更新操作的先後順序。例如,如果zxid1小于zxid2,說明zxid1操作先于zxid2發生。
需要指出的是,zxid對于整個zk都是唯一的,即使操作的是不同的znode。
-
cZxid
Znode建立的事務id。
-
mZxid
Znode被修改的事務id,即每次對znode的修改都會更新mZxid。
圖3:Zxid在用戶端重連中的作用
在叢集模式下,用戶端有多個伺服器可以連接配接,當嘗試連接配接到一個不同的伺服器時,這個伺服器的狀态要與最後連接配接的伺服器的狀态要保持一緻。Zk正是使用zxid來辨別這個狀态,圖3描述了用戶端在重連情況下zxid的作用。當用戶端因逾時與S1斷開連接配接後,用戶端開始嘗試連接配接S2,但S2延遲于用戶端所識别的狀态。然而,S3的狀态與用戶端所識别的狀态一緻,是以用戶端可以安全連接配接上S3。
時間戳
包括znode的建立時間和修改時間,建立時間是znode建立時的時間,建立後就不會改變;修改時間在每次更新znode時都會發生變化。
以下指令建立了一個 /module2 節點。
create /module2 module2
Created /module2
通過 get 指令,可以看到 /module2的 ctime和mtime均為Sat Jul 02 11:18:32 CST 2016。
get /module2
module2
cZxid = 0x2
ctime = Sat Jul 02 11:18:32 CST 2016
mZxid = 0x2
mtime = Sat Jul 02 11:18:32 CST 2016
pZxid = 0x2
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 7
numChildren = 0
修改 /module2,可以看到 ctime 沒有發生變化,mtime已更新為最新的時間。
set /module2 module2_1
cZxid = 0x2
ctime = Sat Jul 02 11:18:32 CST 2016
mZxid = 0x3
mtime = Sat Jul 02 11:18:50 CST 2016
pZxid = 0x2
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 9
numChildren = 0
參考資料
- http://zookeeper.apache.org/doc/trunk/zookeeperProgrammers.html#ch_zkDataModel
- http://java.globinch.com/enterprise-services/zookeeper/apache-zookeeper-explained-tutorial-cases-zookeeper-java-api-examples/#
- 《ZooKeeper分布式過程協同技術詳解》,Flavio Junqueira等著,謝超等譯