ZooKeeper的基本原理
ZNode的基本概念
ZooKeeper數據模型的結構與Unix文件系統很類似,整體上可以看作是一棵樹,每個節點稱做一個ZNode。每個ZNode都可以通過其路徑唯一標識,在每個ZNode上可存儲少量數據(默認是1M, 可以通過配置修改, 通常不建議在ZNode上存儲大量的數據)。另外,每個ZNode上還存儲了其Acl信息,這里需要注意,雖說ZNode的樹形結構跟Unix文件系統很類似,但是其Acl與Unix文件系統是完全不同的,每個ZNode的Acl是獨立的,子結點不會繼承父結點的。
ZNode根據其本身的特性,可以分為下面兩類:
- Regular ZNode: 常規型ZNode, 用戶需要顯式的創建、刪除
- Ephemeral ZNode: 臨時型ZNode, 用戶創建它之后,可以顯式的刪除,也可以在創建它的Session結束后,由ZooKeeper Server自動刪除
Zookeeper這種數據結構有如下這些特點:
1)每個子目錄項如NameService都被稱作為znode,這個znode是被它所在的路徑唯一標識,如Server1這個znode的標識為/NameService/Server1。
2)znode可以有子節點目錄,並且每個znode可以存儲數據,注意EPHEMERAL(臨時的)類型的目錄節點不能有子節點目錄。ZNode一個Sequential的特性,如果創建的時候指定的話,該ZNode的名字后面會自動Append一個不斷增加的SequenceNo。
3)znode是有版本的(version),每個znode中存儲的數據可以有多個版本,也就是一個訪問路徑中可以存儲多份數據,version號自動增加。
4)znode可以是臨時節點(EPHEMERAL),可以是持久節點(PERSISTENT)。如果創建的是臨時節點,一旦創建這個EPHEMERALznode的客戶端與服務器失去聯系,這個znode也將自動刪除,Zookeeper的客戶端和服務器通信采用長連接方式,每個客戶端和服務器通過心跳來保持連接,這個連接狀態稱為session,如果znode是臨時節點,這個session失效,znode也就刪除了。
5)znode的目錄名可以自動編號,如App1已經存在,再創建的話,將會自動命名為App2。
6)znode可以被監控,包括這個目錄節點中存儲的數據的修改,子節點目錄的變化等,一旦變化可以通知設置監控的客戶端,這個是Zookeeper的核心特性,Zookeeper的很多功能都是基於這個特性實現的。Watcher ZooKeeper支持一種Watch操作,Client可以在某個ZNode上設置一個Watcher,來Watch該ZNode上的變化。如果該ZNode上有相應的變化,就會觸發這個Watcher,把相應的事件通知給設置Watcher的Client。需要注意的是,ZooKeeper中的Watcher是一次性的,即觸發一次就會被取消,如果想繼續Watch的話,需要客戶端重新設置Watcher。這個跟epoll里的oneshot模式有點類似。
7)ZXID:每次對Zookeeper的狀態的改變都會產生一個zxid(ZooKeeper Transaction Id),zxid是全局有序的,如果zxid1小於zxid2,則zxid1在zxid2之前發生。
8)Session: Client與ZooKeeper之間的通信,需要創建一個Session,這個Session會有一個超時時間。因為ZooKeeper集群會把Client的Session信息持久化,所以在Session沒超時之前,Client與ZooKeeper Server的連接可以在各個ZooKeeper Server之間透明地移動。在實際的應用中,如果Client與Server之間的通信足夠頻繁,Session的維護就不需要其它額外的消息了。否則,ZooKeeper Client會每t/3 ms發一次心跳給Server,如果Client 2t/3 ms沒收到來自Server的心跳回應,就會換到一個新的ZooKeeper Server上。這里t是用戶配置的Session的超時時間。

