Zookeeper深入原理

1、系統(tǒng)模型

1.1、數(shù)據(jù)模型

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

zookeeper數(shù)據(jù)結構.png

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)分布式的通知功能。

Watcher.png

從上圖可以看出 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 信息。下面分別介紹:

  1. 權限模式(Scheme)

    方案 說明
    world 只有一個用戶:anyone,代表登錄 Zookeeper 所有人(默認)
    ip 對客戶端使用IP地址認證。
    auth 使用已添加認證的用戶認證。
    digest 使用“用戶名:密碼”方式認證。
  1. 授權對象(ID)

    授權對象ID是指,權限賦予的實體,例如:IP 地址或用戶。

  1. 權限(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 選舉流程。

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

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

  3. 處理投票。針對每一個投票,服務器都需要將別人的投票和自己的投票進行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ā)出上一次投票信息即可。

  4. 統(tǒng)計投票。每次投票后,服務器都會統(tǒng)計投票信息,判斷是否已經有過半機器接受到相同的投票信息,對于server1、server2而言,都統(tǒng)計出集群中已經有兩臺機器接受了(2, 0)的投票信息,此時便認為已經選出了leader。

  5. 改變服務器狀態(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選舉。選舉過程如下:

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

2.4、Observer 角色及其設置

observer角色特點:

  1. 不參與集群的leader選舉
  2. 不參與集群中寫數(shù)據(jù)時的ack反饋

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

peerType=observer

并在所有server的配置文件中,配置成observer模式的server的那行配置追加:observer,例如:

server.3=192.168.60.130:2289:3389:observer
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容