ZooKeeper解惑

最近針對ZK一些比較疑惑的問題,再看了一下相關代碼,列舉以下。這裏只列官方文檔中沒有的,或者不清晰的。以zookeeper-3.3.3爲基準。如下用ZK表示ZooKeeper。java

一個ZooKeeper對象,表明一個ZK Client。應用經過ZooKeeper對象中的讀寫API與ZK集羣進行交互。一個簡單的建立一條數據的例子,只需以下兩行代碼:node

ZooKeeper zk = new ZooKeeper(serverList, sessionTimeout, watcher);
zk.create("/test", new byte[0], Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);

Client和ZK集羣的鏈接和Session的創建過程

ZooKeeper對象一旦建立,就會啓動一個線程(ClientCnxn)去鏈接ZK集羣。ZooKeeper內部維護了一個Client端狀態。web

 

public enum States {
        CONNECTING, ASSOCIATING, CONNECTED, CLOSED, AUTH_FAILED;
        …}


第一次鏈接ZK集羣時,首先將狀態置爲CONNECTING,而後挨個嘗試鏈接serverlist中的每一臺Server。Serverlist在初始化時,順序已經被隨機打亂:
Collections.shuffle(serverAddrsList)
這樣能夠避免多個client以一樣的順序重連server。重連的間隔毫秒數是0-1000之間的一個隨機數。
一旦鏈接上一臺server,首先發送一個ConnectRequest包,將ZooKeeper構造函數傳入的sessionTimeout數值發動給Server。ZooKeeper Server有兩個配置項:算法

 

minSessionTimeout 單位毫秒。默認2倍tickTime
maxSessionTimeout 單位毫秒。默認20倍tickTime
(tickTime也是一個配置項。是Server內部控制時間邏輯的最小時間單位)apache

若是客戶端發來的sessionTimeout超過min-max這個範圍,server會自動截取爲min或max,而後爲這個Client新建一個Session對象。Session對象包含sessionId、timeout、tickTime三個屬性。其中sessionId是Server端維護的一個原子自增long型(8字節)整數;啓動時Leader將其初始化爲1個字節的leader Server Id+當前時間的後5個字節+2個字節的0;這個能夠保證在leader切換中,sessionId的惟一性(只要leader兩次切換爲同一個Server的時間間隔中session創建數不超過( 2的16次方)*間隔毫秒數。。。不可能達到的數值)。api


ZK Server端維護以下3個Map結構,Session建立後相關數據分別放入這三個Map中:安全

 

Map<Long[sessionId], Session> sessionsById
Map<Long[sessionId], Integer> sessionsWithTimeout
Map<Long[tickTime], SessionSet> sessionSets服務器

其中sessionsById簡單用來存放Session對象及校驗sessionId是否過時。sessionsWithTimeout用來維護session的持久化:數據會寫入snapshot,在Server重啓時會從snapshot恢復到sessionsWithTimeout,從而可以維持跨重啓的session狀態。session

Session對象的tickTime屬性表示session的過時時間。sessionSets這個Map會以過時時間爲key,將全部過時時間相同的session收集爲一個集合。Server每次接到Client的一個請求或者心跳時,會根據當前時間和其sessionTimeout從新計算過時時間並更新Session對象和sessionSets。計算出的過時時間點會向上取整爲ZKServer的屬性tickTime的整數倍。Server啓動時會啓動一個獨立的線程負責將大於當前時間的全部tickTime對應的Session所有清除關閉。數據結構

Leader收到鏈接請求後,會發起一個createSession的Proposal,若是表決成功,最終全部的Server都會在其內存中創建一樣的Session,並做一樣的過時管理。等表決經過後,與客戶端創建鏈接的Server爲這個session生成一個password,連同sessionId,sessionTimeOut一塊兒返回給客戶端(ConnectResponse)。客戶端若是須要重連Server,能夠新建一個ZooKeeper對象,將上一個成功鏈接的ZooKeeper 對象的sessionId和password傳給Server
ZooKeeper zk = new ZooKeeper(serverList, sessionTimeout, watcher, sessionId,passwd);
ZKServer會根據sessionId和password爲同一個client恢復session,若是尚未過時的話。

Server生成password的算法比較有意思:

