Zookeeper深入原理

Over Coding發表於2020-11-30

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
NodeCreatedWatcher監聽的資料節點被建立時
NodeDeletedWatcher監聽的資料節點被刪除時
NodeDataChangedWatcher監聽的資料節點內容發生變更時(無論內容資料是否變化)
NodeChildrenChangedWatcher監聽的資料節點的子節點列表發生變更時

:客戶端接收到的相關事件通知中只包含狀態及型別等資訊,不包括節點變化前後的具體內容,變化前的資料需業務自身儲存,變化後的資料需呼叫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節點為例,說明呼叫的註冊方法和可監聽事件間的關係:

註冊方式CreatedChildrenChangedChangedDeleted
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 資訊。下面分別介紹:

  1. 許可權模式(Scheme)

    方案說明
    world只有一個使用者:anyone,代表登入 Zookeeper 所有人(預設)
    ip對客戶端使用IP地址認證。
    auth使用已新增認證的使用者認證。
    digest使用“使用者名稱:密碼”方式認證。
  2. 授權物件(ID)

    授權物件ID是指,許可權賦予的實體,例如:IP 地址或使用者。

  3. 許可權(Permission)

    許可權ACL簡寫描述
    createc可以建立子節點。
    deleted可以刪除子節點(僅下一級節點)。
    readr可以讀取節點資料或子節點列表。
    writew可以對節點進行更新操作。
    admina可以設定節點訪問控制列表許可權。
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 選舉流程。

  1. 每個server發出一個投票。由於是初始情況,server1和server2都會將自己作為leader伺服器來進行投票,每次投票會包含所推舉的伺服器的myid和zxid,使用(myid, zxid)來表示,此時server1的投票為(1, 0),server2的投票為(2, 0),然後各自將這個投票發給叢集中其他機器。

  2. 叢集中的每臺伺服器接收來自叢集中各個伺服器的投票。

  3. 處理投票。針對每一個投票,伺服器都需要將別人的投票和自己的投票進行pk,pk規則如下

    • 優先檢查zxid。zxid比較大的伺服器優先作為leader。
    • 如果zxid相同,那麼就比較myid。myid較大的伺服器作為leader伺服器。

    ​ 對於Server1而言,它的投票是(1, 0),接收Server2的投票為(2, 0),首先會比較兩者的zxid,均為0,再比較myid,此時server2的myid最大,於是更新自己的投票為(2, 0),然後重新投票,對於server2而言,其無須更新自己的投票,只是再次向叢集中所有機器發出上一次投票資訊即可。

  4. 統計投票。每次投票後,伺服器都會統計投票資訊,判斷是否已經有過半機器接受到相同的投票資訊,對於server1、server2而言,都統計出叢集中已經有兩臺機器接受了(2, 0)的投票資訊,此時便認為已經選出了leader。

  5. 改變伺服器狀態。一旦確定了leader,每個伺服器就會更新自己的狀態,如果是follower,那麼就變更為following,如果是leader,就變更為leading。

2.3、伺服器執行時期的 Leader 選舉

在zookeeper執行期間,leader與非leader伺服器各司其職,即便當有非leader伺服器當機或新加入,此時也不會影響leader,但是一旦leader伺服器掛了,那麼整個叢集將暫停對外服務,進入新一輪leader選舉,其過程和啟動時期的Leader選舉過程基本一致。

假設正在執行的有server1、server2、server3三臺伺服器,當前leader是server2,若某一時刻leader掛了,此時便開始Leader選舉。選舉過程如下:

  1. 變更狀態。leader掛後,餘下的非 Observer 伺服器都會將自己的伺服器狀態變更為looking,然後開始進入leader選舉過程。
  2. 每個server會發出一個投票。在執行期間,每個伺服器上的zxid可能不同,此時假定server1的zxid為123,server3的zxid為122,在第一輪投票中,server1和server3都會投自己,產生投票(1, 123),(3, 122),然後各自將投票傳送給叢集中所有機器。
  3. 接收來自各個伺服器的投票。
  4. 處理投票。對於投票的處理,和上面提到的伺服器啟動期間的處理規則是一致的。在這個例子裡面,由於 Server1 的 zxid 為 123,Server3 的 zxid 為 122,那麼顯然,Server1 會成為 Leader。
  5. 統計投票。
  6. 改變伺服器狀態。

2.4、Observer 角色及其設定

observer角色特點:

  1. 不參與叢集的leader選舉
  2. 不參與叢集中寫資料時的ack反饋

為了使用observer角色,在任何想變成observer角色的配置檔案中加入如下配置:

peerType=observer

並在所有server的配置檔案中,配置成observer模式的server的那行配置追加:observer,例如:

server.3=192.168.60.130:2289:3389:observer

相關文章