前言
上篇我們學習了Zookeeper在分佈式下的常見場景與解決方案,本篇我們開始學習Zookeeper核心模型,瞭解zk的數據模型、節點特性、版本與權限等核心功能原理
數據模型
Zookeeper的視圖結構和標準的Unix文件系統非常相似,在Zookeeper中沒有目錄和文件等概念,而是有一個數據節點的概念,稱之爲 ZNode
。而每一個 ZNode
則是我們每個路徑創建對應的節點,由於每個 ZNode
上可以保存數據,並且還可以掛載子 ZNode
節點,所以形成了一個樹形結構,大概如下:
而在每個 ZNode
中有一個事務ID的概念,在Zookeeper中將每個數據的變更操作視爲事務操作,其中包括數據節點的創建、刪除、更新與會話的變更等。而每一次事務操作,zookeeper都會爲其分配一個全局的事務ID,用ZXID表示,定義爲64位的數字,而ZXID的大小也代表了zookeeper執行操作的順序性。
數據節點特性
節點類型
在Zookeeper中,每個ZNode都有自己的生命週期,而生命週期取決於每個ZNode的類型。在Zookeeper中數據節點主要可以分爲持久化節點(PERSISTENT)、臨時節點(EPHEMERAL)以及順序節點( SEQUENTIAL)三大類。而在使用過程中,無論是持久化節點還是臨時節點,都可以組合順序節點使用,因此總共組合成了四種數據節點,如下:
持久化節點
持久化節點是Zookeeper中最常見的一種數據節點,所謂持久化,即從節點創建開始,除非手動觸發刪除節點的操作,否則都會一直存在於Zookeeper服務器上,哪怕是服務器宕機等故障,也會在重啓以後恢復。
持久化順序節點
持久化順序節點,其自身是持久化的節點,唯一的區別表現在順序性上。當創建持久化順序節點以後,zookeeper中的父節點會爲第一級子節點維護一份順序,用於記錄每一個子節點創建的順序性。因此基於順序節點的特性,在節點被創建的時候,會自動添加一個數字後綴,作爲一個新的、完整的節點名。
臨時節點
和持久化節點不同的是,臨時節點的生命週期與當前創建節點的客戶端會話有關。如果創建當前節點的客戶端會話過期失效,那麼該客戶端創建的所有臨時節點都會被清理。因爲臨時節點並不會持久化存儲,因此在Zookeeper中也不允許在臨時節點中掛載其他節點,防止因爲臨時節點被清理導致的一系列問題。
臨時順序節點
和臨時節點特性相同,臨時順序節點在創建的時候具有順序性,會自動生成一個數字後綴,父節點會進行節點的順序管理。還記得上篇我們解決分佈式鎖,以及分佈式隊列等場景中解決方案,就是在一個父節點中創建多個具有順序性的臨時節點,很多時候我們都會利用臨時節點的特性組合順序性來解決開發上的問題
節點狀態信息
前面我們知道ZNode中可以存儲數據,除此之外,每個ZNode中還會保存自身的一些狀態信息。還記得我們開篇的時候,使用的get系列命令,以及Api中的get方法,都會獲取到很多ZNode中存儲的信息,而其中關於當前節點狀態相關的信息都保存在Stat類實例中,我們先來看Stat類的源碼定義:
1. `publicclassStatimplementsRecord{`
2. `privatelong czxid;`
3. `privatelong mzxid;`
4. `privatelong ctime;`
5. `privatelong mtime;`
6. `privateint version;`
7. `privateint cversion;`
8. `privateint aversion;`
9. `privatelong ephemeralOwner;`
10. `privateint dataLength;`
11. `privateint numChildren;`
12. `privatelong pzxid;`
14. `publicStat() {`
15. `}`
16. `publicStat(`
17. `long czxid,`
18. `long mzxid,`
19. `long ctime,`
20. `long mtime,`
21. `int version,`
22. `int cversion,`
23. `int aversion,`
24. `long ephemeralOwner,`
25. `int dataLength,`
26. `int numChildren,`
27. `long pzxid) {`
28. `this.czxid=czxid;`
29. `this.mzxid=mzxid;`
30. `this.ctime=ctime;`
31. `this.mtime=mtime;`
32. `this.version=version;`
33. `this.cversion=cversion;`
34. `this.aversion=aversion;`
35. `this.ephemeralOwner=ephemeralOwner;`
36. `this.dataLength=dataLength;`
37. `this.numChildren=numChildren;`
38. `this.pzxid=pzxid;`
39. `}`
40. `.........`
41. `}`
可以看到在Stat類的定義中,定義了很多變量用來存儲不同的數據,而這些變量分別代表什麼意思,有什麼作用,接下來我們來對這些變量進行說明:
czxid
czxid代表Create ZXID,即代表着當前zookeeper節點創建的時候分配的事務ID,並且當前ID在創建完成以後不會再變更
mzxid
mzxid代表Modified ZXID,即代表着當前節點每一次觸發變更操作的時候分配的事務ID,這裏保存的是最後一次變更的時候分配的事務ID。
ctime
ctime代表Create time,與czxid搭配使用,即在創建當前節點的時候,記錄創建的時間
mtime
mtime代表Modified time,與mzxid搭配使用,即會記錄最後一次節點變更操作的時間
version
version代表當前節點的版本號
cversion
cversion代表當前節點中創建的子節點的版本號
aversion
aversion代表當前節點中ACL權限相關的版本號
ephemeralOwner
ephemeralOwner用來保存創建節點的時候生成的會話sessionId,如果當前節點是持久化節點,這個值一般爲0(0x0)
dataLength
dataLength保存了當前節點中存儲的數據對應的字節長度
numChildren
numChildren中保存了當前節點中創建的子節點的個數
pzxid
pzxid保存了該節點的子節點列表中最後一次被修改的時候生成的事務ID,需要注意的是這裏只有子節點列表變化纔會重新生成pzxid,如果某個子節點內容修改等操作並不會生成新的pzxid
節點的版本控制
從stat類的定義中,我們看到,在ZNode中,存在多種事務操作的ID,但是zk是如何保證每次事務操作的正確性和穩定的呢?這個時候我們不禁要考慮分佈式場景下一個概念--鎖,在分佈式系統中,一般事務操作,都要保證排他性,而主流的鎖方案分爲悲觀鎖和樂觀鎖兩種。悲觀鎖具有強烈的獨佔和排他性,但是整個處理過程中,數據會完全被鎖定,其他的事務對該數據將做不了任何操作,哪怕是讀取數據的操作,直到事務操作結束釋放悲觀鎖爲止。但是在分佈式場景下,更多的操作是讀取共享的數據,如果使用悲觀鎖,則會造成大量的數據被鎖定,造成性能大幅度下降。因此樂觀鎖的概念出現,在樂觀鎖中的絕大多數操作都是不對數據加鎖的,而是在更新操作之前,去檢查當前事務讀取的數據與即將要修改的數據是否一致,從而確定是否在讀取完數據到更新數據之前的過程中,有木有別的事務對該數據進行了修改操作。如果發現已經被更新了,則回滾當前事務操作,如果沒有修改則執行當前的事務。在JDK中,樂觀鎖的一個典型實現則是利用CAS理論實現的,而Zookeeper也基於類似的實現方案,在每次事務操作之前,都會在 PrepRequestProcessor
處理器中的 setData
數據更新操作之前,進行一次版本檢查操作,如下:
1. `int newVersion = checkAndIncVersion(nodeRecord.stat.getVersion(), setDataRequest.getVersion(), path);`
checkAndIncVersion方法如下:
1. `privatestaticint checkAndIncVersion(int currentVersion, int expectedVersion, String path)`
2. `throwsKeeperException.BadVersionException{`
3. `//判斷當前請求的版本不是-1,並且與原來的版本號不同時,則拋出異常,其他情況下則將版本號+1`
4. `if(expectedVersion != -1&& expectedVersion != currentVersion) {`
5. `thrownewKeeperException.BadVersionException(path);`
6. `}`
7. `return currentVersion + 1;`
8. `}`
通知機制Watcher
在前面的文章中,我們學習了Zookeeper的發佈訂閱功能的實踐,同時我們也知道zk中存在多種監聽通知,可以實現一對一,一對多等不同的通知機制。而zk中的watcher註冊和通知過程如下:
從這我們可以看出,整個Watcher機制中,主要包括watcherManager、客戶端線程以及Zookeeper服務端三部分。大概的過程爲客戶端在朝服務器註冊Watcher的同時,將watcher對象存儲在客戶端的WatcherManager中。當Zookeeper服務端觸發了Watcher事件後,會向客戶端發送通知,客戶端線程則是從WatcherManager中取出Watcher對象來執行對應的邏輯。
Watcher接口
在Zookeeper中,Watcher接口表示一個標準的事件處理器。定義了對應的邏輯,其中KeeperState和EventType兩個枚舉類型的屬性分別代表了通知的狀態以及通知的事件類型,並且在Watcher接口中,定義了一個方法作爲觸發通知以後的邏輯處理方法:
process(WatchedEventevent)
,接下來我們來看看這兩個核心的枚舉類及其參數
KeeperState
KeeperState定義了Watcher事件的所有狀態類型,代碼如下:
1. `publicenumKeeperState{`
2. `@Deprecated`
3. `Unknown(-1), //未知狀態,已經廢棄`
4. `Disconnected(0),//斷開連接`
5. `@Deprecated`
6. `NoSyncConnected(1),//沒有連接,已經廢棄`
7. `SyncConnected(3),//已經連接`
8. `AuthFailed(4),//權限異常`
9. `ConnectedReadOnly(5),//當前連接僅僅支持讀操作`
10. `SaslAuthenticated(6),`
11. `Expired(-112),`
12. `Closed(7);//關閉連接`
13. `.........................`
14. `}`
EventType
除了KeeperState以外,EventType代表了通知的類型,代碼如下:
1. `publicenumEventType{`
2. `None(-1),//未知`
3. `NodeCreated(1),//創建節點成功通知`
4. `NodeDeleted(2),//節點移除通知`
5. `NodeDataChanged(3),//當前節點數據變化通知`
6. `NodeChildrenChanged(4),//子節點列表變化通知`
7. `DataWatchRemoved(5),`
8. `ChildWatchRemoved(6);`
9. `}`
而在Zookeeper開發過程中,往往是兩種類型參數組合返回,我們將常見的場景通知和組合列成表格,大概如下:
KeeperState | EventType | 觸發場景 |
---|---|---|
SyncConnected | None | 建立連接成功 |
NodeCreated | 節點被創建 | |
NodeDeleted | 節點被刪除 | |
NodeDataChanged | 節點內容變化 | |
NodeChild renChanged | 節點所屬子節點列表變化 | |
Disconnected | None | 斷開連接 |
Expired | None | 超時 |
AuthFailed | None | 1.錯誤的Scheme進行權限檢查 2.SASL權限檢查失敗 |
ACL權限操作
在Zookeeper中,提供了ACL權限機制來保障節點及對應數據的安全,但是需要注意的是Zookeeper中的ACL機制和傳統的ACL並不一樣,分別分爲:權限模式(Scheme)、授權對象(ID)和權限(Permission),通常是以Scheme:ID:Permission方式組合成一個有效的ACL信息。接下來我們具體的學習這三種組成模塊:
權限模式---Scheme
權限模式在Zookeeper中來確定權限驗證過程中的校驗策略。常見的策略有四種:
IP
IP模式一般通過IP地址進行粗粒度的權限控制,例如"ip:192.168.1.1/24"代表是192.168.1.*這個網段的IP進行的權限控制。
Digest
Digest是使用最多的一種權限模式,基於傳統的"username:password"模式來控制對應的權限,當我們使用Digest方式來驗證權限的時候,Zookeeper中先後兩次進行編碼處理,分別是SHA-1和BASE64算法,加密過程的實現在 DigestAuthenticationProvider
類的
generateDigest(StringidPassword)
方法中進行封裝
World
World模式屬於一種開放的權限模式,此模式下幾乎沒有任何權限控制,所有用戶都可以隨意對任何節點進行操作
Super
在Zookeeper中存在一個超級管理員的模式,此模式不需要主動設置,在任何其他的權限策略下都可以使用,稱之爲Super權限,一旦獲取了Super權限,即擁有了超級管理員權限,可以對所有的節點進行任意操作。
授權對象--ID
授權對象指的是權限所在的用戶或者指定的權限實體,而在不同的模式下,授權對象都是不同的,各個權限模式與授權對象的關係如下:
權限模式 | 授權對象 |
---|---|
IP | 通常是一個ip或者一個ip段,例如192.168.1.1 |
Digest | 自定義的,一般爲"username:password" |
World | anyone |
Super | 與Digest一樣,需要指定一個超級管理員信息 |
權限--Permission
權限即指的是經過權限模式認證後的被允許的操作,在Zookeeper中,權限操作分爲五大類:
-
Create(C):數據節點的創建權限,並且允許創建子節點
-
Delete(D):允許刪除該節點的子節點
-
Read(R):允許對該數據節點進行讀取,也可以獲取子節點列表、讀取子節點等
-
Write(W):數據節點的更新權限,允許對該節點內容修改
-
Admin(A):數據節點的管理權限,允許對其設置ACL權限操作
實現自定義權限控制器
一般開發中,Zookeeper自帶的權限操作已經滿足日常使用,但是如果需要特殊的權限控制操作,Zookeeper同樣支持自定義一個權限控制器,在Zookeeper中,權限主要在org.apache.zookeeper. server.auth.AuthenticationProvider接口中定義,其代碼定義如下:
1. `publicinterfaceAuthenticationProvider{`
2. `String getScheme();`
4. `KeeperException.Code handleAuthentication(ServerCnxn cnxn, byte authData[]);`
6. `boolean matches(String id, String aclExpr);`
8. `boolean isAuthenticated();`
10. `boolean isValid(String id);`
11. `}`
我們需要實現自定義的權限控制器只要實現當前接口,在實現完畢以後,我們將該自定義的權限控制器註冊到Zookeeper服務中去,而註冊的方式有兩種:
1.系統屬性配置
在Zookeeper啓動的時候,在啓動參數中指定:
1. `-Dzookeeper.authProvider.l=xxx.MyauthProvider`
2.配置文件方式
Zookeeper中的zoo.cfg配置文件中可以添加如下的配置:
1. `authProvider.l=xxx.MyauthProvider`
Super模式使用
在Zookeeper使用過程中,往往存在一個場景,即原來節點的創建者設置了ACL權限,但是這個創建者已經不再使用了,而其他的客戶端想要使用該節點,該怎麼做呢?這個時候就需要Super模式的用戶出馬了!使用超級管理員權限,具體的步驟如下:
1.在Zookeeper啓動的時候添加系統屬性:
1. `-Dzookeeper.DigestAuthenticationProvider. superDigest=root:kWN6aNSbjcKWPqjiV7c`
2. `gON24raU=`
即指定了root用戶擁有超級管理員權限,設置好以後啓動Zookeeper,即可在客戶端中使用了
2.編寫客戶端代碼使用當前Super權限的管理員操作節點,例如:
1. `publicclassAuthSample_Super{`
2. `fin a l sta tic String PATH = "/zk-book";`
3. `public sta tic void m ain(String[] args) throwsException{`
4. `ZooKeeper zookeeperl = newZooKeeper("domainl.book.zookeeper:2181",5000,`
5. `null);`
6. `zookeeperl.addAuthlnfo("digest", "root:tru e".getBytes());`
7. `zookeeperl.create( PATH, "init".getBytes(), Ids.CREATORALLACL, CreateMode.EPHEMERAL );`
8. `ZooKeeper zookeeper2 = newZooKeeper("domainl.book.zookeeper:2181",50000,`
9. `n u ll);`
10. `zookeeper2.addAuthlnfo("digest", "root:zk-book".getBytes());`
11. `System.out.println(zookeeper2.getData(PATH, false, null));`
12. `ZooKeeper zookeeper3 = newZooKeeper("domainl.book. zookeeper:2181",50000,`
13. `n u ll);`
14. `zookeeper3.addAuthlnfo("digest", "root:false ". getBytes());`
15. `System.out.println(zookeeper3.getData(PATH,false,null));`
16. `}`
17. `}`
運行以後,結果如下:
1. `[B<a7b7072`
2. `org.apache.zookeeper.KeeperException$NoAuthException:`
3. `KeeperErrorCode= NoAuthfor/zk-book`
可見root用戶的確可以操作一個受限制的節點