一、什么是Zookeeper
????????Zookeeper最早起源于雅虎研究院的一個研究小組。在當時,研究人員發現,在雅虎內部很多大型系統基本都需要依賴一個類似的系統來進行分布式協調,但是這些系統往往都存在分布式單點問題。所以,雅虎的開發人員就試圖開發一個通用的無單點問題的分布式協調框架,以便讓開發人員將精力集中在處理業務邏輯上。
????????關于“ZooKeeper”這個項目的名字,其實也有一段趣聞。在立項初期,考慮到之前內部很多項目都是使用動物的名字來命名的(例如著名的Pig項目),雅虎的工程師希望給這個項目也取一個動物的名字。時任研究院的首席科學家RaghuRamakrishnan開玩笑地說:“在這樣下去,我們這兒就變成動物園了!”此話一出,大家紛紛表示就叫動物園管理員吧一一一因為各個以動物命名的分布式組件放在一起,雅虎的整個分布式系統看上去就像一個大型的動物園了,而Zookeeper正好要用來進行分布式環境的協調一一于是,Zookeeper的名字也就由此誕生了。
????????ZooKeeper 是一個典型的分布式數據一致性解決方案,分布式應用程序可以基于 ZooKeeper 實現諸如數據發布/訂閱、負載均衡、命名服務、分布式協調/通知、集群管理、Master 選舉、分布式鎖和分布式隊列等功能。
二、基本概念
????????ZooKeeper 是一個開源的分布式協調服務,ZooKeeper框架最初是在“Yahoo!"上構建的,用于以簡單而穩健的方式訪問他們的應用程序。 后來,Apache ZooKeeper成為Hadoop,HBase和其他分布式框架使用的有組織服務的標準。 例如,Apache HBase使用ZooKeeper跟蹤分布式數據的狀態。ZooKeeper 的設計目標是將那些復雜且容易出錯的分布式一致性服務封裝起來,構成一個高效可靠的原語集,并以一系列簡單易用的接口提供給用戶使用。
????????Zookeeper是一個分布式協調服務;就是為用戶的分布式應用程序提供協調服務;
????????Zookeeper是為別的分布式程序服務的;
????????Zookeeper所提供的服務涵蓋:主從協調、服務器節點動態上下線、統一配置管理、分布式共享鎖、統一名稱服務……
????????ZooKeeper ?本身就是一個分布式程序(只要半數以上節點存活,ZooKeeper ?就能正常服務)。
????????為了保證高可用,最好是以集群形態來部署 ZooKeeper,這樣只要集群中大部分機器是可用的(能夠容忍一定的機器故障),那么 ZooKeeper 本身仍然是可用的。
????????ZooKeeper ?將數據保存在內存中,這也就保證了 高吞吐量和低延遲(但是內存限制了能夠存儲的容量不太大,此限制也是保持znode中存儲的數據量較小的進一步原因)。
????????ZooKeeper 是高性能的。 在“讀”多于“寫”的應用程序中尤其地高性能,因為“寫”會導致所有的服務器間同步狀態。(“讀”多于“寫”是協調服務的典型場景。)
????????ZooKeeper有臨時節點的概念。 當創建臨時節點的客戶端會話一直保持活動,瞬時節點就一直存在。而當會話終結時,瞬時節點被刪除。持久節點是指一旦這個ZNode被創建了,除非主動進行ZNode的移除操作,否則這個ZNode將一直保存在Zookeeper上。
????????ZooKeeper 底層其實只提供了兩個功能:①管理(存儲、讀取)用戶程序提交的數據;②為用戶程序提交數據節點監聽服務。
????會話(Session)
????????Session 指的是 ZooKeeper ?服務器與客戶端會話。在 ZooKeeper 中,一個客戶端連接是指客戶端和服務器之間的一個 TCP 長連接。客戶端啟動的時候,首先會與服務器建立一個 TCP 連接,從第一次連接建立開始,客戶端會話的生命周期也開始了。通過這個連接,客戶端能夠通過心跳檢測與服務器保持有效的會話,也能夠向Zookeeper服務器發送請求并接受響應,同時還能夠通過該連接接收來自服務器的Watch事件通知。?Session的sessionTimeout值用來設置一個客戶端會話的超時時間。當由于服務器壓力太大、網絡故障或是客戶端主動斷開連接等各種原因導致客戶端連接斷開時,只要在sessionTimeout規定的時間內能夠重新連接上集群中任意一臺服務器,那么之前創建的會話仍然有效。
????????在為客戶端創建會話之前,服務端首先會為每個客戶端都分配一個sessionID。由于 sessionID 是 Zookeeper 會話的一個重要標識,許多與會話相關的運行機制都是基于這個 sessionID 的,因此,無論是哪臺服務器為客戶端分配的 sessionID,都務必保證全局唯一。
Znode
????????在談到分布式的時候,我們通常說的“節點"是指組成集群的每一臺機器。然而,在Zookeeper中,“節點"分為兩類,第一類同樣是指構成集群的機器,我們稱之為機器節點;第二類則是指數據模型中的數據單元,我們稱之為數據節點一一ZNode。
Zookeeper將所有數據存儲在內存中,數據模型是一棵樹(Znode Tree),由斜杠(/)的進行分割的路徑,就是一個Znode,例如/foo/path1。每個上都會保存自己的數據內容,同時還會保存一系列屬性信息。
????????在Zookeeper中,node可以分為持久節點和臨時節點兩類。所謂持久節點是指一旦這個ZNode被創建了,除非主動進行ZNode的移除操作,否則這個ZNode將一直保存在Zookeeper上。而臨時節點就不一樣了,它的生命周期和客戶端會話綁定,一旦客戶端會話失效,那么這個客戶端創建的所有臨時節點都會被移除。另外,ZooKeeper還允許用戶為每個節點添加一個特殊的屬性:SEQUENTIAL.一旦節點被標記上這個屬性,那么在這個節點被創建的時候,Zookeeper會自動在其節點名后面追加上一個整型數字,這個整型數字是一個由父節點維護的自增數字。
Watcher
????????Watcher(事件監聽器),是Zookeeper中的一個很重要的特性。Zookeeper允許用戶在指定節點上注冊一些Watcher,并且在一些特定事件觸發的時候,ZooKeeper服務端會將事件通知到感興趣的客戶端上去,該機制是Zookeeper實現分布式協調服務的重要特性。
三、安裝部署
例如3節點服務器安裝zookeeper集群,分別執行
1)安裝JDK
2)修改環境變量
vi /etc/profile(修改文件)
????export ZOOKEEPER_HOME=/home/hadoop/zookeeper
????export PATH=$PATH:$ZOOKEEPER_HOME/bin
????source /etc/profile
3)修改配置文件
????cd zookeeper/conf
????cp zoo_sample.cfg zoo.cfg
????vi zoo.cfg
????添加內容:
????dataDir=/home/hadoop/zookeeper/data
????dataLogDir=/home/hadoop/zookeeper/log
????server.1=slave1:2888:3888 (主機名, 心跳端口、數據端口)
????server.2=slave2:2888:3888
????server.3=slave3:2888:3888
????其它配置可以按照默認值
4)創建文件夾
cd /home/hadoop/zookeeper/
mkdir -m 755 data
mkdir -m 755 log
5)myid
在data文件夾下新建myid文件,myid的文件內容為:
cd data
vi myid
添加內容:(3臺服務器不同,分別為1、2、3,與zoo.cfg形成對應)
6)啟動(每臺機器)
3臺服務器bin目錄下zkServer.sh start
jps
四、Zookeeper結構和命令
1、特性
Zookeeper:一個leader,多個follower組成的集群
1)、全局數據一致(單一系統映像):每個server保存一份相同的數據副本,client無論連接到哪個server,數據都是一致的
2)、分布式讀寫,更新請求轉發,由leader實施
3)、更新請求順序進行,來自同一個client的更新請求按其發送順序依次執行
4)、數據更新原子性,一次數據更新要么成功,要么失敗
5)、實時性,在一定時間范圍內,client能讀到最新數據
6)、可靠性:?一旦一次更改請求被應用,更改的結果就會被持久化,直到被下一次更改覆蓋
2、zookeeper數據結構
1)、層次化的目錄結構,命名符合常規文件系統規范(見下圖)
2)、每個節點在zookeeper中叫做znode,并且其有一個唯一的路徑標識
3)、節點Znode可以包含數據和子節點(但是EPHEMERAL類型的節點不能有子節點,下一頁詳細講解)
4)、客戶端應用可以在節點上設置監視器(后續詳細講解)
3、節點類型
1)、Znode有兩種類型:
? ? ? ? 短暫(ephemeral)(斷開連接自己刪除)
? ? ? 持久(persistent)(斷開連接不刪除)
2)、Znode有四種形式的目錄節點(默認是persistent )
? ? ? ? PERSISTENT
? ? ? ? ? ? PERSISTENT_SEQUENTIAL(持久序列/test0000000019 )
? ? ? ? ? ? EPHEMERAL
? ? ? ? ? ? EPHEMERAL_SEQUENTIAL
? ? ? 3)、創建znode時設置順序標識,znode名稱后會附加一個值,順序號是一個單調遞增的計數器,由父節點維護
? ? ? 4)、在分布式系統中,順序號可以被用于為所有的事件進行全局排序,這樣客戶端可以通過順序號推斷事件的順序
? 4、zookeeper命令行操作
運行?zkCli.sh?–server?<ip>進入命令行工具
1、使用?ls?命令來查看當前?ZooKeeper?中所包含的內容:
[zk:?202.115.36.251:2181(CONNECTED)?1]?ls?/
2、創建一個新的?znode?,使用?create?/zk?myData?。這個命令創建了一個新的?znode?節點“?zk?”以及與它關聯的字符串:
[zk:?202.115.36.251:2181(CONNECTED)?2]?create?/zk?"myData“
3、我們運行?get?命令來確認?znode?是否包含我們所創建的字符串:
[zk:?202.115.36.251:2181(CONNECTED)?3]?get?/zk
#監聽這個節點的變化,當另外一個客戶端改變/zk時,它會打出下面的
#WATCHER::
#WatchedEvent state:SyncConnected type:NodeDataChanged path:/zk
[zk: localhost:2181(CONNECTED) 4] get /zk watch
4、下面我們通過?set?命令來對?zk?所關聯的字符串進行設置:
[zk:?202.115.36.251:2181(CONNECTED)?4]?set?/zk?"zsl“
5、下面我們將剛才創建的?znode?刪除:
[zk:?202.115.36.251:2181(CONNECTED)?5]?delete?/zk
6、刪除節點:rmr
[zk:?202.115.36.251:2181(CONNECTED)?5]?rmr?/zk
五、 zookeeper-api應用
1、基本使用
org.apache.zookeeper.Zookeeper是客戶端入口主類,負責建立與server的會話
它提供了表 1 所示幾類主要方法? :
六、Zookeeper原理
Zookeeper雖然在配置文件中并沒有指定master和slave
但是,zookeeper工作時,是有一個節點為leader,其他則為follower
Leader是通過內部的選舉機制臨時產生的
1)選舉機制:
以一個簡單的例子來說明整個選舉的過程.
假設有五臺服務器組成的zookeeper集群,它們的id從1-5,同時它們都是最新啟動的,也就是沒有歷史數據,在存放數據量這一點上,都是一樣的.假設這些服務器依序啟動,來看看會發生什么.
1) 服務器1啟動,此時只有它一臺服務器啟動了,它發出去的報沒有任何響應,所以它的選舉狀態一直是LOOKING狀態
2) 服務器2啟動,它與最開始啟動的服務器1進行通信,互相交換自己的選舉結果,由于兩者都沒有歷史數據,所以id值較大的服務器2勝出,但是由于沒有達到超過半數以上的服務器都同意選舉它(這個例子中的半數以上是3),所以服務器1,2還是繼續保持LOOKING狀態.
3) 服務器3啟動,根據前面的理論分析,服務器3成為服務器1,2,3中的老大,而與上面不同的是,此時有三臺服務器選舉了它,所以它成為了這次選舉的leader.
4) 服務器4啟動,根據前面的分析,理論上服務器4應該是服務器1,2,3,4中最大的,但是由于前面已經有半數以上的服務器選舉了服務器3,所以它只能接收當小弟的命了.
5) 服務器5啟動,同4一樣,當小弟.
2)異常集群選舉機制
那么,初始化的時候,是按照上述的說明進行選舉的,但是當zookeeper運行了一段時間之后,有機器down掉,重新選舉時,選舉過程就相對復雜了。
需要加入數據id、leader id和邏輯時鐘。
數據id:數據新的id就大,數據每次更新都會更新id。
Leader id:就是我們配置的myid中的值,每個機器一個。
邏輯時鐘:這個值從0開始遞增,每次選舉對應一個值,也就是說: ?如果在同一次選舉中,那么這個值應該是一致的 ; ?邏輯時鐘值越大,說明這一次選舉leader的進程更新.
選舉的標準就變成:
1、邏輯時鐘小的選舉結果被忽略,重新投票
2、統一邏輯時鐘后,數據id大的勝出
3、數據id相同的情況下,leader id大的勝出
根據這個規則選出leader。
3)選舉算法:
????????Paxos 算法應該可以說是 ?ZooKeeper 的靈魂了。但是,ZooKeeper 并沒有完全采用 Paxos算法 ,而是使用 ZAB 協議作為其保證數據一致性的核心算法。另外,在ZooKeeper的官方文檔中也指出,ZAB協議并不像 Paxos 算法那樣,是一種通用的分布式一致性算法,它是一種特別為Zookeeper設計的崩潰可恢復的原子消息廣播算法。
????????Zookeeper 的核心是原子廣播,這個機制保證了各個Server之間的同步。實現這個機制的協議叫做Zab協議。Zab協議有兩種模式,它們分別是恢復模式(選主)和廣播模式(同步)。當服務啟動或者在領導者崩潰后,Zab就進入了恢復模式,當領導者被選舉出來,且大多數Server完成了和 leader的狀態同步以后,恢復模式就結束了。狀態同步保證了leader和Server具有相同的系統狀態。?
????????為了保證事務的順序一致性,zookeeper采用了遞增的事務id號(zxid)來標識事務。所有的提議(proposal)都在被提出的時候加上了zxid。實現中zxid是一個64位的數字,它高32位是epoch用來標識leader關系是否改變,每次一個leader被選出來,它都會有一個新的epoch,標識當前屬于那個leader的統治時期。低32位用于遞增計數。
七、ZooKeeper 集群角色介紹
最典型集群模式: Master/Slave 模式(主備模式)。在這種模式中,通常 Master服務器作為主服務器提供寫服務,其他的 Slave 服務器從服務器通過異步復制的方式獲取 Master 服務器最新的數據提供讀服務。
但是,在 ZooKeeper 中沒有選擇傳統的 ?Master/Slave 概念,而是引入了Leader、Follower 和 Observer 三種角色。如下圖所示
ZooKeeper 集群中的所有機器通過一個 Leader 選舉過程來選定一臺稱為 “Leader” 的機器,Leader 既可以為客戶端提供寫服務又能提供讀服務。除了 Leader 外,Follower 和 ?Observer 都只能提供讀服務。Follower 和 ?Observer 唯一的區別在于 Observer 機器不參與 Leader 的選舉過程,也不參與寫操作的“過半寫成功”策略,因此 Observer 機器可以在不影響寫性能的情況下提升集群的讀性能。
當 Leader 服務器出現網絡中斷、崩潰退出與重啟等異常情況時,ZAB 協議就會進人恢復模式并選舉產生新的Leader服務器。這個過程大致是這樣的:
1)、Leader election(選舉階段):節點在一開始都處于選舉階段,只要有一個節點得到超半數節點的票數,它就可以當選準 leader。
2)、Discovery(發現階段):在這個階段,followers 跟準 leader 進行通信,同步 followers 最近接收的事務提議。
3)、Synchronization(同步階段):同步階段主要是利用 leader 前一階段獲得的最新提議歷史,同步集群中所有的副本。同步完成之后 準 leader 才會成為真正的 leader。
4)、(廣播階段)?:到了這個階段,Zookeeper 集群才能正式對外提供事務服務,并且 leader 可以進行消息廣播。同時如果有新的節點加入,還需要對新節點進行同步。
八、Zookeeper常用應用場景
1)統一命名服務(Name Service)
分布式應用中,通常需要有一套完整的命名規則,既能夠產生唯一的名稱又便于人識別和記住,通常情況下用樹形的名稱結構是一個理想的選擇,樹形的名稱結構是一個有層次的目錄結構,既對人友好又不會重復。說到這里你可能想到了 JNDI,沒錯 Zookeeper 的 Name Service 與 JNDI 能夠完成的功能是差不多的,它們都是將有層次的目錄結構關聯到一定資源上,但是 Zookeeper 的 Name Service 更加是廣泛意義上的關聯,也許你并不需要將名稱關聯到特定資源上,你可能只需要一個不會重復名稱,就像數據庫中產生一個唯一的數字主鍵一樣。
Name Service 已經是 Zookeeper 內置的功能,你只要調用 Zookeeper 的 API 就能實現。如調用 create 接口就可以很容易創建一個目錄節點。
2)配置管理(Configuration Management)
配置的管理在分布式應用環境中很常見,例如同一個應用系統需要多臺 PC Server 運行,但是它們運行的應用系統的某些配置項是相同的,如果要修改這些相同的配置項,那么就必須同時修改每臺運行這個應用系統的 PC Server,這樣非常麻煩而且容易出錯。
像這樣的配置信息完全可以交給 Zookeeper 來管理,將配置信息保存在 Zookeeper 的某個目錄節點中,然后將所有需要修改的應用機器監控配置信息的狀態,一旦配置信息發生變化,每臺應用機器就會收到 Zookeeper 的通知,然后從 Zookeeper 獲取新的配置信息應用到系統中。
圖 1. 配置管理結構圖
3)集群管理(Group Membership)
Zookeeper 能夠很容易的實現集群管理的功能,如有多臺 Server 組成一個服務集群,那么必須要一個“總管”知道當前集群中每臺機器的服務狀態,一旦有機器不能提供服務,集群中其它集群必須知道,從而做出調整重新分配服務策略。同樣當增加集群的服務能力時,就會增加一臺或多臺 Server,同樣也必須讓“總管”知道。
Zookeeper 不僅能夠幫你維護當前的集群中機器的服務狀態,而且能夠幫你選出一個“總管”,讓這個總管來管理集群,這就是 Zookeeper 的另一個功能 Leader Election。
它們的實現方式都是在 Zookeeper 上創建一個 EPHEMERAL 類型的目錄節點,然后每個 Server 在它們創建目錄節點的父目錄節點上調用getChildren(String?path, boolean?watch) 方法并設置 watch 為 true,由于是 EPHEMERAL 目錄節點,當創建它的 Server 死去,這個目錄節點也隨之被刪除,所以 Children 將會變化,這時?getChildren上的 Watch 將會被調用,所以其它 Server 就知道已經有某臺 Server 死去了。新增 Server 也是同樣的原理。
Zookeeper 如何實現 Leader Election,也就是選出一個 Master Server。和前面的一樣每臺 Server 創建一個 EPHEMERAL 目錄節點,不同的是它還是一個 SEQUENTIAL 目錄節點,所以它是個 EPHEMERAL_SEQUENTIAL 目錄節點。之所以它是 EPHEMERAL_SEQUENTIAL 目錄節點,是因為我們可以給每臺 Server 編號,我們可以選擇當前是最小編號的 Server 為 Master,假如這個最小編號的 Server 死去,由于是 EPHEMERAL 節點,死去的 Server 對應的節點也被刪除,所以當前的節點列表中又出現一個最小編號的節點,我們就選擇這個節點為當前 Master。這樣就實現了動態選擇 Master,避免了傳統意義上單 Master 容易出現單點故障的問題。
圖 2. 集群管理結構圖
4)共享鎖(Locks)
共享鎖在同一個進程中很容易實現,但是在跨進程或者在不同 Server 之間就不好實現了。Zookeeper 卻很容易實現這個功能,實現方式也是需要獲得鎖的 Server 創建一個?EPHEMERAL_SEQUENTIAL?目錄節點,然后調用?getChildren方法獲取當前的目錄節點列表中最小的目錄節點是不是就是自己創建的目錄節點,如果正是自己創建的,那么它就獲得了這個鎖,如果不是那么它就調用?exists(String?path, boolean?watch) 方法并監控 Zookeeper 上目錄節點列表的變化,一直到自己創建的節點是列表中最小編號的目錄節點,從而獲得鎖,釋放鎖很簡單,只要刪除前面它自己所創建的目錄節點就行了。
圖 3. Zookeeper 實現 Locks 的流程圖
5)隊列管理
Zookeeper 可以處理兩種類型的隊列:
(1)當一個隊列的成員都聚齊時,這個隊列才可用,否則一直等待所有成員到達,這種是同步隊列。
(2)隊列按照 FIFO 方式進行入隊和出隊操作,例如實現生產者和消費者模型。
同步隊列用 Zookeeper 實現的實現思路如下:
創建一個父目錄 /synchronizing,每個成員都監控標志(Set Watch)位目錄 /synchronizing/start 是否存在,然后每個成員都加入這個隊列,加入隊列的方式就是創建 /synchronizing/member_i 的臨時目錄節點,然后每個成員獲取 / synchronizing 目錄的所有目錄節點,也就是 member_i。判斷 i 的值是否已經是成員的個數,如果小于成員個數等待 /synchronizing/start 的出現,如果已經相等就創建 /synchronizing/start。
用下面的流程圖更容易理解:
? ??
FIFO 隊列用 Zookeeper 實現思路如下:
實現的思路也非常簡單,就是在特定的目錄下創建 SEQUENTIAL 類型的子目錄 /queue_i,這樣就能保證所有成員加入隊列時都是有編號的,出隊列時通過 getChildren( ) 方法可以返回當前所有的隊列中的元素,然后消費其中最小的一個,這樣就能保證 FIFO。
下面是生產者和消費者這種隊列形式的示例代碼,完整的代碼請看附件:
?生產者代碼:
boolean produce(int i) throws KeeperException, InterruptedException{
? ? ? ? ByteBuffer b = ByteBuffer.allocate(4);
? ? ? ? byte[] value;
? ? ? ? b.putInt(i);
? ? ? ? value = b.array();
? ? ? ? zk.create(root + "/element", value, ZooDefs.Ids.OPEN_ACL_UNSAFE,
? ? ? ? ? ? ? ? ? ? CreateMode.PERSISTENT_SEQUENTIAL);
? ? ? ? return true;
? ? }
消費者代碼:
int consume() throws KeeperException, InterruptedException{
? ? ? ? ? int retvalue = -1;
? ? ? ? ? Stat stat = null;
? ? ? ? ? while (true) {
? ? ? ? ? ? ? synchronized (mutex) {
? ? ? ? ? ? ? ? ? List<String> list = zk.getChildren(root, true);
? ? ? ? ? ? ? ? ? if (list.size() == 0) {
? ? ? ? ? ? ? ? ? ? ? mutex.wait();
? ? ? ? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? ? ? Integer min = new Integer(list.get(0).substring(7));
? ? ? ? ? ? ? ? ? ? for(String s : list){
1
? ? ? ? ? ? ? ? ? ? ? ? Integer tempValue = new Integer(s.substring(7));
? ? ? ? ? ? ? ? ? ? ? ? if(tempValue < min) min = tempValue;
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? byte[] b = zk.getData(root + "/element" + min,false, stat);
? ? ? ? ? ? ? ? ? ? zk.delete(root + "/element" + min, 0);
? ? ? ? ? ? ? ? ? ? ByteBuffer buffer = ByteBuffer.wrap(b);
? ? ? ? ? ? ? ? ? ? retvalue = buffer.getInt();
? ? ? ? ? ? ? ? ? ? return retvalue;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? }