1、系統(tǒng)模型
1.1、數(shù)據(jù)模型
Zookeeper 的視圖結構是一個樹形結構,樹上的每個節(jié)點稱之為數(shù)據(jù)節(jié)點(即 ZNode),每個ZNode 上都可以保存數(shù)據(jù),同時還可以掛載子節(jié)點。并且Zookeeper的根節(jié)點為 "/"。

1.2、節(jié)點類型
在 Zookeeper 中,每個數(shù)據(jù)節(jié)點都是有生命周期的,其生命周期的長短取決于數(shù)據(jù)節(jié)點的節(jié)點類型。在 Zookeeper 中有如下幾類節(jié)點:
| 節(jié)點類型 | 說明 |
|---|---|
| 持久節(jié)點(PERSISTENT) | 指該數(shù)據(jù)節(jié)點被創(chuàng)建后,就會一直存在于 Zookeeper 服務器上,直到有刪除操作來主動清除這個節(jié)點。 |
| 持久順序節(jié)點(PERSISTENT_SEQUENTIAL) | 基本特性和持久節(jié)點是一致的,額外的特性表現(xiàn)在順序性上,在 Zookeeper 中,每個父節(jié)點都會為它的第一級子節(jié)點維護一份順序,用于記錄下每個子節(jié)點創(chuàng)建的先后順序?;谶@個順序特性,在創(chuàng)建子節(jié)點的時候,可以設置這個標記,那么在創(chuàng)建節(jié)點過程中,Zookeeper 會自動為給定節(jié)點名加上一個數(shù)字后綴,作為一個新的、完整的節(jié)點名。另外需要注意的是,這個數(shù)字后綴的上限是整型的最大值。 |
| 臨時節(jié)點(EPHEMERAL) | 臨時節(jié)點的生命周期和客戶端的會話綁定在一起,如果客戶端會話失效,那么這個節(jié)點就會被自動清理掉。另外,Zookeeper 規(guī)定了不能基于臨時節(jié)點來創(chuàng)建子節(jié)點,即臨時節(jié)點只能作為葉子節(jié)點。 |
| 臨時順序節(jié)點(EPHEMERAL_SEQUENTIAL) | 基本特性和臨時節(jié)點一致,只是添加了順序的特性。 |
1.3、狀態(tài)信息
每個數(shù)據(jù)節(jié)點中除了存儲了數(shù)據(jù)內容之外,還存儲了數(shù)據(jù)節(jié)點本身的一些狀態(tài)信息(State)。
| 狀態(tài)屬性 | 說明 |
|---|---|
| cZxid | 即 Create ZXID,表示該數(shù)據(jù)節(jié)點被創(chuàng)建時的事務ID。 |
| ctime | 即 Create Time,表示該數(shù)據(jù)節(jié)點被創(chuàng)建的時間。 |
| mZxid | 即 Modified ZXID,表示該節(jié)點最后一次被更新時的事務ID。 |
| mtime | 即 Modified Time,表示該數(shù)據(jù)節(jié)點最后一次被更新的時間。 |
| pZxid | 表示該節(jié)點的子節(jié)點列表最后一次被修改時的事務ID。注意,只有子節(jié)點列表變更了才會變更 pZxid,子節(jié)點內容變更不會影響pZxid。 |
| cversion | 表示子節(jié)點的版本號。 |
| dataVersion | 表示數(shù)據(jù)節(jié)點的版本號。 |
| aclVersion | 表示節(jié)點的 ACL 版本號。 |
| ephemeralOwner | 創(chuàng)建該臨時節(jié)點的會話的sessionID。如果該節(jié)點是持久節(jié)點,那么這個屬性值為0。 |
| dataLength | 表示數(shù)據(jù)內容的長度。 |
| numChildren | 表示當前節(jié)點的子節(jié)點個數(shù)。 |
1.4、ZXID
在Zookeeper 中,事務是指能夠改變 Zookeeper 服務器狀態(tài)的操作,我們也稱之為事務操作或更新操作,一般包括數(shù)據(jù)節(jié)點創(chuàng)建與刪除、數(shù)據(jù)節(jié)點內容更新和客戶端會話創(chuàng)建與失效等操作。對于每一個事務請求,Zookeeper 都會為其分配一個全局唯一的事務ID,用 ZXID 來表示,通常是一個 64 位的數(shù)字。每一個 ZXID 對應一次更新操作,從這些 ZXID 中可以間接地識別出 Zookeeper 處理這些更新操作請求的全局順序。
ZXID 是一個 64 位的數(shù)字,其中低 32 位可以看作是一個簡單的單調遞增的計數(shù)器,針對客戶端的每一個事務請求,Leader 服務器在產生一個新的事務 Proposal 的時候,都會對該計數(shù)器進行加 1 操作;而高 32 位則代表了 Leader 周期 epoch 的編號,每當選舉產生一個新的 Leader 服務器,就會從這個 Leader 服務器上取出其本地日志中最大事務 Proposal 的 ZXID,并從該 ZXID 中解析出對應的 epoch 值,然后再對其進行加 1 操作,之后就會以此編號作為新的 epoch,并將低 32 位置 0 來開始生成新的 ZXID。
1.5、版本
Zookeeper 中為數(shù)據(jù)節(jié)點引入了版本的概念,每個數(shù)據(jù)節(jié)點都具有三種類型的版本信息(在上面的狀態(tài)信息中已經介紹了三種版本信息代表的意思),對數(shù)據(jù)節(jié)點的任何更新操作都會引起版本號的變化。其中我們以 dataVersion 為例來說明。在一個數(shù)據(jù)節(jié)點被創(chuàng)建完畢之后,節(jié)點的dataVersion 值是 0,表示的含義是 ”當前節(jié)點自從創(chuàng)建之后,被更新過 0 次“。如果現(xiàn)在對該節(jié)點的數(shù)據(jù)內容進行更新操作,那么隨后,dataVersion 的值就會變成 1。即表示的是對數(shù)據(jù)節(jié)點的數(shù)據(jù)內容的變更次數(shù)。
版本的作用是用來實現(xiàn)樂觀鎖機制中的 “寫入校驗” 的。例如,當要修改數(shù)據(jù)節(jié)點的數(shù)據(jù)內容時,帶上版本號,如果數(shù)據(jù)節(jié)點的版本號與傳入的版本號相等,就進行修改,否則修改失敗。
1.6、Watcher
1.6.1、概述
Zookeeper 提供了分布式數(shù)據(jù)的發(fā)布/訂閱功能。一個典型的發(fā)布/訂閱模型系統(tǒng)定義了一種一對多的訂閱關系,能夠讓多個訂閱者同時監(jiān)聽某一個主題對象,當這個主題對象自身狀態(tài)變化時,會通知所有訂閱者,使它們能夠做出相應的處理。在 Zookeeper 中,引入了 Watcher 機制來實現(xiàn)這種分布式的通知功能。Zookeeper 允許客戶端向服務端注冊一個 Watcher 監(jiān)聽,當服務端的一些指定事件觸發(fā)了這個 Watcher,那么就會向指定客戶端發(fā)送一個事件通知來實現(xiàn)分布式的通知功能。

