Zookeeper應用場景
數據發佈/訂閱(配置中心)
我們平常的開發過程中,經常會碰到這樣的需求:系統中需要一些通用的配置信息,如一些運行時的開關、前端需要展示的通知信息、數據庫配置信息等等。這些需求通常都要求具備3個特性:
- 數據量比較小
- 數據內容在運行時會變化
- 集羣中的所有機器共享,數據一致
我們假設把這些數據存儲在應用的內存中,那麼數據共享是一個問題,數據變更的通知又是一個問題。
那我們使用Zookeeper怎麼實現呢?
- 配置存儲
我們可以先把數據存儲在Zookeeper的一個節點上。如/app/database_config
2. 配置獲取
應用啓動時,首先會去前面存儲的節點上去拿數據,並且在這個節點上註冊一個數據變更的Watcher監聽。一旦發送數據變化,集羣中所有應用都能收到通知
3. 配置變更
我們需要修改的時候,就利用Zookeeper的修改內容接口對節點上的數據進行修改即可。Zookeeper會自動幫我們發送數據變更通知。
命名服務
命名服務是分佈式系統中比較常見的一個場景。比如我們需要在創單的時候給這個訂單生成一個全局唯一的訂單號。如果是單體應用那麼還比較好做,數據庫就有自增ID,再拼上一些訂單號前綴就可以了。但是如果是分佈式環境下,就會麻煩一些。
那我們使用Zookeeper怎麼實現呢?
- 利用Zookeeper的順序節點能夠維護每個數據節點的順序的特性,就可以做到。
Master選舉
我們有時候有這樣的一個需求,在集羣環境下,只需要一個應用來處理某個耗時資源。我們可以怎麼處理呢?
先假設我們通過數據庫來處理,我們向一個表中插入同樣的一個id的數據,根據數據庫主鍵的唯一特性,只有一個應用能夠操作成功,那麼這個應用就可以被選中成爲Master,來執行這個耗時的操作。
但是我們考慮下面這個問題:如果被選中的這個Master機器宕機了,怎麼告訴其他機器重新選舉呢?不好實現。
利用Zookeeper可以實現這樣的需求。
- 我們所有的應用同時在Zookeeper上創建一個臨時節點,如:/master/binding,只有一個應用能夠創建成功,我們稱他爲master應用,那麼其他沒創建成功的應用可以監聽這個節點。
- 一旦Master應用宕機了,這個節點因爲是臨時節點會被自動刪除,就將通知到其他的應用進行重新選舉。
分佈式鎖
Zookeeper是一個比較知名的實現分佈式鎖的一個方案。我們分排他鎖和共享鎖兩類來講解。
排他鎖
排他鎖,又稱寫鎖或獨佔鎖。如果事務T1對數據對象A加了排他鎖,那麼整個加鎖期間,其他事務都不能對這個對象A進行任何類型的操作,知道事務T1釋放了排他鎖。
下⾯我們就來看看如何藉助ZooKeeper實現排他鎖:
- 獲取鎖。我們先定義一個臨時節點作爲鎖的節點。如:/order/lock。在執行時所有應用都會來創建這個節點,但是隻有一個應用能夠創建成功。創建成功的應用我們認爲他獲取到了鎖。此時它可以執行它的業務方法。同時沒有獲取到鎖的應用可以註冊一個Watcher監聽,以便實時監聽節點變化
- 釋放鎖。當業務執行完畢後,我們要主動刪除這個臨時節點。此時其他的應用就可以去重新獲取鎖。
當持有鎖的機器宕機了,也不會造成死鎖。因爲臨時節點會自動刪除。
也可以創建臨時有序節點,只監聽比自己小一位的那個子節點。避免持有鎖的節點執行完釋放鎖的時候就需要通知所有監聽着的應用(羊羣效應)。
共享鎖
共享鎖,又稱讀鎖。如果事務T1對對象A加了共享鎖,那麼當前事務只能對A進行讀操作。其他事務T2、T3也能對這個對象加共享鎖。
- 我們需要在節點上定義是讀操作還是寫操作。如/order/lock-W-0000001 、/order/lock-R-0000002。
- 應用調用創建節點接口,來創建臨時順序節點
- 應用調用getChildren接口獲取所有已創建的所有子節點列表
- 如果應用是寫請求,判斷它是不是最小的節點的話,如果是就獲取鎖成功。否正向比它序號小的前一個節點註冊監聽
- 如果應用是讀操作,判斷它是不是最小的節點,如果是最小的節點或者比它小的都是讀節點,就獲取鎖成功。否則向比自己序號小的最後一個寫節點註冊監聽。
- 等待watcher通知,繼續進入步驟2
分佈式隊列
分佈式隊列可以簡單分爲兩⼤類:⼀種是常規的FIFO先⼊先出隊列模型,還有⼀種是等待隊列元素聚集後統⼀安排處理執⾏的Barrier模型
FIFO(First Input First Output)
使用Zookeeper實現FIFO隊列,也是依賴於Zookeeper的順序節點特性。思路如下:
- 所有應用都在/queue這個節點下創建臨時順序節點
- 調用getChildren接口來獲取他下面的所有子節點,相當於獲取隊列中的所有元素
- 判斷自己的序號是不是最小的,如果是就執行業務,執行完後刪除節點。如果不是,就監聽比自己序號小的最後一個節點
- 收到watcher後,重複步驟2
Barrier:分佈式屏障
分佈式屏障特指系統的一個協調條件,規定隊列中的元素聚齊後才能進行統一安排。比如所有的員工都把工作做完了,才一起下班去喫飯。
設計思路如下:
創建一個/queue_barrier節點,並且給這個節點賦值數字n,這個n代表只有當/queue_barrier節點下有n個子節點時,才能進行下一步操作。
- 調用getData獲取/queue_barrier節點的內容,假設內容是10
- 調用getChildren接口看看子節點個數到達10了沒,沒有就註冊子節點變更監聽.達到了就開始執行業務
- 接收到通知後,需要重複步驟2
ZAB協議
zookeeper就是使用了ZAB協議來實現的分佈式數據一致性。ZAB協議是專門爲zookeeper設計的一種支持崩潰恢復的原子廣播協議。
ZAB核心
ZAB協議定義了那些會改變Zookeeper服務器數據狀態的事務請求的處理方式。
所有這些會修改狀態的事務請求必須由一個全局唯一的服務器來協調處理,也就是Leader服務器。Leader服務器負責將客戶端請求轉化爲事務Proposal(提議),並將Proposal分發給集羣中所有的Follower服務器,之後Leader服務器需要等待所有Follower服務器的反饋,一旦超過半數的Follower服務器進行了正確的反饋後,Leader接下來就會再次向所有的Follower服務器發Commit消息,把剛纔的Proposal進行提交。
ZAB兩種模式:崩潰恢復和消息廣播
消息廣播
在消息⼴播過程中,Leader服務器會爲每⼀個Follower服務器都各⾃分配⼀個單獨的隊列,然後將需要⼴播的事務Proposal依次放⼊這些隊列中去,並且根據 FIFO策略進⾏消息發送。每⼀個Follower服務器在接收到這個事務Proposal之後,都會⾸先將其以事務⽇志的形式寫⼊到本地磁盤中去,並且在成功寫⼊後反饋給Leader服務器⼀個Ack響應。當Leader服務器接收到超過半數Follower的Ack響應後,就會⼴播⼀個Commit消息給所有的Follower服務器以通知其進⾏事務提交,同時Leader⾃身也會完成對事務的提交,⽽每⼀個Follower服務器在接收到Commit消息後,也會完成對事務的提交。
我們考慮如下兩個問題:
- Leader服務器發出proposal還沒有提交時宕機了
- Leader服務器自己提交後,發出部分讓Follower COMMIT的請求後就宕機了
解決這個問題需要ZAB的崩潰恢復模式
崩潰恢復
Leader宕機後
- 要保證已經在Leader服務器提交的事務被所有服務器提交
- 丟棄那些只是在Leader服務器提出proposal未提交的事務
針對上面兩個要求,如果讓Leader選舉算法能夠保證新選舉出來的Leader服務器擁有集羣中所有機器最⾼編號(即ZXID最⼤)的事務Proposal,那麼就可以保證這個新選舉出來的Leader⼀定具有所有已經提交的提案。他成了Leader後他有的事務肯定是已提交的所有事務了。
新Leader產生後,在正式開始⼯作(即接收客戶端的事務請求,然後提出新的提案)之前,
Leader服務器會⾸先確認事務⽇志中的所有Proposal是否都已經被集羣中過半的機器提交了,即是否完成數據同步。
數據同步
它會爲每一個Follower服務器準備一個隊列,將那些沒有被各Follower服務器同步的事務以Proposal消息的方式發送給Follower服務器,並在每一個Proposal消息後緊接着再發送一個Commit消息。等到Follower服務器將所有尚未同步的事務都成功應用到本地數據庫後,Leader服務器就會將該Follower服務器加入真正可用的Follower列表中。
運⾏時狀態分析
在ZAB協議的設計中,每個進程都有可能處於如下三種狀態之⼀
- LOOKING:Leader選舉階段。
- FOLLOWING:Follower服務器和Leader服務器保持同步狀態。
- LEADING:Leader服務器作爲主進程領導狀態。
所有進程初始狀態都是LOOKING狀態,此時不存在Leader。接下來,進程會試圖選舉出⼀個新的Leader,如果進程發現已經選舉出新的Leader了,那麼它就會切換到FOLLOWING狀態,並開始和Leader保持同步。處於FOLLOWING狀態的進程稱爲Follower,LEADING狀態的進程稱爲Leader,當Leader崩潰或放棄領導地位時,其餘的Follower進程就會轉換到LOOKING狀態開始新⼀輪的Leader選舉。
⼀個Follower只能和⼀個Leader保持同步,Leader進程和所有的Follower進程之間都通過⼼跳檢測機制來感知彼此的情況。若Leader能夠在超時時間內正常收到⼼跳檢測,那麼Follower就會⼀直與該Leader保持連接,⽽如果在指定時間內Leader⽆法從過半的Follower進程那⾥接收到⼼跳檢測,或者TCP連接斷開,那麼Leader會放棄當前週期的領導,並轉換到LOOKING狀態,其他的Follower也會選擇放棄這個Leader,同時轉換到LOOKING狀態,之後會進⾏新⼀輪的Leader選舉。
ZAB與Paxos的聯繫和區別
聯繫:
- 都存在一個類似Leader的角色
- Leader進程都會等待超半數的Follower做出正確反饋後,纔會將一個提議提交
- 在ZAB協議中,每個Proposal中都包含了⼀個epoch值,⽤來代表當前的Leader週期,在Paxos算法中,同樣存在這樣的⼀個標識,名字爲Ballot。
區別:
- Paxos算法中,新選舉產⽣的主進程會進⾏兩個階段的⼯作,第⼀階段稱爲讀階段,新的主進程和其他進程通信來收集主進程提出的提議,並將它們提交。第⼆階段稱爲寫階段,當前主進程開始提出⾃⼰的提議
- ZAB協議在Paxos基礎上添加了同步階段,此時,新的Leader會確保存在過半的Follower已經提交了之前的Leader週期中的所有事務Proposal。這⼀同步階段的引⼊,能夠有效地保證Leader在新的週期中提出事務Proposal之前,所有的進程都已經完成了對之前所有事務Proposal的提交。
總的來說,ZAB協議和Paxos算法的本質區別在於,兩者的設計⽬標不太⼀樣。ZAB協議主要⽤於構建⼀個⾼可⽤的分佈式數據主備系統,⽽Paxos算法則⽤於構建⼀個分佈式的⼀致性狀態機系統。
最終,我們討論一個問題
Zookeeper是屬於強一致性還是弱一致性?
答案:寫入是強一致,讀取是順序一致性。Zookeeper官方說是順序一致性。
因爲客戶讀連接ZooKeeper集羣后,所有的寫操作都必須發送給集羣唯一的leader,這個leader在內部同步塊中賦予每個寫操作一個順序序列號(內部稱爲zxid,是單調增加的),上一個寫操作不commit,下一個寫操作就不執行,這一點實際上已經實現了寫入的強一致性(線性化)了
通過嚴格按照ZXID的順序生效提案保證其順序一致性的。
Zookeeper它還爲我們提供sync()方法,強制讀取在時候從Leader同步數據。