Zookeeper深入原理
1、系統模型
1.1、資料模型
Zookeeper 的檢視結構是一個樹形結構,樹上的每個節點稱之為資料節點(即 ZNode),每個ZNode 上都可以儲存資料,同時還可以掛載子節點。並且Zookeeper的根節點為 “/”。
1.2、節點型別
在 Zookeeper 中,每個資料節點都是有生命週期的,其生命週期的長短取決於資料節點的節點型別。在 Zookeeper 中有如下幾類節點:
節點型別 | 說明 |
---|---|
持久節點(PERSISTENT) | 指該資料節點被建立後,就會一直存在於 Zookeeper 伺服器上,直到有刪除操作來主動清除這個節點。 |
持久順序節點(PERSISTENT_SEQUENTIAL) | 基本特性和持久節點是一致的,額外的特性表現在順序性上,在 Zookeeper 中,每個父節點都會為它的第一級子節點維護一份順序,用於記錄下每個子節點建立的先後順序。基於這個順序特性,在建立子節點的時候,可以設定這個標記,那麼在建立節點過程中,Zookeeper 會自動為給定節點名加上一個數字字尾,作為一個新的、完整的節點名。另外需要注意的是,這個數字字尾的上限是整型的最大值。 |
臨時節點(EPHEMERAL) | 臨時節點的生命週期和客戶端的會話繫結在一起,如果客戶端會話失效,那麼這個節點就會被自動清理掉。另外,Zookeeper 規定了不能基於臨時節點來建立子節點,即臨時節點只能作為葉子節點。 |
臨時順序節點(EPHEMERAL_SEQUENTIAL) | 基本特性和臨時節點一致,只是新增了順序的特性。 |
1.3、狀態資訊
每個資料節點中除了儲存了資料內容之外,還儲存了資料節點本身的一些狀態資訊(State)。
狀態屬性 | 說明 |
---|---|
cZxid | 即 Create ZXID,表示該資料節點被建立時的事務ID。 |
ctime | 即 Create Time,表示該資料節點被建立的時間。 |
mZxid | 即 Modified ZXID,表示該節點最後一次被更新時的事務ID。 |
mtime | 即 Modified Time,表示該資料節點最後一次被更新的時間。 |
pZxid | 表示該節點的子節點列表最後一次被修改時的事務ID。注意,只有子節點列表變更了才會變更 pZxid,子節點內容變更不會影響pZxid。 |
cversion | 表示子節點的版本號。 |
dataVersion | 表示資料節點的版本號。 |
aclVersion | 表示節點的 ACL 版本號。 |
ephemeralOwner | 建立該臨時節點的會話的sessionID。如果該節點是持久節點,那麼這個屬性值為0。 |
dataLength | 表示資料內容的長度。 |
numChildren | 表示當前節點的子節點個數。 |
1.4、ZXID
在Zookeeper 中,事務是指能夠改變 Zookeeper 伺服器狀態的操作,我們也稱之為事務操作或更新操作,一般包括資料節點建立與刪除、資料節點內容更新和客戶端會話建立與失效等操作。對於每一個事務請求,Zookeeper 都會為其分配一個全域性唯一的事務ID,用 ZXID 來表示,通常是一個 64 位的數字。每一個 ZXID 對應一次更新操作,從這些 ZXID 中可以間接地識別出 Zookeeper 處理這些更新操作請求的全域性順序。
ZXID 是一個 64 位的數字,其中低 32 位可以看作是一個簡單的單調遞增的計數器,針對客戶端的每一個事務請求,Leader 伺服器在產生一個新的事務 Proposal 的時候,都會對該計數器進行加 1 操作;而高 32 位則代表了 Leader 週期 epoch 的編號,每當選舉產生一個新的 Leader 伺服器,就會從這個 Leader 伺服器上取出其本地日誌中最大事務 Proposal 的 ZXID,並從該 ZXID 中解析出對應的 epoch 值,然後再對其進行加 1 操作,之後就會以此編號作為新的 epoch,並將低 32 位置 0 來開始生成新的 ZXID。
1.5、版本
Zookeeper 中為資料節點引入了版本的概念,每個資料節點都具有三種型別的版本資訊(在上面的狀態資訊中已經介紹了三種版本資訊代表的意思),對資料節點的任何更新操作都會引起版本號的變化。其中我們以 dataVersion 為例來說明。在一個資料節點被建立完畢之後,節點的dataVersion 值是 0,表示的含義是 ”當前節點自從建立之後,被更新過 0 次“。如果現在對該節點的資料內容進行更新操作,那麼隨後,dataVersion 的值就會變成 1。即表示的是對資料節點的資料內容的變更次數。
版本的作用是用來實現樂觀鎖機制中的 “寫入校驗” 的。例如,當要修改資料節點的資料內容時,帶上版本號,如果資料節點的版本號與傳入的版本號相等,就進行修改,否則修改失敗。
1.6、Watcher
1.6.1、概述
Zookeeper 提供了分散式資料的釋出/訂閱功能。一個典型的釋出/訂閱模型系統定義了一種一對多的訂閱關係,能夠讓多個訂閱者同時監聽某一個主題物件,當這個主題物件自身狀態變化時,會通知所有訂閱者,使它們能夠做出相應的處理。在 Zookeeper 中,引入了 Watcher 機制來實現這種分散式的通知功能。Zookeeper 允許客戶端向服務端註冊一個 Watcher 監聽,當服務端的一些指定事件觸發了這個 Watcher,那麼就會向指定客戶端傳送一個事件通知來實現分散式的通知功能。
從上圖可以看出 Zookeeper 的 Watcher 機制主要包括客戶端執行緒、客戶端WatchMananger 和 Zookeeper 伺服器三部分。在具體工作流程上,簡單地講,客戶端在向 Zookeeper 伺服器註冊 Watcher 的同時,會將 Watcher 物件儲存在客戶端的 WatchMananger 中。當 Zookeeper 伺服器端觸發 Watcher 事件後,會向客戶端傳送通知,客戶端執行緒從 WatchManager 中取出對應的 Watcher 物件來執行回撥邏輯。
1.6.2、Watcher特性
- **一次性:**表示無論是服務端還是客戶端,一旦一個 Watcher 被觸發,Zookeeper 都會將其從相應的儲存中移除。因此,開發人員在 Watcher 的使用上要記住的一點是需要反覆註冊。
- **客戶端序列執行:**客戶端 Watcher 回撥的過程是一個序列同步的過程,這為我們保證了順序,同時,需要開發人員注意的一點是,千萬不要因為一個 Watcher 的處理邏輯影響了整個客戶端的 Watcher 回撥。
- **輕量:**WatchedEvent 是 Zookeeper 整個 Watcher 通知機制的最小通知單元,這個資料結構中只包含三部分內容:通知狀態、事件型別和節點路徑。也就是說,Watcher通知非常簡單,只會告訴客戶端發生了事件,而不會說明事件的具體內容。
1.6.3、watcher介面設計
Watcher是一個介面,任何實現了Watcher介面的類就是一個新的Watcher。Watcher內部包含了兩個列舉類:KeeperState、EventType
-
Watcher通知狀態(KeeperState)
KeeperState是客戶端與服務端連線狀態發生變化時對應的通知型別。路徑為org.apache.zookeeper.Watcher.Event.KeeperState,是一個列舉類,其列舉屬性如下:
列舉屬性 | 說明 |
---|---|
SyncConnected | 客戶端與伺服器正常連線時 |
Disconnected | 客戶端與伺服器斷開連線時 |
Expired | 會話session失效時 |
AuthFailed | 身份認證失敗時 |
-
Watcher事件型別(EventType)
EventType是資料節點(znode)發生變化時對應的通知型別。EventType變化時KeeperState永遠處於SyncConnected通知狀態下;當KeeperState發生變化時,EventType永遠為None。其路徑為org.apache.zookeeper.Watcher.Event.EventType,是一個列舉類,列舉屬性如下:
列舉屬性 | 說明 |
---|---|
None | 無 |
NodeCreated | Watcher監聽的資料節點被建立時 |
NodeDeleted | Watcher監聽的資料節點被刪除時 |
NodeDataChanged | Watcher監聽的資料節點內容發生變更時(無論內容資料是否變化) |
NodeChildrenChanged | Watcher監聽的資料節點的子節點列表發生變更時 |
注:客戶端接收到的相關事件通知中只包含狀態及型別等資訊,不包括節點變化前後的具體內容,變化前的資料需業務自身儲存,變化後的資料需呼叫get等方法重新獲取;
1.6.4、捕獲相應的事件
上面講到zookeeper客戶端連線的狀態和zookeeper對znode節點監聽的事件型別,下面我們來講解如何建立zookeeper的watcher監聽。在zookeeper中採用zk.getChildren(path, watch)、zk.exists(path, watch)、zk.getData(path, watcher, stat)這樣的方式為某個znode註冊監聽。
下表以node-x節點為例,說明呼叫的註冊方法和可監聽事件間的關係:
註冊方式 | Created | ChildrenChanged | Changed | Deleted |
---|---|---|---|---|
zk.exists(“/node-x”,watcher) | 可監控 | 可監控 | 可監控 | |
zk.getData(“/node-x”,watcher) | 可監控 | 可監控 | ||
zk.getChildren(“/node-x”,watcher) | 可監控 | 可監控 |
1.7、ACL
Zookeeper 中提供了一套完善的 ACL(Access Control List)許可權控制機制來保障資料的安全。
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 可以建立子節點。 delete d 可以刪除子節點(僅下一級節點)。 read r 可以讀取節點資料或子節點列表。 write w 可以對節點進行更新操作。 admin a 可以設定節點訪問控制列表許可權。
1.7.2、特性
- zooKeeper的許可權控制是基於每個znode節點的,需要對每個節點設定許可權。
- 每個znode支援設定多種許可權控制方案和多個許可權。
- 子節點不會繼承父節點的許可權,客戶端無權訪問某節點,但可能可以訪問它的子節點。
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
-
多種模式授權
同一個節點可以同時使用多種模式授權
[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,該模式提供一個超管可以方便的訪問任何許可權的節點
假設這個超管是: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、伺服器狀態
- looking:尋找leader狀態。當伺服器處於該狀態時,它會認為當前叢集中沒有Leader,因此需要進入 Leader 選舉流程。
- leading:領導者狀態。表明當前伺服器角色是leader。
- following:跟隨者狀態。表明當前伺服器角色是follower。
- observing:觀察者狀態。表明當前伺服器角色是observer。
2.2、伺服器啟動時期的 Leader 選舉
在伺服器叢集初始化階段,我們以 3 臺機器組成的伺服器叢集為例,當有一臺伺服器server1 啟動的時候,它是無法進行 Leader 選舉的,當第二臺機器 server2 也啟動時,此時這兩臺伺服器已經能夠進行互相通訊,每臺機器都試圖找到一個 Leader,於是便進入了 Leader 選舉流程。
-
每個server發出一個投票。由於是初始情況,server1和server2都會將自己作為leader伺服器來進行投票,每次投票會包含所推舉的伺服器的myid和zxid,使用(myid, zxid)來表示,此時server1的投票為(1, 0),server2的投票為(2, 0),然後各自將這個投票發給叢集中其他機器。
-
叢集中的每臺伺服器接收來自叢集中各個伺服器的投票。
-
處理投票。針對每一個投票,伺服器都需要將別人的投票和自己的投票進行pk,pk規則如下
- 優先檢查zxid。zxid比較大的伺服器優先作為leader。
- 如果zxid相同,那麼就比較myid。myid較大的伺服器作為leader伺服器。
對於Server1而言,它的投票是(1, 0),接收Server2的投票為(2, 0),首先會比較兩者的zxid,均為0,再比較myid,此時server2的myid最大,於是更新自己的投票為(2, 0),然後重新投票,對於server2而言,其無須更新自己的投票,只是再次向叢集中所有機器發出上一次投票資訊即可。
-
統計投票。每次投票後,伺服器都會統計投票資訊,判斷是否已經有過半機器接受到相同的投票資訊,對於server1、server2而言,都統計出叢集中已經有兩臺機器接受了(2, 0)的投票資訊,此時便認為已經選出了leader。
-
改變伺服器狀態。一旦確定了leader,每個伺服器就會更新自己的狀態,如果是follower,那麼就變更為following,如果是leader,就變更為leading。
2.3、伺服器執行時期的 Leader 選舉
在zookeeper執行期間,leader與非leader伺服器各司其職,即便當有非leader伺服器當機或新加入,此時也不會影響leader,但是一旦leader伺服器掛了,那麼整個叢集將暫停對外服務,進入新一輪leader選舉,其過程和啟動時期的Leader選舉過程基本一致。
假設正在執行的有server1、server2、server3三臺伺服器,當前leader是server2,若某一時刻leader掛了,此時便開始Leader選舉。選舉過程如下:
- 變更狀態。leader掛後,餘下的非 Observer 伺服器都會將自己的伺服器狀態變更為looking,然後開始進入leader選舉過程。
- 每個server會發出一個投票。在執行期間,每個伺服器上的zxid可能不同,此時假定server1的zxid為123,server3的zxid為122,在第一輪投票中,server1和server3都會投自己,產生投票(1, 123),(3, 122),然後各自將投票傳送給叢集中所有機器。
- 接收來自各個伺服器的投票。
- 處理投票。對於投票的處理,和上面提到的伺服器啟動期間的處理規則是一致的。在這個例子裡面,由於 Server1 的 zxid 為 123,Server3 的 zxid 為 122,那麼顯然,Server1 會成為 Leader。
- 統計投票。
- 改變伺服器狀態。
2.4、Observer 角色及其設定
observer角色特點:
- 不參與叢集的leader選舉
- 不參與叢集中寫資料時的ack反饋
為了使用observer角色,在任何想變成observer角色的配置檔案中加入如下配置:
peerType=observer
並在所有server的配置檔案中,配置成observer模式的server的那行配置追加:observer,例如:
server.3=192.168.60.130:2289:3389:observer
相關文章
- 深入瞭解Zookeeper核心原理
- 深入淺出 ZooKeeper
- ZooKeeper 工作、選舉 原理
- zookeeper使用和原理探究
- Zookeeper watch機制原理
- zookeeper的原理和使用(一)
- 一文搞懂Zookeeper原理
- 深入淺出Zookeeper(七):Leader選舉
- zookeeper的基本原理(二)
- Zookeeper watcher 事件機制原理剖析事件
- Zookeeper ZAB協議原理淺析協議
- ZooKeeper核心原理及應用場景
- Zookeeper基礎概念及相關原理
- Dubbo 中 Zookeeper 註冊中心原理分析
- 深入vuex原理(上)Vue
- ThreadLocal原理深入解析thread
- zookeeper的原理和使用(二)-leader選舉
- Zookeeper客戶端使用與叢集原理客戶端
- zookeeper 分散式鎖的原理及實現分散式
- 深入JavaScript繼承原理JavaScript繼承
- 深入清除浮動原理
- 深入HBase架構原理架構
- 深入瞭解Synchronized原理synchronized
- 啟動Dubbo專案註冊Zookeeper時提示zookeeper not connected異常原理解析
- Zookeeper基礎原理&應用場景詳解
- 深入淺出HTTPS工作原理HTTP
- 深入解讀Quartz的原理quartz
- Docker 深入篇之 Build 原理DockerUI
- 深入理解HTTPS工作原理HTTP
- 【Java面試】Zookeeper中的Watch機制的原理?Java面試
- Redis、Zookeeper實現分散式鎖——原理與實踐Redis分散式
- 深入淺出,ARCore開發原理
- 深入分析 Javac 編譯原理Java編譯原理
- 深入理解:Spring MVC工作原理SpringMVC
- 深入淺出 Viewport 設計原理View
- 深入淺出人臉識別原理
- 深入解析 ResNet:實現與原理
- 深入理解Argo CD工作原理Go