new Random(sessionId ^ superSecret).nextBytes(byte[] passwd)

superSecret是一個固定的常量。Server不保存password,每次在返回client的ConnectRequest響應時計算生成。在客戶端重連時再從新計算,與傳入的password做比較。由於Random相同的seed隨機生成的序列是徹底相同的!

Client發送完ConnectRequest包,會緊接着發送authInfo包(OpCode.auth)和setWatches 包OpCode.setWatches;authInfo列表由ZooKeeper的addAuthInfo()方法添加,用來進行自定義的認證和受權。

最後當zookeeper.disableAutoWatchReset爲false時,若創建鏈接時ZooKeeper註冊的Watcher不爲空,那麼會經過setWatches告訴ZKServer從新註冊這些Watcher。這個用來在Client自動切換ZKServer或重練時,還沒有觸發的Watcher可以帶到新的Server上

以上是鏈接初始化的時候作的事情。

關於ACL

以前看到不少例子裏
zk.create(「/test」, new byte[0], Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
中Ids.OPEN_ACL_UNSAFE的地方用 Ids.CREATOR_ALL_ACL,在zookeeper-3.3.3上面跑直接就掛了,報下面的錯:
org.apache.zookeeper.KeeperException$InvalidACLException: KeeperErrorCode = InvalidACL for /test
at org.apache.zookeeper.KeeperException.create(KeeperException.java:112)
at org.apache.zookeeper.KeeperException.create(KeeperException.java:42)
at org.apache.zookeeper.ZooKeeper.create(ZooKeeper.java:637)

是由於3.3.3的ACL進行了細微的調整。先來看下ACL的數據結構:
每個znode節點上均可以設置一個訪問控制列表,數據結構爲List

 

ACL
+–perms int (allow What)
+–id Id
    +–scheme String (Who)
    +–id String      (How)


一個ACL對象就是一個Id和permission對,用來表示哪一個/哪些範圍的Id(Who)在經過了怎樣的鑑權(How)以後,就容許進行那些操做(What):Who How What;permission(What)就是一個int表示的位碼,每一位表明一個對應操做的容許狀態。相似unix的文件權限,不一樣的是共有5種操做:CREATE、READ、WRITE、DELETE、ADMIN(對應更改ACL的權限);Id由scheme(Who)和一個具體的字符串鑑權表達式id(How)構成,用來描述哪一個/哪些範圍的Id應該怎樣被鑑權。Scheme事實上是所使用的鑑權插件的標識。id的具體格式和語義由scheme對應的鑑權實現決定。不論是內置仍是自定義的鑑權插件都要實現AuthenticationProvider接口(如下簡稱AP)。自定義的鑑權插件由zookeeper.authProvider開頭的系統屬性指定其類名,例如:
authProvider.1=com.f.MyAuth
authProvider.2=com.f.MyAuth2
AP接口的getScheme()方法定義了其對應的scheme

 

客戶端與Server創建鏈接時,會將ZooKeeper.addAuthInfo()方法添加的每一個authInfo都發送給ZKServer。

 

void addAuthInfo(String scheme, byte auth[])


addAuthInfo 方法自己也會直接將authInfo發送給ZKServer。ZKServer接受到authInfo請求後,首先根據scheme找到對應的AP,而後調用其handleAuthentication()方法將auth數據傳入。對應的AP將auth數據解析爲一個Id,將其加入鏈接上綁定的authInfo列表(List)中。Server在接入客戶端鏈接時,首先會自動在鏈接上加上一個默認的scheme爲ip的authIndo:authInfo.add(new Id(「ip」, client-ip));

 

鑑權時調用AP的matches()方法判斷進行該操做的當前鏈接上綁定的authInfo是否與所操做的znode的ACL列表匹配。

ZK有4個內置的scheme:

• world 只有一個惟一的id:anyone;表示任何人均可以作對應的操做。這個scheme沒有對應的鑑權實現。只要一個znode的ACL list中包含有這個scheme的Id,其對應的操做就運行執行
• auth 沒有對應的id,或者只有一個空串」」id。這個scheme沒有對應的鑑權實現。語義是當前鏈接綁定的適合作建立者鑑權的autoInfo (經過調用autoInfo的scheme對應的AP的isAuthenticated()得知)都擁有對應的權限。遇到這個auth後,Server會根據當前鏈接綁定的符合要求的autoInfo生成ACL加入到所操做znode的acl列表中。
• digest 使用username:password格式的字符串生成MD5 hash 做爲ACL ID。 具體格式爲:username:base64 encoded SHA1 password digest.對應內置的鑑權插件:DigestAuthenticationProvider
• ip 用IP通配符匹配客戶端ip。對應內置鑑權插件IPAuthenticationProvider

只有兩類API會改變Znode的ACL列表:一個是create(),一個是setACL()。因此這兩個方法都要求傳入一個List。Server接到這兩種更新請求後,會判斷指定的每個ACL中,scheme對應的AuthenticationProvider是否存在,若是存在,調用其isValid(String)方法判斷對應的id表達式是否合法。。。具體參見PrepRequestProcessor.fixupACL()方法。上文的那個報錯是由於CREATOR_ALL_ACL只包含一個ACL : Perms.ALL, Id(「auth」, 「」),而auth要求將鏈接上適合作建立者鑑權的autoInfo都加入節點的acl中,而此時鏈接上只有一個默認加入的Id(「ip」, client-ip),其對應的IPAuthenticationProvider的isAuthenticated()是返回false的,表示不用來鑑權node的建立者。
tbd:具體例子

關於Watcher

先來看一下ZooKeeper的API: 讀API包括exists,getData,getChildren四種

 

Stat exists(String path, Watcher watcher)
Stat exists(String path, boolean watch)
void exists(String path, Watcher watcher, StatCallback cb, Object ctx)
void exists(String path, boolean watch  , StatCallback cb, Object ctx)

 

byte[] getData(String path, Watcher watcher, Stat stat)
byte[] getData(String path, boolean watch , Stat stat)
void getData(String path, Watcher watcher, DataCallback cb, Object ctx)
void getData(String path, boolean watch , DataCallback cb, Object ctx)

List<String> getChildren(String path, Watcher watcher)
List<String> getChildren(String path, boolean watch )
void getChildren(String path, Watcher watcher, ChildrenCallback cb, Object ctx)
void getChildren(String path, boolean watch , ChildrenCallback cb, Object ctx)

List<String> getChildren(String path, Watcher watcher, Stat stat)
List<String> getChildren(String path, boolean watch , Stat stat)
void getChildren(String path, Watcher watcher, Children2Callback cb, Object ctx)
void getChildren(String path, boolean watch , Children2Callback cb, Object ctx)

每一種按同步仍是異步,添加指定watcher仍是默認watcher又分爲4種。默認watcher是隻在ZooKeeper zk = new ZooKeeper(serverList, sessionTimeout, watcher)中指定的watch。若是包含boolean watch的讀方法傳入true則將默認watcher註冊爲所關注事件的watch。若是傳入false則不註冊任何watch

寫API包括create、delete、setData、setACL四種,每一種根據同步仍是異步又分爲兩種:

 

String create(String path, byte data[], List<ACL> acl, CreateMode createMode)
void   create(String path, byte data[], List<ACL> acl, CreateMode createMode, StringCallback cb, Object ctx)

 

void delete(String path, int version)
void delete(String path, int version, VoidCallback cb, Object ctx)

Stat setData(String path, byte data[], int version)
void setData(String path, byte data[], int version, StatCallback cb, Object ctx)

Stat setACL(String path, List<ACL> acl, int version)
void setACL(String path, List<ACL> acl, int version, StatCallback cb, Object ctx)

一個讀寫交互,或者說pub/sub的簡單描述以下圖:

更詳細一點:

可見Watcher機制的輕量性:通知的只是事件。Client和server端額外傳輸的只是個boolean值。對於讀寫api操做來講,path和eventType的信息自己就有了。只有在notify的時候才須要加上path、eventType的信息。內部存儲上,Server端只維護一個Map(固然會根據watcher的類型分爲兩個),key爲path,value爲自己以及存在的鏈接對象。因此存儲上也不是負擔。不會隨着watcher的增長無限制的增大

Watcher的一次性設計也大大的減輕了服務器的負擔和風險。假設watcher不是一次性,那麼在更新很頻繁的時候,大量的通知要不要丟棄?精簡?併發怎麼處理?都是一個問題。一次性的話,這些問題就都丟給了Client端。而且Client端事實上並不須要每次密集更新都去處理。

若是一個znode上大量Client都註冊了watcher,那麼觸發的時候是串行的。這裏可能會有一些延遲。

關於Log文件和snapshot

Follower/Leader每接收到一個PROPOSAL消息以後,都會寫入log文件。log文件的在配置項dataLogDir指定的目錄下面。文件名爲log.+第一條記錄對應的zxid

[linxuan@test036081 conf]$ ls /usr/zkdataLogDir/version-2/
log.100000001 log.200000001

ZooKeeper在每次寫入log文件時會作檢查,當文件剩餘大小不足4k的時候,默認會一次性預擴展64M大小。這個預擴展大小能夠經過系統屬性zookeeper.preAllocSize或配置參數preAllocSize指定,單位爲K;

會爲每條記錄計算checksum,放在實際數據前面

每寫1000條log作一次flush(調用BufferedOutputStream.flush()和FileChannel.force(false))。這個次數直到3.3.3都是硬編碼的,沒法配置

每當log寫到必定數目時,ZooKeeper會將當前數據的快照輸出爲一個snapshot文件:

 

randRoll = Random.nextInt(snapCount/2);
      if (logCount > (snapCount / 2 + randRoll)) {
           rollLog();
           take_a_snapshot_in_a_new_started_thread();
      }


這個randRoll是一個隨機數,爲了不幾臺Zk Server在同一時間都作snapshot
輸出快照的log數目閥值snapCount能夠經過zookeeper.snapCount系統屬性設置,默認是100000條。輸出snapshot文件的操做在新建立的單獨線程裏進行。任一時刻只會有一個snapshot線程。Snapshot文件在配置項dataDir指定的目錄下面生成,命名格式爲snapshot.+最後一個更新的zxid。

 

如指定dataDir=/home/linxuan/zookeeper-3.3.3/data,則snapshot文件爲:
[linxuan@test036081 version-2]$ ls /home/linxuan/zookeeper-3.3.3/data/version-2
snapshot.0 snapshot.100000002

每一個snapshot文件也都會寫入所有數據的一個checksum。

ZK在每次啓動snapshot線程前都會將當前的log文件刷出,在下次寫入時建立一個新的log文件。無論當前的log文件有沒有寫滿。舊的log文件句柄會在下一次commit(也就是flush的時候)再順便關閉。

因此這種機制下,log文件會有必定的空間浪費,大多狀況下會沒有寫滿就換到下一個文件了。能夠經過調整preAllocSize和snapCount兩個參數來減小這種浪費。可是定時自動刪除沒用的log文件仍是必須的,只保留最新的便可。

爲了保證消息的安全,排隊的消息在沒有flush到log文件以前不會提交到下一個環節。而爲了提升log文件寫入的效率,又必須作批量flush。因此更新消息實際上也是和批量flushlog文件的操做一塊兒,批量提交到下一個協議環節的。當請求比較少時(包括讀請求),每一個更新會很快刷出,即便沒有寫夠1000條。當請求壓力很大時,纔會一直等堆積到1000條才刷出log文件,同時送出消息到下一個環節。這裏的實現比較細緻,實質上是在壓力大時,不光是寫log,連同消息處理都作了一個批量操做。具體實現細節在SyncRequestProcessor中

Client和ZK集羣的完整交互

ZK總體上來講,經過單線程和大量的隊列來達到消息在集羣內完成一致性協議的狀況下,仍然能保證全局順序。下面是一個線程和queue的全景圖:

這個圖中,除了個別的以外,每一個節點都要麼表明一個Thread,要麼表明一個queue

其餘

ZKServer內部經過大量的queue來處理消息,保證順序。這些queue的大小自己都不設上限。有一個配置屬性globalOutstandingLimit用來指定Server的最大請求堆積數。ZKServer在讀入消息時若是發覺內部的全局消息計數大於這個值,就會直接關閉當前鏈接上的讀取來保護服務端。(取消與當前Client的Nio鏈接上的讀取事件註冊)

相關文章
相關標籤/搜索