(client不論連接到哪個Server,展示給它都是同一個視圖,這是zookeeper最重要的性能。)
ACL介紹
ACL分為兩個維度,一個是屬組,一個是權限,子目錄/文件默認繼承父目錄的ACL。而在Zookeeper中,node的ACL是沒有繼承關系的,是獨立控制的。Zookeeper的ACL,可以從三個維度來理解:一是scheme; 二是user; 三是permission,通常表示為scheme:id:permissions。
(1)scheme: scheme對應於采用哪種方案來進行權限管理,zookeeper實現了一個pluggable的ACL方案,可以通過擴展scheme,來擴展ACL的機制。zookeeper-3.4.4缺省支持下面幾種scheme:
- world: 它下面只有一個id, 叫anyone, world:anyone代表任何人,zookeeper中對所有人有權限的結點就是屬於world:anyone的
- auth: 它不需要id, 只要是通過authentication的user都有權限(zookeeper支持通過kerberos來進行authencation, 也支持username/password形式的authentication)
- digest: 它對應的id為username:BASE64(SHA1(password)),它需要先通過username:password形式的authentication
- ip: 它對應的id為客戶機的IP地址,設置的時候可以設置一個ip段,比如ip:192.168.1.0/16, 表示匹配前16個bit的IP段
- super: 在這種scheme情況下,對應的id擁有超級權限,可以做任何事情(cdrwa)
另外,zookeeper-3.4.4的代碼中還提供了對sasl的支持,不過缺省是沒有開啟的,需要配置才能啟用。
- sasl: sasl的對應的id,是一個通過sasl authentication用戶的id,zookeeper-3.4.4中的sasl authentication是通過kerberos來實現的,也就是說用戶只有通過了kerberos認證,才能訪問它有權限的node.
(2)id: id與scheme是緊密相關的。
(3)permission: zookeeper目前支持下面一些權限:
- CREATE(c): 創建權限,可以在在當前node下創建child node
- DELETE(d): 刪除權限,可以刪除當前的node
- READ(r): 讀權限,可以獲取當前node的數據,可以list當前node所有的child nodes
- WRITE(w): 寫權限,可以向當前node寫數據
- ADMIN(a): 管理權限,可以設置當前node的permission
具體來說就是每種scheme對應於一種ACL機制,可以通過擴展scheme來擴展ACL的機制。在具體的實現中,每種scheme對應一種AuthenticationProvider。每種AuthenticationProvider實現了當前機制下authentication的檢查,通過了authentication的檢查,然后再進行統一的permission檢查,如此便實現了ACL。所有的AuthenticationProvider都注冊在ProviderRegistry中,新擴展的AuthenticationProvider可以通過配置注冊到ProviderRegistry中去。下面是實施檢查的具體實現:
void checkACL(ZooKeeperServer zks, List<acl> acl, int perm, List<id> ids) throws KeeperException.NoAuthException { if (skipACL) { return; } if (acl == null || acl.size() == 0) { return; } for (Id authId : ids) { if (authId.getScheme().equals("super")) { return; } } for (ACL a : acl) { Id id = a.getId(); if ((a.getPerms() & perm) != 0) { if (id.getScheme().equals("world") && id.getId().equals("anyone")) { return; } AuthenticationProvider ap = ProviderRegistry.getProvider(id .getScheme()); if (ap != null) { for (Id authId : ids) { if (authId.getScheme().equals(id.getScheme()) && ap.matches(authId.getId(), id.getId())) { return; } } } } } throw new KeeperException.NoAuthException(); } </id></acl>
可以通過下面兩種方式把新擴展的AuthenticationProvider注冊到ProviderRegistry:
配置文件:在zookeeper的配置文件中,加入authProvider.$n=$classname即可
JVM參數:啟動Zookeeper的時候,通過-Dzookeeper.authProvider.$n=$classname的方式,把AuthenticaitonProvider傳入
在上面的配置中, $n是為了區分不同的provider的一個序號,只要保證不重復即可,沒有實際的意義,通常用數字1,2,3等
可以通過zookeeper client來管理ACL, zookeeper的發行包中提供了一個cli工具zkcli.sh,可以通過它來進行acl管理(使用自行百度)
讀寫模式
在ZooKeeper集群中,讀可以從任意一個ZooKeeper Server讀,這一點是保證ZooKeeper比較好的讀性能的關鍵;寫的請求會先Forwarder到Leader,然后由Leader來通過ZooKeeper中的原子廣播協議,將請求廣播給所有的Follower,Leader收到一半以上的寫成功的Ack后,就認為該寫成功了,就會將該寫進行持久化,並告訴客戶端寫成功了。
WAL和Snapshot
和大多數分布式系統一樣,ZooKeeper也有WAL(Write-Ahead-Log),對於每一個更新操作,ZooKeeper都會先寫WAL, 然后再對內存中的數據做更新,然后向Client通知更新結果。另外,ZooKeeper還會定期將內存中的目錄樹進行Snapshot,落地到磁盤上,這個跟HDFS中的FSImage是比較類似的。這么做的主要目的,一當然是數據的持久化,二是加快重啟之后的恢復速度,如果全部通過Replay WAL的形式恢復的話,會比較慢。
FIFO
對於每一個ZooKeeper客戶端而言,所有的操作都是遵循FIFO順序的,這一特性是由下面兩個基本特性來保證的:一是ZooKeeper Client與Server之間的網絡通信是基於TCP,TCP保證了Client/Server之間傳輸包的順序;二是ZooKeeper Server執行客戶端請求也是嚴格按照FIFO順序的。
Linearizability(線性一致性)
在ZooKeeper中,所有的更新操作都有嚴格的偏序關系,更新操作都是串行執行的,這一點是保證ZooKeeper功能正確性的關鍵。
ZooKeeper Session
Client和Zookeeper集群建立連接,整個session狀態變化如圖所示:

