簡介
ZooKeeper是一個開源的**分布式協調服務**,由雅虎創建,是Google Chubby的開源實現。分布式應用程序可以基於ZooKeeper實現諸如**數據發布/訂閱、負載均衡、命名服務、分布式協調/通知、集群管理、Master選舉、分布式鎖和分布式隊列**等功能。
基本概念
本節將介紹ZooKeeper的幾個核心概念。這些概念貫穿於之后對ZooKeeper更深入的講解,因此有必要預先了解這些概念。
集群角色
在ZooKeeper中,有三種角色:
- Leader
- Follower
- Observer
一個ZooKeeper集群同一時刻只會有一個Leader,其他都是Follower或Observer。
ZooKeeper配置很簡單,每個節點的配置文件(zoo.cfg)都是一樣的,只有myid文件不一樣。myid的值必須是zoo.cfg中server.{數值}的{數值}部分。
zoo.cfg文件內容示例:
maxClientCnxns=0
# The number of milliseconds of each tick tickTime=2000 # The number of ticks that the initial # synchronization phase can take initLimit=10 # The number of ticks that can pass between # sending a request and getting an acknowledgement syncLimit=5 # the directory where the snapshot is stored. dataDir=/var/lib/zookeeper/data # the port at which the clients will connect clientPort=2181 # the directory where the transaction logs are stored. dataLogDir=/var/lib/zookeeper/logs server.1=192.168.20.101:2888:3888 server.2=192.168.20.102:2888:3888 server.3=192.168.20.103:2888:3888 server.4=192.168.20.104:2888:3888 server.5=192.168.20.105:2888:3888 minSessionTimeout=4000 maxSessionTimeout=100000
在裝有ZooKeeper的機器的終端執行 zookeeper-server status
可以看當前節點的ZooKeeper是什么角色(Leader or Follower)。
[root@node-20-103 ~]# zookeeper-server status JMX enabled by default Using config: /etc/zookeeper/conf/zoo.cfg Mode: follower
[root@node-20-104 ~]# zookeeper-server status JMX enabled by default Using config: /etc/zookeeper/conf/zoo.cfg Mode: leader
如上,node-20-104是Leader,node-20-103是follower。
ZooKeeper默認只有Leader和Follower兩種角色,沒有Observer角色。
為了使用Observer模式,在任何想變成Observer的節點的配置文件中加入:peerType=observer
並在所有server的配置文件中,配置成observer模式的server的那行配置追加:observer,例如:server.1:localhost:2888:3888:observer
ZooKeeper集群的所有機器通過一個**Leader選舉過程**來選定一台被稱為**『Leader』**的機器,**Leader服務器**為客戶端提供**讀**和**寫**服務。
Follower和Observer都**能**提供**讀**服務,**不能**提供**寫**服務。兩者唯一的區別在於,**Observer**機器**不參與Leader選舉**過程,也**不參與寫操作**的『過半寫成功』策略,因此Observer可以在**不影響寫性能**的情況下**提升**集群的**讀性能**。
會話(Session)
Session是指**客戶端會話**,在講解客戶端會話之前,我們先來了解下**客戶端連接**。在ZooKeeper中,一個客戶端連接是指客戶端和ZooKeeper服務器之間的**TCP長連接**。ZooKeeper對外的服務端口默認是**2181**,客戶端啟動時,首先會與服務器建立一個TCP連接,從第一次連接建立開始,客戶端會話的生命周期也開始了,通過這個連接,客戶端能夠通過**心跳檢測**和服務器保持有效的會話,也能夠向ZooKeeper服務器**發送請求**並**接受響應**,同時還能通過該連接接收來自服務器的**Watch事件通知**。Session的**SessionTimeout**值用來設置一個客戶端會話的**超時時間**。當由於服務器壓力太大、網絡故障或是客戶端主動斷開連接等各種原因導致客戶端連接斷開時,只要在SessionTimeout規定的時間內能夠**重新連接上**集群中**任意一台**服務器,那么之前創建的會話**仍然有效**。
數據節點(ZNode)
在談到分布式的時候,**一般**『節點』指的是組成集群的每一台**機器**。而ZooKeeper中的數據節點是指**數據模型**中的**數據單元**,稱為**ZNode**。ZooKeeper將所有數據存儲在**內存中**,數據模型是一棵**樹(ZNode Tree)**,由斜杠(/)進行分割的路徑,就是一個ZNode,如/hbase/master,其中hbase和master**都是**ZNode。每個ZNode上都會保存**自己的數據內容**,同時會保存一系列**屬性信息**。
注:
這里的ZNode可以理解成**既是**Unix里的**文件**,**又是**Unix里的**目錄**。因為每個ZNode不僅本身可以**寫數據**(相當於Unix里的文件),還可以有**下一級文件或目錄**(相當於Unix里的目錄)。
在ZooKeeper中,ZNode可以分為**持久節點**和**臨時節點**兩類。
持久節點
所謂持久節點是指一旦這個ZNode被創建了,除非主動進行ZNode的移除操作,否則這個ZNode將一直保存在ZooKeeper上。
臨時節點
臨時節點的生命周期跟客戶端會話綁定,一旦客戶端會話失效,那么這個客戶端創建的所有臨時節點都會被移除。
另外,ZooKeeper還允許用戶為每個節點添加一個特殊的屬性:SEQUENTIAL。一旦節點被標記上這個屬性,那么在這個節點被創建的時候,ZooKeeper就會自動在其節點后面追加上一個整型數字,這個整型數字是一個由父節點維護的自增數字。
版本
ZooKeeper的每個ZNode上都會存儲數據,對應於每個ZNode,ZooKeeper都會為其維護一個叫作Stat的數據結構,Stat中記錄了這個ZNode的三個數據版本,分別是version(當前ZNode的版本)、cversion(當前ZNode子節點的版本)和aversion(當前ZNode的ACL版本)。
狀態信息
每個ZNode除了存儲數據內容之外,還存儲了ZNode本身的一些狀態信息。用 get 命令可以同時獲得某個ZNode的內容和狀態信息。如下:
[zk: localhost:2181(CONNECTED) 23] get /yarn-leader-election/appcluster-yarn/ActiveBreadCrumb appcluster-yarnrm1 cZxid = 0x1b00133dc0 //Created ZXID,表示該ZNode被創建時的事務ID ctime = Tue Jan 03 15:44:42 CST 2017 //Created Time,表示該ZNode被創建的時間 mZxid = 0x1d00000063 //Modified ZXID,表示該ZNode最后一次被更新時的事務ID mtime = Fri Jan 06 08:44:25 CST 2017 //Modified Time,表示該節點最后一次被更新的時間 pZxid = 0x1b00133dc0 //表示該節點的子節點列表最后一次被修改時的事務ID。注意,只有子節點列表變更了才會變更pZxid,子節點內容變更不會影響pZxid。 cversion = 0 //子節點的版本號 dataVersion = 11 //數據節點的版本號 aclVersion = 0 //ACL版本號 ephemeralOwner = 0x0 //創建該節點的會話的seddionID。如果該節點是持久節點,那么這個屬性值為0。 dataLength = 22 //數據內容的長度 numChildren = 0 //子節點的個數
在ZooKeeper中,version屬性是用來實現樂觀鎖機制中的『寫入校驗』的(保證分布式數據原子性操作)。
事務操作
在ZooKeeper中,能改變ZooKeeper服務器狀態的操作稱為事務操作。一般包括數據節點創建與刪除、數據內容更新和客戶端會話創建與失效等操作。對應每一個事務請求,ZooKeeper都會為其分配一個全局唯一的事務ID,用ZXID表示,通常是一個64位的數字。每一個ZXID對應一次更新操作,從這些ZXID中可以間接地識別出ZooKeeper處理這些事務操作請求的全局順序。
Watcher
Watcher(事件監聽器),是ZooKeeper中一個很重要的特性。ZooKeeper允許用戶在指定節點上注冊一些Watcher,並且在一些特定事件觸發的時候,ZooKeeper服務端會將事件通知到感興趣的客戶端上去。該機制是ZooKeeper實現分布式協調服務的重要特性。
ACL
ZooKeeper采用ACL(Access Control Lists)策略來進行權限控制。ZooKeeper定義了如下5種權限。
- CREATE: 創建子節點的權限。
- READ: 獲取節點數據和子節點列表的權限。
- WRITE:更新節點數據的權限。
- DELETE: 刪除子節點的權限。
- ADMIN: 設置節點ACL的權限。
注意:CREATE 和 DELETE 都是針對子節點的權限控制。
ZooKeeper典型應用場景
ZooKeeper是一個**高可用**的分布式**數據管理與協調框架**。基於對ZAB算法的實現,該框架能夠很好地保證分布式環境中數據的**一致性**。也是基於這樣的特性,使得ZooKeeper成為了解決分布式一致性問題的利器。
數據發布與訂閱(配置中心)
數據發布與訂閱,即所謂的**配置中心**,顧名思義就是發布者將數據發布到ZooKeeper節點上,供訂閱者進行數據訂閱,進而達到**動態獲取數據**的目的,實現配置信息的**集中式管理**和**動態更新**。
在我們平常的應用系統開發中,經常會碰到這樣的需求:系統中需要使用一些通用的配置信息,例如**機器列表信息**、**數據庫配置信息**等。這些全局配置信息通常具備以下3個特性。
- 數據量通常比較**小。**
- 數據內容在運行時**動態變化**。
- 集群中各機器共享,**配置一致**。
對於這樣的全局配置信息就可以發布到ZooKeeper上,讓客戶端(集群的機器)去訂閱該消息。
發布/訂閱系統一般有兩種設計模式,分別是**推(Push)**和**拉(Pull)**模式。
- 推:**服務端主動**將數據更新發送給所有訂閱的客戶端。
- 拉:**客戶端主動**發起請求來獲取最新數據,通常客戶端都采用**定時輪詢**拉取的方式。
ZooKeeper采用的是**推拉相結合**的方式。如下:
客戶端想服務端**注冊**自己需要關注的節點,一旦該節點的數據發生**變更**,那么服務端就會向相應的客戶端發送Watcher事件**通知**,客戶端接收到這個消息通知后,需要**主動**到服務端**獲取**最新的數據(**推拉結合**)。
命名服務(Naming Service)
命名服務也是分布式系統中比較常見的一類場景。在分布式系統中,通過使用命名服務,客戶端應用能夠根據指定**名字**來獲取**資源或服務的地址,提供者等信息**。被命名的實體通常可以是**集群中的機器,提供的服務,遠程對象等等**——這些我們都可以統稱他們為**名字(Name)**。其中較為常見的就是一些分布式服務框架(如RPC、RMI)中的服務地址列表。通過在ZooKeepr里創建順序節點,能夠很容易創建一個**全局唯一的路徑**,這個路徑就可以作為一個**名字**。
ZooKeeper的命名服務即生成**全局唯一的ID**。
分布式協調/通知
ZooKeeper中特有**Watcher注冊**與**異步通知機制**,能夠很好的實現分布式環境下不同機器,甚至不同系統之間的**通知與協調**,從而實現**對數據變更的實時處理**。使用方法通常是不同的客戶端都對ZK上同一個ZNode進行注冊,監聽ZNode的變化(包括ZNode本身內容及子節點的),如果ZNode發生了變化,那么所有訂閱的客戶端都能夠接收到相應的Watcher通知,並做出相應的處理。
ZK的分布式協調/通知,是一種通用的分布式系統機器間的通信方式。
心跳檢測
機器間的心跳檢測機制是指在分布式環境中,不同機器(或進程)之間需要檢測到彼此是否在正常運行,例如A機器需要知道B機器是否正常運行。在傳統的開發中,我們通常是通過主機直接是否可以**相互PING通**來判斷,更復雜一點的話,則會通過在機器之間建立長連接,通過**TCP連接**固有的心跳檢測機制來實現上層機器的心跳檢測,這些都是非常常見的心跳檢測方法。
下面來看看如何使用ZK來實現分布式機器(進程)間的心跳檢測。
基於ZK的**臨時節點**的特性,可以讓不同的進程都在ZK的一個**指定節點**下創建**臨時子節點**,不同的進程直接可以根據這個臨時子節點來判斷對應的進程**是否存活**。通過這種方式,檢測和被檢測系統直接並不需要直接相關聯,而是通過ZK上的某個節點進行關聯,大大**減少了系統耦合**。
工作進度匯報
在一個常見的**任務分發系統**中,通常任務被分發到不同的機器上執行后,需要實時地將自己的任務執行進度**匯報**給分發系統。這個時候就可以通過ZK來實現。在ZK上選擇一個節點,每個任務客戶端都在這個節點下面創建**臨時子節點**,這樣便可以實現兩個功能:
- 通過判斷臨時節點是否存在來確定任務機器**是否存活**。
- 各個任務機器會實時地將自己的**任務執行進度寫到這個臨時節點上去**,以便中心系統能夠實時地獲取到任務的**執行進度**。
Master選舉
Master選舉可以說是ZooKeeper**最典型的應用場景**了。比如HDFS中Active NameNode的選舉、YARN中Active ResourceManager的選舉和HBase中Active HMaster的選舉等。
針對Master選舉的需求,通常情況下,我們可以選擇常見的**關系型數據庫**中的**主鍵特性**來實現:希望成為Master的機器都向數據庫中插入一條**相同主鍵ID**的記錄,數據庫會幫我們進行**主鍵沖突檢查**,也就是說,**只有一台**機器能插入成功——那么,我們就認為向數據庫中**成功插入**數據的客戶端機器**成為Master**。
依靠關系型數據庫的主鍵特性確實能夠很好地保證在集群中選舉出唯一的一個Master。但是,如果當前選舉出的Master掛了,那么該如何處理?誰來告訴我Master掛了呢?顯然,關系型數據庫無法通知我們這個事件。但是,ZooKeeper可以做到!
利用ZooKeepr的強一致性,能夠很好地保證在分布式高並發情況下節點的創建一定能夠保證全局唯一性,即ZooKeeper將會保證客戶端**無法創建一個已經存在的ZNode**。也就是說,如果同時有多個客戶端請求創建**同一個**臨時節點,那么最終一定**只有一個**客戶端請求能夠創建成功。利用這個特性,就能很容易地在分布式環境中進行Master選舉了。
成功創建該節點的客戶端所在的機器就成為了Master。同時,其他沒有成功創建該節點的客戶端,都會在該節點上**注冊**一個子節點變更的**Watcher**,用於監控當前Master機器是否存活,一旦發現當前的Master掛了,那么其他客戶端將會**重新進行Master選舉**。
這樣就實現了Master的**動態選舉**。
分布式鎖
分布式鎖是控制**分布式系統**之間**同步訪問共享資源**的一種方式。
分布式鎖又分為**排他鎖**和**共享鎖**兩種。
排他鎖
排他鎖(Exclusive Locks,簡稱X鎖),又稱為**寫鎖**或**獨占鎖**。
如果事務T1對數據對象O1加上了排他鎖,那么在整個加鎖期間,只允許事務T1對O1進行**讀取和更新**操作,其他任何事務都不能在對這個數據對象進行任何類型的操作(不能再對該對象加鎖),直到T1釋放了排他鎖。
可以看出,排他鎖的核心是如何保證當前**只有一個事務獲得鎖**,並且鎖**被釋放**后,所有正在等待獲取鎖的事務都能夠**被通知到**。
如何利用ZooKeeper實現排他鎖?
定義鎖
ZooKeeper上的**一個ZNode可以表示一個鎖**。例如/exclusive_lock/lock節點就可以被定義為一個鎖。
獲得鎖
如上所說,把ZooKeeper上的一個ZNode看作是一個鎖,**獲得鎖**就通過**創建ZNode**的方式來實現。所有客戶端都去/exclusive_lock節點下創建臨時子節點/exclusive_lock/lock。ZooKeeper會保證在所有客戶端中,最終只有一個客戶端能夠創建成功,那么就可以認為該客戶端獲得了鎖。同時,所有沒有獲取到鎖的客戶端就需要到/exclusive_lock節點上注冊一個子節點變更的Watcher監聽,以便實時監聽到lock節點的變更情況。
釋放鎖
因為/exclusive_lock/lock是一個**臨時節點**,因此在以下兩種情況下,都有可能釋放鎖。
- 當前獲得鎖的客戶端機器發生**宕機**或**重啟**,那么該臨時節點就會**被刪除,釋放鎖**。
- 正常執行完業務邏輯后,客戶端就會**主動**將自己創建的臨時節點**刪除,釋放鎖**。
無論在什么情況下移除了lock節點,ZooKeeper都會**通知**所有在/exclusive_lock節點上注冊了節點變更Watcher監聽的客戶端。這些客戶端在接收到通知后,再次**重新發起**分布式鎖獲取,即重復『獲取鎖』過程。
共享鎖
共享鎖(Shared Locks,簡稱S鎖),又稱為讀鎖。如果事務T1對數據對象O1加上了共享鎖,那么T1只能對O1進行**讀操作**,其他事務也能**同時對O1加共享鎖**(不能是排他鎖),直到O1上的所有共享鎖都釋放后O1才能被加排他鎖。
總結:可以**多個事務同時獲得**一個對象的**共享鎖**(同時讀),有共享鎖就不能再加排他鎖(因為排他鎖是寫鎖)