從上圖可以看出 Zookeeper 的 Watcher 機制主要包括客戶端線程、客戶端WatchMananger 和 Zookeeper 服務器三部分。在具體工作流程上,簡單地講,客戶端在向 Zookeeper 服務器注冊 Watcher 的同時,會將 Watcher 對象存儲在客戶端的 WatchMananger 中。當 Zookeeper 服務器端觸發(fā) Watcher 事件后,會向客戶端發(fā)送通知,客戶端線程從 WatchManager 中取出對應的 Watcher 對象來執(zhí)行回調邏輯。
1.6.2、Watcher特性
- 一次性:表示無論是服務端還是客戶端,一旦一個 Watcher 被觸發(fā),Zookeeper 都會將其從相應的存儲中移除。因此,開發(fā)人員在 Watcher 的使用上要記住的一點是需要反復注冊。
- 客戶端串行執(zhí)行:客戶端 Watcher 回調的過程是一個串行同步的過程,這為我們保證了順序,同時,需要開發(fā)人員注意的一點是,千萬不要因為一個 Watcher 的處理邏輯影響了整個客戶端的 Watcher 回調。
- 輕量:WatchedEvent 是 Zookeeper 整個 Watcher 通知機制的最小通知單元,這個數(shù)據(jù)結構中只包含三部分內容:通知狀態(tài)、事件類型和節(jié)點路徑。也就是說,Watcher通知非常簡單,只會告訴客戶端發(fā)生了事件,而不會說明事件的具體內容。
1.6.3、watcher接口設計
Watcher是一個接口,任何實現(xiàn)了Watcher接口的類就是一個新的Watcher。Watcher內部包含了兩個枚舉類:KeeperState、EventType
-
Watcher通知狀態(tài)(KeeperState)
KeeperState是客戶端與服務端連接狀態(tài)發(fā)生變化時對應的通知類型。路徑為org.apache.zookeeper.Watcher.Event.KeeperState,是一個枚舉類,其枚舉屬性如下:
| 枚舉屬性 | 說明 |
|---|---|
| SyncConnected | 客戶端與服務器正常連接時 |
| Disconnected | 客戶端與服務器斷開連接時 |
| Expired | 會話session失效時 |
| AuthFailed | 身份認證失敗時 |
-
Watcher事件類型(EventType)
EventType是數(shù)據(jù)節(jié)點(znode)發(fā)生變化時對應的通知類型。EventType變化時KeeperState永遠處于SyncConnected通知狀態(tài)下;當KeeperState發(fā)生變化時,EventType永遠為None。其路徑為org.apache.zookeeper.Watcher.Event.EventType,是一個枚舉類,枚舉屬性如下:
| 枚舉屬性 | 說明 |
|---|---|
| None | 無 |
| NodeCreated | Watcher監(jiān)聽的數(shù)據(jù)節(jié)點被創(chuàng)建時 |
| NodeDeleted | Watcher監(jiān)聽的數(shù)據(jù)節(jié)點被刪除時 |
| NodeDataChanged | Watcher監(jiān)聽的數(shù)據(jù)節(jié)點內容發(fā)生變更時(無論內容數(shù)據(jù)是否變化) |
| NodeChildrenChanged | Watcher監(jiān)聽的數(shù)據(jù)節(jié)點的子節(jié)點列表發(fā)生變更時 |
注:客戶端接收到的相關事件通知中只包含狀態(tài)及類型等信息,不包括節(jié)點變化前后的具體內容,變化前的數(shù)據(jù)需業(yè)務自身存儲,變化后的數(shù)據(jù)需調用get等方法重新獲??;
1.6.4、捕獲相應的事件
上面講到zookeeper客戶端連接的狀態(tài)和zookeeper對znode節(jié)點監(jiān)聽的事件類型,下面我們來講解如何建立zookeeper的watcher監(jiān)聽。在zookeeper中采用zk.getChildren(path, watch)、zk.exists(path, watch)、zk.getData(path, watcher, stat)這樣的方式為某個znode注冊監(jiān)聽。
下表以node-x節(jié)點為例,說明調用的注冊方法和可監(jiān)聽事件間的關系:
| 注冊方式 | Created | ChildrenChanged | Changed | Deleted |
|---|---|---|---|---|
| zk.exists(“/node-x”,watcher) | 可監(jiān)控 | 可監(jiān)控 | 可監(jiān)控 | |
| zk.getData(“/node-x”,watcher) | 可監(jiān)控 | 可監(jiān)控 | ||
| zk.getChildren(“/node-x”,watcher) | 可監(jiān)控 | 可監(jiān)控 |
1.7、ACL
Zookeeper 中提供了一套完善的 ACL(Access Control List)權限控制機制來保障數(shù)據(jù)的安全。
1.7.1、概述
ACL 由三部分組成,分別是:權限模式(Scheme)、授權對象(ID)和權限(Permission),通常使用“scheme: ?id:permission”來標識一個有效的ACL 信息。下面分別介紹:
-
權限模式(Scheme)
方案 說明 world 只有一個用戶:anyone,代表登錄 Zookeeper 所有人(默認) ip 對客戶端使用IP地址認證。 auth 使用已添加認證的用戶認證。 digest 使用“用戶名:密碼”方式認證。
-
授權對象(ID)
授權對象ID是指,權限賦予的實體,例如:IP 地址或用戶。
-
權限(Permission)
權限 ACL簡寫 描述 create c 可以創(chuàng)建子節(jié)點。 delete d 可以刪除子節(jié)點(僅下一級節(jié)點)。 read r 可以讀取節(jié)點數(shù)據(jù)或子節(jié)點列表。 write w 可以對節(jié)點進行更新操作。 admin a 可以設置節(jié)點訪問控制列表權限。
1.7.2、特性
- zooKeeper的權限控制是基于每個znode節(jié)點的,需要對每個節(jié)點設置權限。
- 每個znode支持設置多種權限控制方案和多個權限。
- 子節(jié)點不會繼承父節(jié)點的權限,客戶端無權訪問某節(jié)點,但可能可以訪問它的子節(jié)點。
1.7.3、案例
-
world授權模式
命令
setAcl <path> world:anyone:<acl>案例
[zk: localhost:2181(CONNECTED) 0] create /node1 "node1" Created /node1 [zk: localhost:2181(CONNECTED) 1] getAcl /node1 'world,'anyone : cdrwa [zk: localhost:2181(CONNECTED) 2] setAcl /node1 world:anyone:crwa cZxid = 0x100000004 ctime = Fri May 29 14:31:54 CST 2020 mZxid = 0x100000004 mtime = Fri May 29 14:31:54 CST 2020 pZxid = 0x100000004 cversion = 0 dataVersion = 0 aclVersion = 1 ephemeralOwner = 0x0 dataLength = 5 numChildren = 0 -
IP授權模式
命令
setAcl <path> ip:<ip>:<acl>案例
注意:遠程登錄zookeeper命令:./zkCli.sh -server ip
[zk: localhost:2181(CONNECTED) 18] create /node2 "node2" Created /node2 [zk: localhost:2181(CONNECTED) 23] setAcl /node2 ip:192.168.150.101:cdrwa cZxid = 0xe ctime = Fri Dec 13 22:30:29 CST 2019 mZxid = 0x10 mtime = Fri Dec 13 22:33:36 CST 2019 pZxid = 0xe cversion = 0 dataVersion = 2 aclVersion = 1 ephemeralOwner = 0x0 dataLength = 20 numChildren = 0 [zk: localhost:2181(CONNECTED) 25] getAcl /node2 'ip,'192.168.150.101 : cdrwa #使用IP非 192.168.150.101 的機器 [zk: localhost:2181(CONNECTED) 0] get /node2 Authentication is not valid : /node2 #沒有權限 -
Auth授權模式
命令
addauth digest <user>:<password> #添加認證用戶 setAcl <path> auth:<user>:<acl>案例
[zk: localhost:2181(CONNECTED) 6] create /node3 "node3" Created /node3 #添加認證用戶 [zk: localhost:2181(CONNECTED) 7] addauth digest ld:123456 [zk: localhost:2181(CONNECTED) 8] setAcl /node3 auth:ld:cdrwa cZxid = 0x10000000c ctime = Fri May 29 14:47:13 CST 2020 mZxid = 0x10000000c mtime = Fri May 29 14:47:13 CST 2020 pZxid = 0x10000000c cversion = 0 dataVersion = 0 aclVersion = 1 ephemeralOwner = 0x0 dataLength = 5 numChildren = 0 [zk: localhost:2181(CONNECTED) 9] getAcl /node3 'digest,'ld:kesl2p6Yx58a+/mP+TKSFZkzkZ0= : cdrwa #添加認證用戶后可以訪問 [zk: localhost:2181(CONNECTED) 10] get /node3 node3 cZxid = 0x10000000c ctime = Fri May 29 14:47:13 CST 2020 mZxid = 0x10000000c mtime = Fri May 29 14:47:13 CST 2020 pZxid = 0x10000000c cversion = 0 dataVersion = 0 aclVersion = 1 ephemeralOwner = 0x0 dataLength = 5 numChildren = 0 -
Digest授權模式
命令
setAcl <path> digest:<user>:<password>:<acl>這里的密碼是經過SHA1及BASE64處理的密文,在SHELL中可以通過以下命令計算:
echo -n <user>:<password> | openssl dgst -binary -sha1 | openssl base64先來計算一個密文
echo -n monkey:123456 | openssl dgst -binary -sha1 | openssl base64案例
[zk: localhost:2181(CONNECTED) 12] create /node4 "node4" Created /node4 [zk: localhost:2181(CONNECTED) 13] setAcl /node4 digest:monkey:Rk6u/zJJdOYrTZ6+J0p4/4gTILg=:cdrwa cZxid = 0x10000000e ctime = Fri May 29 14:52:50 CST 2020 mZxid = 0x10000000e mtime = Fri May 29 14:52:50 CST 2020 pZxid = 0x10000000e cversion = 0 dataVersion = 0 aclVersion = 1 ephemeralOwner = 0x0 dataLength = 5 numChildren = 0 #沒有權限無法讀取 [zk: localhost:2181(CONNECTED) 14] getAcl /node4 Authentication is not valid : /node4 #添加認證用戶 [zk: localhost:2181(CONNECTED) 15] addauth digest monkey:123456 [zk: localhost:2181(CONNECTED) 16] getAcl /node4 'digest,'monkey:Rk6u/zJJdOYrTZ6+J0p4/4gTILg= : cdrwa [zk: localhost:2181(CONNECTED) 17] get /node4 node4 cZxid = 0x10000000e ctime = Fri May 29 14:52:50 CST 2020 mZxid = 0x10000000e mtime = Fri May 29 14:52:50 CST 2020 pZxid = 0x10000000e cversion = 0 dataVersion = 0 aclVersion = 1 ephemeralOwner = 0x0 dataLength = 5 numChildren = 0 -
多種模式授權
同一個節(jié)點可以同時使用多種模式授權
[zk: localhost:2181(CONNECTED) 18] create /node5 "node5" Created /node5 [zk: localhost:2181(CONNECTED) 19] addauth digest ld:123456 [zk: localhost:2181(CONNECTED) 20] setAcl /node5 ip:192.168.150.101:cdrwa,auth:ld:cdrwa cZxid = 0x100000010 ctime = Fri May 29 14:56:38 CST 2020 mZxid = 0x100000010 mtime = Fri May 29 14:56:38 CST 2020 pZxid = 0x100000010 cversion = 0 dataVersion = 0 aclVersion = 1 ephemeralOwner = 0x0 dataLength = 5 numChildren = 0
1.7.4、ACL 超級管理員
zookeeper的權限管理模式有一種叫做super,該模式提供一個超管可以方便的訪問任何權限的節(jié)點
假設這個超管是:super:admin,需要先為超管生成密碼的密文
echo -n super:admin | openssl dgst -binary -sha1 | openssl base64
那么打開zookeeper目錄下的/bin/zkServer.sh服務器腳本文件,找到如下一行:
nohup $JAVA "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}"
這就是腳本中啟動zookeeper的命令,默認只有以上兩個配置項,我們需要加一個超管的配置項
"-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 #添加認證用戶
2、Leader 選舉
2.1、服務器狀態(tài)
- looking:尋找leader狀態(tài)。當服務器處于該狀態(tài)時,它會認為當前集群中沒有Leader,因此需要進入 Leader 選舉流程。
- leading:領導者狀態(tài)。表明當前服務器角色是leader。
- following:跟隨者狀態(tài)。表明當前服務器角色是follower。
- observing:觀察者狀態(tài)。表明當前服務器角色是observer。
2.2、服務器啟動時期的 Leader 選舉
在服務器集群初始化階段,我們以 3 臺機器組成的服務器集群為例,當有一臺服務器server1 啟動的時候,它是無法進行 Leader 選舉的,當?shù)诙_機器 server2 也啟動時,此時這兩臺服務器已經能夠進行互相通信,每臺機器都試圖找到一個 Leader,于是便進入了 Leader 選舉流程。
每個server發(fā)出一個投票。由于是初始情況,server1和server2都會將自己作為leader服務器來進行投票,每次投票會包含所推舉的服務器的myid和zxid,使用(myid, zxid)來表示,此時server1的投票為(1, 0),server2的投票為(2, 0),然后各自將這個投票發(fā)給集群中其他機器。
集群中的每臺服務器接收來自集群中各個服務器的投票。
-
處理投票。針對每一個投票,服務器都需要將別人的投票和自己的投票進行pk,pk規(guī)則如下
- 優(yōu)先檢查zxid。zxid比較大的服務器優(yōu)先作為leader。
- 如果zxid相同,那么就比較myid。myid較大的服務器作為leader服務器。
? 對于Server1而言,它的投票是(1, 0),接收Server2的投票為(2, 0),首先會比較兩者的zxid,均為0,再比較myid,此時server2的myid最大,于是更新自己的投票為(2, 0),然后重新投票,對于server2而言,其無須更新自己的投票,只是再次向集群中所有機器發(fā)出上一次投票信息即可。
統(tǒng)計投票。每次投票后,服務器都會統(tǒng)計投票信息,判斷是否已經有過半機器接受到相同的投票信息,對于server1、server2而言,都統(tǒng)計出集群中已經有兩臺機器接受了(2, 0)的投票信息,此時便認為已經選出了leader。
改變服務器狀態(tài)。一旦確定了leader,每個服務器就會更新自己的狀態(tài),如果是follower,那么就變更為following,如果是leader,就變更為leading。
2.3、服務器運行時期的 Leader 選舉
在zookeeper運行期間,leader與非leader服務器各司其職,即便當有非leader服務器宕機或新加入,此時也不會影響leader,但是一旦leader服務器掛了,那么整個集群將暫停對外服務,進入新一輪leader選舉,其過程和啟動時期的Leader選舉過程基本一致。
假設正在運行的有server1、server2、server3三臺服務器,當前l(fā)eader是server2,若某一時刻leader掛了,此時便開始Leader選舉。選舉過程如下:
- 變更狀態(tài)。leader掛后,余下的非 Observer 服務器都會將自己的服務器狀態(tài)變更為looking,然后開始進入leader選舉過程。
- 每個server會發(fā)出一個投票。在運行期間,每個服務器上的zxid可能不同,此時假定server1的zxid為123,server3的zxid為122,在第一輪投票中,server1和server3都會投自己,產生投票(1, 123),(3, 122),然后各自將投票發(fā)送給集群中所有機器。
- 接收來自各個服務器的投票。
- 處理投票。對于投票的處理,和上面提到的服務器啟動期間的處理規(guī)則是一致的。在這個例子里面,由于 Server1 的 zxid 為 123,Server3 的 zxid 為 122,那么顯然,Server1 會成為 Leader。
- 統(tǒng)計投票。
- 改變服務器狀態(tài)。
2.4、Observer 角色及其設置
observer角色特點:
- 不參與集群的leader選舉
- 不參與集群中寫數(shù)據(jù)時的ack反饋
為了使用observer角色,在任何想變成observer角色的配置文件中加入如下配置:
peerType=observer
并在所有server的配置文件中,配置成observer模式的server的那行配置追加:observer,例如:
server.3=192.168.60.130:2289:3389:observer