如果Client因為Timeout和Zookeeper Server失去連接,client處在CONNECTING狀態,會自動嘗試再去連接Server,如果在session有效期內再次成功連接到某個Server,則回到CONNECTED狀態。
注意:如果因為網絡狀態不好,client和Server失去聯系,client會停留在當前狀態,會嘗試主動再次連接Zookeeper Server。client不能宣稱自己的session expired,session expired是由Zookeeper Server來決定的,client可以選擇自己主動關閉session。
ZooKeeper Watch
Zookeeper watch是一種監聽通知機制。Zookeeper所有的讀操作getData(), getChildren()和 exists()都可以設置監視(watch),監視事件可以理解為一次性的觸發器,官方定義如下: a watch event is one-time trigger, sent to the client that set the watch, whichoccurs when the data for which the watch was set changes。Watch的三個關鍵點:
*(一次性觸發)One-time trigger
當設置監視的數據發生改變時,該監視事件會被發送到客戶端,例如,如果客戶端調用了getData("/znode1", true) 並且稍后 /znode1 節點上的數據發生了改變或者被刪除了,客戶端將會獲取到 /znode1 發生變化的監視事件,而如果 /znode1 再一次發生了變化,除非客戶端再次對/znode1 設置監視,否則客戶端不會收到事件通知。
*(發送至客戶端)Sent to the client
Zookeeper客戶端和服務端是通過 socket 進行通信的,由於網絡存在故障,所以監視事件很有可能不會成功地到達客戶端,監視事件是異步發送至監視者的,Zookeeper 本身提供了順序保證(ordering guarantee):即客戶端只有首先看到了監視事件后,才會感知到它所設置監視的znode發生了變化(a client will never see a change for which it has set a watch until it first sees the watch event)。網絡延遲或者其他因素可能導致不同的客戶端在不同的時刻感知某一監視事件,但是不同的客戶端所看到的一切具有一致的順序。
*(被設置 watch 的數據)The data for which the watch was set
這意味着znode節點本身具有不同的改變方式。你也可以想象 Zookeeper 維護了兩條監視鏈表:數據監視和子節點監視(data watches and child watches) getData() 和exists()設置數據監視,getChildren()設置子節點監視。或者你也可以想象 Zookeeper 設置的不同監視返回不同的數據,getData() 和 exists() 返回znode節點的相關信息,而getChildren() 返回子節點列表。因此,setData() 會觸發設置在某一節點上所設置的數據監視(假定數據設置成功),而一次成功的create() 操作則會出發當前節點上所設置的數據監視以及父節點的子節點監視。一次成功的 delete操作將會觸發當前節點的數據監視和子節點監視事件,同時也會觸發該節點父節點的child watch。
Zookeeper 中的監視是輕量級的,因此容易設置、維護和分發。當客戶端與 Zookeeper 服務器失去聯系時,客戶端並不會收到監視事件的通知,只有當客戶端重新連接后,若在必要的情況下,以前注冊的監視會重新被注冊並觸發,對於開發人員來說這通常是透明的。只有一種情況會導致監視事件的丟失,即:通過exists()設置了某個znode節點的監視,但是如果某個客戶端在此znode節點被創建和刪除的時間間隔內與zookeeper服務器失去了聯系,該客戶端即使稍后重新連接 zookeeper服務器后也得不到事件通知。
ZooKeeper的工作原理
在zookeeper的集群中,各個節點共有下面3種角色和4種狀態:
- 角色:leader,follower,observer
- 狀態:leading,following,observing,looking
Zookeeper的核心是原子廣播,這個機制保證了各個Server之間的同步。實現這個機制的協議叫做Zab協議(ZooKeeper Atomic Broadcast protocol)。Zab協議有兩種模式,它們分別是恢復模式(Recovery選主)和廣播模式(Broadcast同步)。當服務啟動或者在領導者崩潰后,Zab就進入了恢復模式,當領導者被選舉出來,且大多數Server完成了和leader的狀態同步以后,恢復模式就結束了。狀態同步保證了leader和Server具有相同的系統狀態。
為了保證事務的順序一致性,zookeeper采用了遞增的事務id號(zxid)來標識事務。所有的提議(proposal)都在被提出的時候加上了zxid。實現中zxid是一個64位的數字,它高32位是epoch用來標識leader關系是否改變,每次一個leader被選出來,它都會有一個新的epoch,標識當前屬於那個leader的統治時期。低32位用於遞增計數。
每個Server在工作過程中有4種狀態:
LOOKING:當前Server不知道leader是誰,正在搜尋。
LEADING:當前Server即為選舉出來的leader。
FOLLOWING:leader已經選舉出來,當前Server與之同步。
OBSERVING:observer的行為在大多數情況下與follower完全一致,但是他們不參加選舉和投票,而僅僅接受(observing)選舉和投票的結果。
Leader Election
當leader崩潰或者leader失去大多數的follower,這時候zk進入恢復模式,恢復模式需要重新選舉出一個新的leader,讓所有的Server都恢復到一個正確的狀態。Zk的選舉算法有兩種:一種是基於basic paxos實現的,另外一種是基於fast paxos算法實現的。系統默認的選舉算法為fast paxos。先介紹basic paxos流程:
1.選舉線程由當前Server發起選舉的線程擔任,其主要功能是對投票結果進行統計,並選出推薦的Server;
2.選舉線程首先向所有Server發起一次詢問(包括自己);
3.選舉線程收到回復后,驗證是否是自己發起的詢問(驗證zxid是否一致),然后獲取對方的id(myid),並存儲到當前詢問對象列表中,最后獲取對方提議的leader相關信息(id,zxid),並將這些信息存儲到當次選舉的投票記錄表中;
4.收到所有Server回復以后,就計算出zxid最大的那個Server,並將這個Server相關信息設置成下一次要投票的Server;
5.線程將當前zxid最大的Server設置為當前Server要推薦的Leader,如果此時獲勝的Server獲得n/2 + 1的Server票數,設置當前推薦的leader為獲勝的Server,將根據獲勝的Server相關信息設置自己的狀態,否則,繼續這個過程,直到leader被選舉出來。
通過流程分析我們可以得出:要使Leader獲得多數Server的支持,則Server總數必須是奇數2n+1,且存活的Server的數目不得少於n+1.
每個Server啟動后都會重復以上流程。在恢復模式下,如果是剛從崩潰狀態恢復的或者剛啟動的server還會從磁盤快照中恢復數據和會話信息,zk會記錄事務日志並定期進行快照,方便在恢復時進行狀態恢復。
fast paxos流程是在選舉過程中,某Server首先向所有Server提議自己要成為leader,當其它Server收到提議以后,解決epoch和zxid的沖突,並接受對方的提議,然后向對方發送接受提議完成的消息,重復這個流程,最后一定能選舉出Leader。
Leader工作流程
Leader主要有三個功能:
1.恢復數據;
2.維持與Learner的心跳,接收Learner請求並判斷Learner的請求消息類型;
3.Learner的消息類型主要有PING消息、REQUEST消息、ACK消息、REVALIDATE消息,根據不同的消息類型,進行不同的處理。
PING消息是指Learner的心跳信息;REQUEST消息是Follower發送的提議信息,包括寫請求及同步請求;
ACK消息是Follower的對提議的回復,超過半數的Follower通過,則commit該提議;REVALIDATE消息是用來延長SESSION有效時間。
Follower工作流程
Follower主要有四個功能:
1. 向Leader發送請求(PING消息、REQUEST消息、ACK消息、REVALIDATE消息);
2.接收Leader消息並進行處理;
3.接收Client的請求,如果為寫請求,發送給Leader進行投票;
4.返回Client結果。
Follower的消息循環處理如下幾種來自Leader的消息:
1.PING消息:心跳消息
2.PROPOSAL消息:Leader發起的提案,要求Follower投票
3.COMMIT消息:服務器端最新一次提案的信息
4.UPTODATE消息:表明同步完成
5.REVALIDATE消息:根據Leader的REVALIDATE結果,關閉待revalidate的session還是允許其接受消息
6.SYNC消息:返回SYNC結果到客戶端,這個消息最初由客戶端發起,用來強制得到最新的更新。
Zab: Broadcasting State Updates(事務)
Zookeeper Server接收到一次request,如果是follower,會轉發給leader,Leader執行請求並通過Transaction的形式廣播這次執行。
Zookeeper集群如何決定一個Transaction是否被commit執行?
通過“兩段提交協議”(a two-phase commit):
- Leader給所有的follower發送一個PROPOSAL消息。
- 一個follower接收到這次PROPOSAL消息,寫到磁盤,發送給leader一個ACK消息,告知已經收到。
- 當Leader收到法定人數(quorum)的follower的ACK時候,發送commit消息執行。
Zab協議保證:
1) 如果leader以T1和T2的順序廣播,那么所有的Server必須先執行T1,再執行T2。
2) 如果任意一個Server以T1、T2的順序commit執行,其他所有的Server也必須以T1、T2的順序執行。
“兩段提交協議”最大的問題是如果Leader發送了PROPOSAL消息后crash或暫時失去連接,會導致整個集群處在一種不確定的狀態(follower不知道該放棄這次提交還是執行提交)。Zookeeper這時會選出新的leader,請求處理也會移到新的leader上,不同的leader由不同的epoch標識。
切換Leader時,需要解決下面兩個問題:
Never forget delivered messages
Leader在commit投遞到任何一台follower之前crash,只有它自己commit了。新Leader必須保證這個事務也必須commit。
Let go of messages that are skipped
Leader產生某個proposal,但是在crash之前,沒有follower看到這個proposal。該server恢復時,必須丟棄這個proposal。
Zookeeper會盡量保證不會同時有2個活動的Leader,因為2個不同的Leader會導致集群處在一種不一致的狀態,所以Zab協議同時保證:
1) 在新的leader廣播Transaction之前,先前Leader commit的Transaction都會先執行。
2) 在任意時刻,都不會有2個Server同時有法定人數(quorum)的支持者。
這里的quorum是一半以上的Server數目,確切的說是有投票權力的Server(不包括Observer)。
Client API的使用
ZooKeeper Client Library提供了豐富直觀的API供用戶程序使用,下面是一些常用的API:
- create(path, data, flags): 創建一個ZNode, path是其路徑,data是要存儲在該ZNode上的數據,flags常用的有: PERSISTEN, PERSISTENT_SEQUENTAIL, EPHEMERAL, EPHEMERAL_SEQUENTAIL
- delete(path, version): 刪除一個ZNode,可以通過version刪除指定的版本, 如果version是-1的話,表示刪除所有的版本
- exists(path, watch): 判斷指定ZNode是否存在,並設置是否Watch這個ZNode。這里如果要設置Watcher的話,Watcher是在創建ZooKeeper實例時指定的,如果要設置特定的Watcher的話,可以調用另一個重載版本的exists(path, watcher)。以下幾個帶watch參數的API也都類似
- getData(path, watch): 讀取指定ZNode上的數據,並設置是否watch這個ZNode
- setData(path, watch): 更新指定ZNode的數據,並設置是否Watch這個ZNode
- getChildren(path, watch): 獲取指定ZNode的所有子ZNode的名字,並設置是否Watch這個ZNode
- sync(path): 把所有在sync之前的更新操作都進行同步,達到每個請求都在半數以上的ZooKeeper Server上生效。path參數目前沒有用
- setAcl(path, acl): 設置指定ZNode的Acl信息
- getAcl(path): 獲取指定ZNode的Acl信息
ZooKeeper典型的應用場景
名字服務(NameService)
分布式應用中,通常需要一套完備的命令機制,既能產生唯一的標識,又方便人識別和記憶。 我們知道,每個ZNode都可以由其路徑唯一標識,路徑本身也比較簡潔直觀,另外ZNode上還可以存儲少量數據,這些都是實現統一的NameService的基礎。
下面以在HDFS中實現NameService為例,來說明實現NameService的基本布驟:
- 目標:通過簡單的名字來訪問指定的HDFS機群
- 定義命名規則:這里要做到簡潔易記憶。下面是一種可選的方案: [serviceScheme://][zkCluster]-[clusterName],比如hdfs://lgprc-example/表示基於lgprc ZooKeeper集群的用來做example的HDFS集群
- 配置DNS映射: 將zkCluster的標識lgprc通過DNS解析到對應的ZooKeeper集群的地址
- 創建ZNode: 在對應的ZooKeeper上創建/NameService/hdfs/lgprc-example結點,將HDFS的配置文件存儲於該結點下
- 用戶程序要訪問hdfs://lgprc-example/的HDFS集群,首先通過DNS找到lgprc的ZooKeeper機群的地址,然后在ZooKeeper的/NameService/hdfs/lgprc-example結點中讀取到HDFS的配置,進而根據得到的配置,得到HDFS的實際訪問入口
命名服務也是分布式系統中比較常見的一類場景。在分布式系統中,通過使用命名服務,客戶端應用能夠根據指定名字來獲取資源或服務的地址,提供者等信息。被命名的實體通常可以是集群中的機器,提供的服務地址,遠程對象等等——這些我們都可以統稱他們為名字(Name)。其中較為常見的就是一些分布式服務框架中的服務地址列表。通過調用ZK提供的創建節點的API,能夠很容易創建一個全局唯一的path,這個path就可以作為一個名稱。
阿里巴巴集團開源的分布式服務框架Dubbo中使用ZooKeeper來作為其命名服務,維護全局的服務地址列表。在Dubbo實現中:
服務提供者在啟動的時候,向ZK上的指定節點/dubbo/${serviceName}/providers目錄下寫入自己的URL地址,這個操作就完成了服務的發布。
服務消費者啟動的時候,訂閱/dubbo/${serviceName}/providers目錄下的提供者URL地址, 並向/dubbo/${serviceName} /consumers目錄下寫入自己的URL地址。
注意,所有向ZK上注冊的地址都是臨時節點,這樣就能夠保證服務提供者和消費者能夠自動感應資源的變化。
另外,Dubbo還有針對服務粒度的監控,方法是訂閱/dubbo/${serviceName}目錄下所有提供者和消費者的信息。
配置管理(Configuration Management)
在分布式系統中,常會遇到這樣的場景: 某個Job的很多個實例在運行,它們在運行時大多數配置項是相同的,如果想要統一改某個配置,一個個實例去改,是比較低效,也是比較容易出錯的方式。通過ZooKeeper可以很好的解決這樣的問題,下面的基本的步驟:
- 將公共的配置內容放到ZooKeeper中某個ZNode上,比如/service/common-conf
- 所有的實例在啟動時都會傳入ZooKeeper集群的入口地址,並且在運行過程中Watch /service/common-conf這個ZNode
- 如果集群管理員修改了common-conf,所有的實例都會被通知到,根據收到的通知更新自己的配置,並繼續Watch /service/common-conf
發布與訂閱模型,即所謂的配置中心,顧名思義就是發布者將數據發布到ZK節點上,供訂閱者動態獲取數據,實現配置信息的集中式管理和動態更新。例如全局的配置信息,服務式服務框架的服務地址列表等就非常適合使用。
- 應用中用到的一些配置信息放到ZK上進行集中管理。這類場景通常是這樣:應用在啟動的時候會主動來獲取一次配置,同時,在節點上注冊一個Watcher,這樣一來,以后每次配置有更新的時候,都會實時通知到訂閱的客戶端,從來達到獲取最新配置信息的目的。
- 分布式搜索服務中,索引的元信息和服務器集群機器的節點狀態存放在ZK的一些指定節點,供各個客戶端訂閱使用。
- 分布式日志收集系統。這個系統的核心工作是收集分布在不同機器的日志。收集器通常是按照應用來分配收集任務單元,因此需要在ZK上創建一個以應用名作為path的節點P,並將這個應用的所有機器ip,以子節點的形式注冊到節點P上,這樣一來就能夠實現機器變動的時候,能夠實時通知到收集器調整任務分配。
- 系統中有些信息需要動態獲取,並且還會存在人工手動去修改這個信息的發問。通常是暴露出接口,例如JMX接口,來獲取一些運行時的信息。引入ZK之后,就不用自己實現一套方案了,只要將這些信息存放到指定的ZK節點上即可。
注意:在上面提到的應用場景中,有個默認前提是:數據量很小,但是數據更新可能會比較快的場景。
負載均衡
這里說的負載均衡是指軟負載均衡。在分布式環境中,為了保證高可用性,通常同一個應用或同一個服務的提供方都會部署多份,達到對等服務。而消費者就須要在這些對等的服務器中選擇一個來執行相關的業務邏輯,其中比較典型的是消息中間件中的生產者,消費者負載均衡。
消息中間件中發布者和訂閱者的負載均衡,linkedin開源的KafkaMQ和阿里開源的metaq都是通過zookeeper來做到生產者、消費者的負載均衡。
這里以metaq為例如講下:
生產者負載均衡:
metaq發送消息的時候,生產者在發送消息的時候必須選擇一台broker上的一個分區來發送消息,因此metaq在運行過程中,會把所有broker和對應的分區信息全部注冊到ZK指定節點上,默認的策略是一個依次輪詢的過程,生產者在通過ZK獲取分區列表之后,會按照brokerId和partition的順序排列組織成一個有序的分區列表,發送的時候按照從頭到尾循環往復的方式選擇一個分區來發送消息。
消費者負載均衡:
在消費過程中,一個消費者會消費一個或多個分區中的消息,但是一個分區只會由一個消費者來消費。MetaQ的消費策略是:
- 每個分區針對同一個group只掛載一個消費者。
- 如果同一個group的消費者數目大於分區數目,則多出來的消費者將不參與消費。
- 如果同一個group的消費者數目小於分區數目,則有部分消費者需要額外承擔消費任務。
在某個消費者故障或者重啟等情況下,其他消費者會感知到這一變化(通過 zookeeper watch消費者列表),然后重新進行負載均衡,保證所有的分區都有消費者進行消費。
組員管理(Group Membership)
在典型的Master-Slave結構的分布式系統中,Master需要作為“總管”來管理所有的Slave, 當有Slave加入,或者有Slave宕機,Master都需要感知到這個事情,然后作出對應的調整,以便不影響整個集群對外提供服務。以HBase為例,HMaster管理了所有的RegionServer,當有新的RegionServer加入的時候,HMaster需要分配一些Region到該RegionServer上去,讓其提供服務;當有RegionServer宕機時,HMaster需要將該RegionServer之前服務的Region都重新分配到當前正在提供服務的其它RegionServer上,以便不影響客戶端的正常訪問。下面是這種場景下使用ZooKeeper的基本步驟:
- Master在ZooKeeper上創建/service/slaves結點,並設置對該結點的Watcher
- 每個Slave在啟動成功后,創建唯一標識自己的臨時性(Ephemeral)結點/service/slaves/${slave_id},並將自己地址(ip/port)等相關信息寫入該結點
- Master收到有新子結點加入的通知后,做相應的處理
- 如果有Slave宕機,由於它所對應的結點是臨時性結點,在它的Session超時后,ZooKeeper會自動刪除該結點
- Master收到有子結點消失的通知,做相應的處理
分布式通知/協調
ZooKeeper中特有watcher注冊與異步通知機制,能夠很好的實現分布式環境下不同系統之間的通知與協調,實現對數據變更的實時處理。使用方法通常是不同系統都對ZK上同一個znode進行注冊,監聽znode的變化(包括znode本身內容及子節點的),其中一個系統update了znode,那么另一個系統能夠收到通知,並作出相應處理
- 另一種心跳檢測機制:檢測系統和被檢測系統之間並不直接關聯起來,而是通過zk上某個節點關聯,大大減少系統耦合。
- 另一種系統調度模式:某系統有控制台和推送系統兩部分組成,控制台的職責是控制推送系統進行相應的推送工作。管理人員在控制台作的一些操作,實際上是修改了ZK上某些節點的狀態,而ZK就把這些變化通知給他們注冊Watcher的客戶端,即推送系統,於是,作出相應的推送任務。
- 另一種工作匯報模式:一些類似於任務分發系統,子任務啟動后,到zk來注冊一個臨時節點,並且定時將自己的進度進行匯報(將進度寫回這個臨時節點),這樣任務管理者就能夠實時知道任務進度。
總之,使用zookeeper來進行分布式通知和協調能夠大大降低系統之間的耦合
zk鎖相關
簡單互斥鎖(Simple Lock)
我們知識,在傳統的應用程序中,線程、進程的同步,都可以通過操作系統提供的機制來完成。但是在分布式系統中,多個進程之間的同步,操作系統層面就無能為力了。這時候就需要像ZooKeeper這樣的分布式的協調(Coordination)服務來協助完成同步,下面是用ZooKeeper實現簡單的互斥鎖的步驟,這個可以和線程間同步的mutex做類比來理解:
- 多個進程嘗試去在指定的目錄下去創建一個臨時性(Ephemeral)結點 /locks/my_lock
- ZooKeeper能保證,只會有一個進程成功創建該結點,創建結點成功的進程就是搶到鎖的進程,假設該進程為A
- 其它進程都對/locks/my_lock進行Watch
- 當A進程不再需要鎖,可以顯式刪除/locks/my_lock釋放鎖;或者是A進程宕機后Session超時,ZooKeeper系統自動刪除/locks/my_lock結點釋放鎖。此時,其它進程就會收到ZooKeeper的通知,並嘗試去創建/locks/my_lock搶鎖,如此循環反復
互斥鎖(Simple Lock without Herd Effect)
上一節的例子中有一個問題,每次搶鎖都會有大量的進程去競爭,會造成羊群效應(Herd Effect),為了解決這個問題,我們可以通過下面的步驟來改進上述過程:
- 每個進程都在ZooKeeper上創建一個臨時的順序結點(Ephemeral Sequential) /locks/lock_${seq}
- ${seq}最小的為當前的持鎖者(${seq}是ZooKeeper生成的Sequenctial Number)
- 其它進程都對只watch比它次小的進程對應的結點,比如2 watch 1, 3 watch 2, 以此類推
- 當前持鎖者釋放鎖后,比它次大的進程就會收到ZooKeeper的通知,它成為新的持鎖者,如此循環反復
這里需要補充一點,通常在分布式系統中用ZooKeeper來做Leader Election(選主)就是通過上面的機制來實現的,這里的持鎖者就是當前的“主”。
讀寫鎖(Read/Write Lock)
我們知道,讀寫鎖跟互斥鎖相比不同的地方是,它分成了讀和寫兩種模式,多個讀可以並發執行,但寫和讀、寫都互斥,不能同時執行行。利用ZooKeeper,在上面的基礎上,稍做修改也可以實現傳統的讀寫鎖的語義,下面是基本的步驟:
- 每個進程都在ZooKeeper上創建一個臨時的順序結點(Ephemeral Sequential) /locks/lock_${seq}
- ${seq}最小的一個或多個結點為當前的持鎖者,多個是因為多個讀可以並發
- 需要寫鎖的進程,Watch比它次小的進程對應的結點
- 需要讀鎖的進程,Watch比它小的最后一個寫進程對應的結點
- 當前結點釋放鎖后,所有Watch該結點的進程都會被通知到,他們成為新的持鎖者,如此循環反復
屏障相關
屏障(Barrier)
在分布式系統中,屏障是這樣一種語義: 客戶端需要等待多個進程完成各自的任務,然后才能繼續往前進行下一步。下用是用ZooKeeper來實現屏障的基本步驟:
- Client在ZooKeeper上創建屏障結點/barrier/my_barrier,並啟動執行各個任務的進程
- Client通過exist()來Watch /barrier/my_barrier結點
- 每個任務進程在完成任務后,去檢查是否達到指定的條件,如果沒達到就啥也不做,如果達到了就把/barrier/my_barrier結點刪除
- Client收到/barrier/my_barrier被刪除的通知,屏障消失,繼續下一步任務
雙屏障(Double Barrier)
雙屏障是這樣一種語義: 它可以用來同步一個任務的開始和結束,當有足夠多的進程進入屏障后,才開始執行任務;當所有的進程都執行完各自的任務后,屏障才撤銷。下面是用ZooKeeper來實現雙屏障的基本步驟:
- Client Watch /barrier/ready結點, 通過判斷該結點是否存在來決定是否啟動任務
- 每個任務進程進入屏障時創建一個臨時結點/barrier/process/${process_id},然后檢查進入屏障的結點數是否達到指定的值,如果達到了指定的值,就創建一個/barrier/ready結點,否則繼續等待
- Client收到/barrier/ready創建的通知,就啟動任務執行過程
- Client Watch /barrier/process,如果其沒有子結點,就可以認為任務執行結束,可以離開屏障
- 每個任務進程執行任務結束后,都需要刪除自己對應的結點/barrier/process/${process_id}
-
進入屏障:
-
離開屏障:
分布式隊列(屏障的具體場景)
隊列方面,簡單地講有兩種,一種是常規的先進先出隊列,另一種是要等到隊列成員聚齊之后的才統一按序執行。
對於第一種先進先出隊列,和分布式鎖服務中的控制時序場景基本原理一致。
第二種隊列其實是在FIFO隊列的基礎上作了一個增強。通常可以在 /queue 這個znode下預先建立一個/queue/num 節點,並且賦值為n(或者直接給/queue賦值n),表示隊列大小,之后每次有隊列成員加入后,就判斷下是否已經到達隊列大小,決定是否可以開始執行了。這種用法的典型場景是,分布式環境中,一個大任務Task A,需要在很多子任務完成(或條件就緒)情況下才能進行。這個時候,凡是其中一個子任務完成(就緒),那么就去 /taskList 下建立自己的臨時時序節點(CreateMode.EPHEMERAL_SEQUENTIAL),當 /taskList 發現自己下面的子節點滿足指定個數,就可以進行下一步按序進行處理了。
