【進階】ZooKeeper 相關概念總結
1. 開卷有益
學習是一種習慣,只有把這種習慣保持下來,每天不學習一點就感覺渾身不自在,達到這樣的境界,那麼你成為大佬也就不遠了買,正如我們標題所寫的“開卷有益”。人生匆匆,要想過得有意義,那麼加油吧!
文章很長,先贊後看,養成習慣。
2. 什麼是ZooKeeper
ZooKeeper
由Yahoo
開發,後來捐贈給了Apache
,現已成為Apache
頂級專案。ZooKeeper
是一個開源的分散式應用程式協調伺服器,其為分散式系統提供一致性服務。其一致性是通過基於Paxos
演算法的ZAB
協議完成的。其主要功能包括:配置維護、分散式同步、叢集管理、分散式事務等。
簡單來說,ZooKeeper
是一個分散式協調服務框架
其實解釋到分散式這個概念的時候,我發現有些同學並不是能把 **分散式和叢集 **這兩個概念很好的理解透。前段時間有同學和我探討起分散式的東西,他說分散式不就是加機器嗎?一臺機器不夠用再加一臺抗壓唄。當然加機器這種說法也無可厚非,你一個分散式系統必定涉及到多個機器,但是你別忘了,計算機學科中還有一個相似的概念——Cluster
,叢集不也是加機器嗎?但是 叢集 和 分散式 其實就是兩個完全不同的概念。
比如,我現在有一個秒殺服務,併發量太大單機系統承受不住,那我加幾臺伺服器也一樣提供秒殺服務,這個時候就是Cluster
叢集。
但是,我現在換一種方式,我將一個秒殺服務拆分成多個子服務
Distributed
分散式。而我為什麼反駁同學所說的分散式就是加機器呢?因為我認為加機器更加適用於構建叢集,因為它真是隻有加機器。而對於分散式來說,你首先需要將業務進行拆分,然後再加機器(不僅僅是加機器那麼簡單),同時你還要去解決分散式帶來的一系列問題。
比如各個分散式元件如何協調起來,如何減少各個系統之間的耦合度,分散式事務的處理,如何去配置整個分散式系統等等。ZooKeeper
主要就是解決這些問題的。
3. 一致性問題
設計一個分散式系統必定會遇到一個問題——因為分割槽容忍性(partition tolerance)的存在,就必定要求我們需要在系統可用性(availability)和資料一致性(consistency)中做出權衡
CAP
定理。
理解起來其實很簡單,比如說把一個班級作為整個系統,而學生是系統中的一個個獨立的子系統。這個時候班裡的小紅小明偷偷談戀愛被班裡的大嘴巴小花發現了,小花欣喜若狂告訴了周圍的人,然後小紅小明談戀愛的訊息在班級裡傳播起來了。當在訊息的傳播(散佈)過程中,你抓到一個同學問他們的情況,如果回答你不知道,那麼說明整個班級系統出現了資料不一致的問題(因為小花已經知道這個訊息了)。而如果他直接不回答你,因為整個班級有訊息在進行傳播(為了保證一致性,需要所有人都知道才可提供服務),這個時候就出現了系統的可用性問題。
而上述前者就是Eureka
的處理方式,它保證了AP(可用性),後者就是我們今天所要講的ZooKeeper
的處理方式,它保證了CP(資料一致性)。
4. 一致性協議和演算法
而為了解決資料一致性問題,在科學家和程式設計師的不斷探索中,就出現了很多的一致性協議和演算法。比如 2PC(兩階段提交),3PC(三階段提交),Paxos演算法等等。
這時候請你思考一個問題,同學之間如果採用傳紙條的方式去傳播訊息,那麼就會出現一個問題——我咋知道我的小紙條有沒有傳到我想要傳遞的那個人手中呢?萬一被哪個小傢伙給劫持篡改了呢,對吧?
這個時候就引申出一個概念——拜占庭將軍問題。它意指在不可靠通道上試圖通過訊息傳遞的方式達到一致性是不可能的, 所以所有的一致性演算法的必要前提就是安全可靠的訊息通道。
而為什麼要去解決資料一致性的問題?你想想,如果一個秒殺系統將服務拆分成了下訂單和加積分服務,這兩個服務部署在不同的機器上了,萬一在訊息的傳播過程中積分系統宕機了,總不能你這邊下了訂單卻沒加積分吧?你總得保證兩邊的資料需要一致吧?
4.1. 2PC(兩階段提交)
兩階段提交是一種保證分散式系統資料一致性的協議,現在很多資料庫都是採用的兩階段提交協議來完成分散式事務的處理。
在介紹2PC之前,我們先來想想分散式事務到底有什麼問題呢?
還拿秒殺系統的下訂單和加積分兩個系統來舉例吧(我想你們可能都吐了),我們此時下完訂單會發個訊息給積分系統告訴它下面該增加積分了。如果我們僅僅是傳送一個訊息也不收回復,那麼我們的訂單系統怎麼能知道積分系統的收到訊息的情況呢?如果我們增加一個收回復的過程,那麼當積分系統收到訊息後返回給訂單系統一個Response
,但在中間出現了網路波動,那個回覆訊息沒有傳送成功,訂單系統是不是以為積分系統訊息接收失敗了?它是不是會回滾事務?但此時積分系統是成功收到訊息的,它就會去處理訊息然後給使用者增加積分,這個時候就會出現積分加了但是訂單沒下成功。
所以我們所需要解決的是在分散式系統中,整個呼叫鏈中,我們所有服務的資料處理要麼都成功要麼都失敗,即所有服務的原子性問題。
在兩階段提交中,主要涉及到兩個角色,分別是協調者和參與者。
第一階段:當要執行一個分散式事務的時候,事務發起者首先向協調者發起事務請求,然後協調者會給所有參與者傳送prepare
請求(其中包括事務內容)告訴參與者你們需要執行事務了,如果能執行我發的事務內容那麼就先執行但不提交,執行後請給我回復。然後參與者收到prepare
訊息後,他們會開始執行事務(但不提交),並將Undo
和Redo
資訊記入事務日誌中,之後參與者就向協調者反饋是否準備好了。
第二階段:第二階段主要是協調者根據參與者反饋的情況來決定接下來是否可以進行事務的提交操作,即提交事務或者回滾事務。
比如這個時候所有的參與者都返回了準備好了的訊息,這個時候就進行事務的提交,協調者此時會給所有的參與者傳送Commit
請求,當參與者收到Commit
請求的時候會執行前面執行的事務的提交操作,提交完畢之後將給協調者傳送提交成功的響應。
而如果在第一階段並不是所有參與者都返回了準備好了的訊息,那麼此時協調者將會給所有參與者傳送回滾事務的rollback
請求,參與者收到之後將會回滾它在第一階段所做的事務處理,然後再將處理情況返回給協調者,最終協調者收到響應後便給事務發起者返回處理失敗的結果。
個人覺得 2PC 實現得還是比較雞肋的,因為事實上它只解決了各個事務的原子性問題,隨之也帶來了很多的問題。
- 單點故障問題,如果協調者掛了那麼整個系統都處於不可用的狀態了。
- 阻塞問題,即當協調者傳送
prepare
請求,參與者收到之後如果能處理那麼它將會進行事務的處理但並不提交,這個時候會一直佔用著資源不釋放,如果此時協調者掛了,那麼這些資源都不會再釋放了,這會極大影響效能。 - 資料不一致問題,比如當第二階段,協調者只發送了一部分的
commit
請求就掛了,那麼也就意味著,收到訊息的參與者會進行事務的提交,而後面沒收到的則不會進行事務提交,那麼這時候就會產生資料不一致性問題。
4.2. 3PC(三階段提交)
因為2PC存在的一系列問題,比如單點,容錯機制缺陷等等,從而產生了3PC(三階段提交)。那麼這三階段又分別是什麼呢?
千萬不要吧PC理解成個人電腦了,其實他們是 phase-commit 的縮寫,即階段提交。
- CanCommit階段:協調者向所有參與者傳送
CanCommit
請求,參與者收到請求後會根據自身情況檢視是否能執行事務,如果可以則返回 YES 響應並進入預備狀態,否則返回 NO 。 - PreCommit階段:協調者根據參與者返回的響應來決定是否可以進行下面的
PreCommit
操作。如果上面參與者返回的都是 YES,那麼協調者將向所有參與者傳送PreCommit
預提交請求,參與者收到預提交請求後,會進行事務的執行操作,並將Undo
和Redo
資訊寫入事務日誌中,最後如果參與者順利執行了事務則給協調者返回成功的響應。如果在第一階段協調者收到了任何一個 NO的資訊,或者在一定時間內並沒有收到全部的參與者的響應,那麼就會中斷事務,它會向所有參與者傳送中斷請求(abort),參與者收到中斷請求之後會立即中斷事務,或者在一定時間內沒有收到協調者的請求,它也會中斷事務。 - DoCommit階段:這個階段其實和
2PC
的第二階段差不多,如果協調者收到了所有參與者在PreCommit
階段的 YES 響應,那麼協調者將會給所有參與者傳送DoCommit
請求,參與者收到DoCommit
請求後則會進行事務的提交工作,完成後則會給協調者返回響應,協調者收到所有參與者返回的事務提交成功的響應之後則完成事務。若協調者在PreCommit
階段收到了任何一個 NO 或者在一定時間內沒有收到所有參與者的響應,那麼就會進行中斷請求的傳送,參與者收到中斷請求後則會通過上面記錄的回滾日誌來進行事務的回滾操作,並向協調者反饋回滾狀況,協調者收到參與者返回的訊息後,中斷事務。
這裡是
3PC
在成功的環境下的流程圖,你可以看到3PC
在很多地方進行了超時中斷的處理,比如協調者在指定時間內為收到全部的確認訊息則進行事務中斷的處理,這樣能減少同步阻塞的時間。還有需要注意的是,3PC
在DoCommit
階段參與者如未收到協調者傳送的提交事務的請求,它會在一定時間內進行事務的提交。為什麼這麼做呢?是因為這個時候我們肯定保證了在第一階段所有的協調者全部返回了可以執行事務的響應,這個時候我們有理由相信其他系統都能進行事務的執行和提交,所以不管協調者有沒有發訊息給參與者,進入第三階段參與者都會進行事務的提交操作。
總之,3PC
通過一系列的超時機制很好的緩解了阻塞問題,但是最重要的一致性並沒有得到根本的解決,比如在PreCommit
階段,當一個參與者收到了請求之後其他參與者和協調者掛了或者出現了網路分割槽,這個時候收到訊息的參與者都會進行事務提交,這就會出現資料不一致性問題。
所以,要解決一致性問題還需要靠Paxos
演算法。
4.3.Paxos
演算法
Paxos
演算法是基於訊息傳遞且具有高度容錯特性的一致性演算法,是目前公認的解決分散式一致性問題最有效的演算法之一,其解決的問題就是在分散式系統中如何就某個值(決議)達成一致。
在Paxos
中主要有三個角色,分別為Proposer提案者
、Acceptor表決者
、Learner學習者
。Paxos
演算法和2PC
一樣,也有兩個階段,分別為Prepare
和accept
階段。
4.3.1. prepare 階段
Proposer提案者
:負責提出proposal
,每個提案者在提出提案時都會首先獲取到一個具有全域性唯一性的、遞增的提案編號N,即在整個叢集中是唯一的編號 N,然後將該編號賦予其要提出的提案,在第一階段是隻將提案編號傳送給所有的表決者。Acceptor表決者
:每個表決者在accept
某提案後,會將該提案編號N記錄在本地,這樣每個表決者中儲存的已經被 accept 的提案中會存在一個編號最大的提案,其編號假設為maxN
。每個表決者僅會accept
編號大於自己本地maxN
的提案,在批准提案時表決者會將以前接受過的最大編號的提案作為響應反饋給Proposer
。
下面是
prepare
階段的流程圖,你可以對照著參考一下。
4.3.2. accept 階段
當一個提案被Proposer
提出後,如果Proposer
收到了超過半數的Acceptor
的批准(Proposer
本身同意),那麼此時Proposer
會給所有的Acceptor
傳送真正的提案(你可以理解為第一階段為試探),這個時候Proposer
就會發送提案的內容和提案編號。
表決者收到提案請求後會再次比較本身已經批准過的最大提案編號和該提案編號,如果該提案編號大於等於已經批准過的最大提案編號,那麼就accept
該提案(此時執行提案內容但不提交),隨後將情況返回給Proposer
。如果不滿足則不迴應或者返回 NO 。
當Proposer
收到超過半數的accept
,那麼它這個時候會向所有的acceptor
傳送提案的提交請求。需要注意的是,因為上述僅僅是超過半數的acceptor
批准執行了該提案內容,其他沒有批准的並沒有執行該提案內容,所以這個時候需要向未批准的acceptor
傳送提案內容和提案編號並讓它無條件執行和提交,而對於前面已經批准過該提案的acceptor
來說僅僅需要傳送該提案的編號,讓acceptor
執行提交就行了。
而如果Proposer
如果沒有收到超過半數的accept
那麼它將會將遞增該Proposal
的編號,然後重新進入Prepare
階段。
對於
Learner
來說如何去學習Acceptor
批准的提案內容,這有很多方式,讀者可以自己去了解一下,這裡不做過多解釋。
4.3.3.paxos
演算法的死迴圈問題
其實就有點類似於兩個人吵架,小明說我是對的,小紅說我才是對的,兩個人據理力爭的誰也不讓誰 。
比如說,此時提案者 P1 提出一個方案 M1,完成了Prepare
階段的工作,這個時候acceptor
則批准了 M1,但是此時提案者 P2 同時也提出了一個方案 M2,它也完成了Prepare
階段的工作。然後 P1 的方案已經不能在第二階段被批准了(因為acceptor
已經批准了比 M1 更大的 M2),所以 P1 自增方案變為 M3 重新進入Prepare
階段,然後acceptor
,又批准了新的 M3 方案,它又不能批准 M2 了,這個時候 M2 又自增進入Prepare
階段。。。
就這樣無休無止的永遠提案下去,這就是paxos
演算法的死迴圈問題。
那麼如何解決呢?很簡單,人多了容易吵架,我現在就允許一個能提案就行了。
5. 引出ZAB
5.1.Zookeeper
架構
作為一個優秀高效且可靠的分散式協調框架,ZooKeeper
在解決分散式資料一致性問題時並沒有直接使用Paxos
,而是專門定製了一致性協議叫做ZAB(ZooKeeper Automic Broadcast)
原子廣播協議,該協議能夠很好地支援崩潰恢復。
5.2.ZAB
中的三個角色
和介紹Paxos
一樣,在介紹ZAB
協議之前,我們首先來了解一下在ZAB
中三個主要的角色,Leader 領導者
、Follower跟隨者
、Observer觀察者
。
Leader
:叢集中唯一的寫請求處理者,能夠發起投票(投票也是為了進行寫請求)。Follower
:能夠接收客戶端的請求,如果是讀請求則可以自己處理,如果是寫請求則要轉發給Leader
。在選舉過程中會參與投票,有選舉權和被選舉權。Observer
:就是沒有選舉權和被選舉權的Follower
。
在ZAB
協議中對zkServer
(即上面我們說的三個角色的總稱) 還有兩種模式的定義,分別是訊息廣播和崩潰恢復。
5.3. 訊息廣播模式
說白了就是ZAB
協議是如何處理寫請求的,上面我們不是說只有Leader
能處理寫請求嘛?那麼我們的Follower
和Observer
是不是也需要同步更新資料呢?總不能資料只在Leader
中更新了,其他角色都沒有得到更新吧?
不就是在整個叢集中保持資料的一致性嘛?如果是你,你會怎麼做呢?
廢話,第一步肯定需要Leader
將寫請求廣播出去呀,讓Leader
問問Followers
是否同意更新,如果超過半數以上的同意那麼就進行Follower
和Observer
的更新(和Paxos
一樣)。當然這麼說有點虛,畫張圖理解一下。
嗯。。。看起來很簡單,貌似懂了。這兩個Queue
哪冒出來的?答案是ZAB
需要讓Follower
和Observer
保證順序性。何為順序性,比如我現在有一個寫請求A,此時Leader
將請求A廣播出去,因為只需要半數同意就行,所以可能這個時候有一個Follower
F1因為網路原因沒有收到,而Leader
又廣播了一個請求B,因為網路原因,F1竟然先收到了請求B然後才收到了請求A,這個時候請求處理的順序不同就會導致資料的不同,從而產生資料不一致問題。
所以在Leader
這端,它為每個其他的zkServer
準備了一個佇列,採用先進先出的方式傳送訊息。由於協議是 **通過TCP
**來進行網路通訊的,保證了訊息的傳送順序性,接受順序性也得到了保證。
除此之外,在ZAB
中還定義了一個全域性單調遞增的事務IDZXID
,它是一個64位long型,其中高32位表示epoch
年代,低32位表示事務id。epoch
是會根據Leader
的變化而變化的,當一個Leader
掛了,新的Leader
上位的時候,年代(epoch
)就變了。而低32位可以簡單理解為遞增的事務id。
定義這個的原因也是為了順序性,每個proposal
在Leader
中生成後需要通過其ZXID
來進行排序,才能得到處理。
5.4. 崩潰恢復模式
說到崩潰恢復我們首先要提到ZAB
中的Leader
選舉演算法,當系統出現崩潰影響最大應該是Leader
的崩潰,因為我們只有一個Leader
,所以當Leader
出現問題的時候我們勢必需要重新選舉Leader
。
Leader
選舉可以分為兩個不同的階段,第一個是我們提到的Leader
宕機需要重新選舉,第二則是當Zookeeper
啟動時需要進行系統的Leader
初始化選舉。下面我先來介紹一下ZAB
是如何進行初始化選舉的。
假設我們叢集中有3臺機器,那也就意味著我們需要兩臺以上同意(超過半數)。比如這個時候我們啟動了server1
,它會首先投票給自己,投票內容為伺服器的myid
和ZXID
,因為初始化所以ZXID
都為0,此時server1
發出的投票為 (1,0)。但此時server1
的投票僅為1,所以不能作為Leader
,此時還在選舉階段所以整個叢集處於Looking
狀態。
接著server2
啟動了,它首先也會將投票選給自己(2,0),並將投票資訊廣播出去(server1
也會,只是它那時沒有其他的伺服器了),server1
在收到server2
的投票資訊後會將投票資訊與自己的作比較。首先它會比較ZXID
,ZXID
大的優先為Leader
,如果相同則比較myid
,myid
大的優先作為Leader
。所以此時server1
發現server2
更適合做Leader
,它就會將自己的投票資訊更改為(2,0)然後再廣播出去,之後server2
收到之後發現和自己的一樣無需做更改,並且自己的投票已經超過半數,則確定server2
為Leader
,server1
也會將自己伺服器設定為Following
變為Follower
。整個伺服器就從Looking
變為了正常狀態。
當server3
啟動發現叢集沒有處於Looking
狀態時,它會直接以Follower
的身份加入叢集。
還是前面三個server
的例子,如果在整個叢集執行的過程中server2
掛了,那麼整個叢集會如何重新選舉Leader
呢?其實和初始化選舉差不多。
首先毫無疑問的是剩下的兩個Follower
會將自己的狀態從Following
變為Looking
狀態,然後每個server
會向初始化投票一樣首先給自己投票(這不過這裡的zxid
可能不是0了,這裡為了方便隨便取個數字)。
假設server1
給自己投票為(1,99),然後廣播給其他server
,server3
首先也會給自己投票(3,95),然後也廣播給其他server
。server1
和server3
此時會收到彼此的投票資訊,和一開始選舉一樣,他們也會比較自己的投票和收到的投票(zxid
大的優先,如果相同那麼就myid
大的優先)。這個時候server1
收到了server3
的投票發現沒自己的合適故不變,server3
收到server1
的投票結果後發現比自己的合適於是更改投票為(1,99)然後廣播出去,最後server1
收到了發現自己的投票已經超過半數就把自己設為Leader
,server3
也隨之變為Follower
。
請注意
ZooKeeper
為什麼要設定奇數個結點?比如這裡我們是三個,掛了一個我們還能正常工作,掛了兩個我們就不能正常工作了(已經沒有超過半數的節點數了,所以無法進行投票等操作了)。而假設我們現在有四個,掛了一個也能工作,但是掛了兩個也不能正常工作了,這是和三個一樣的,而三個比四個還少一個,帶來的效益是一樣的,所以Zookeeper
推薦奇數個server
。
那麼說完了ZAB
中的Leader
選舉方式之後我們再來了解一下崩潰恢復是什麼玩意?
其實主要就是當叢集中有機器掛了,我們整個叢集如何保證資料一致性?
如果只是Follower
掛了,而且掛的沒超過半數的時候,因為我們一開始講了在Leader
中會維護佇列,所以不用擔心後面的資料沒接收到導致資料不一致性。
如果Leader
掛了那就麻煩了,我們肯定需要先暫停服務變為Looking
狀態然後進行Leader
的重新選舉(上面我講過了),但這個就要分為兩種情況了,分別是確保已經被Leader提交的提案最終能夠被所有的Follower提交和跳過那些已經被丟棄的提案。
確保已經被Leader提交的提案最終能夠被所有的Follower提交是什麼意思呢?
假設Leader (server2)
傳送commit
請求(忘了請看上面的訊息廣播模式),他傳送給了server3
,然後要發給server1
的時候突然掛了。這個時候重新選舉的時候我們如果把server1
作為Leader
的話,那麼肯定會產生資料不一致性,因為server3
肯定會提交剛剛server2
傳送的commit
請求的提案,而server1
根本沒收到所以會丟棄。
那怎麼解決呢?
聰明的同學肯定會質疑,這個時候server1
已經不可能成為Leader
了,因為server1
和server3
進行投票選舉的時候會比較ZXID
,而此時server3
的ZXID
肯定比server1
的大了。(不理解可以看前面的選舉演算法)
那麼跳過那些已經被丟棄的提案又是什麼意思呢?
假設Leader (server2)
此時同意了提案N1,自身提交了這個事務並且要傳送給所有Follower
要commit
的請求,卻在這個時候掛了,此時肯定要重新進行Leader
的選舉,比如說此時選server1
為Leader
(這無所謂)。但是過了一會,這個掛掉的Leader
又重新恢復了,此時它肯定會作為Follower
的身份進入叢集中,需要注意的是剛剛server2
已經同意提交了提案N1,但其他server
並沒有收到它的commit
資訊,所以其他server
不可能再提交這個提案N1了,這樣就會出現資料不一致性問題了,所以該提案N1最終需要被拋棄掉。
6. Zookeeper的幾個理論知識
瞭解了ZAB
協議還不夠,它僅僅是Zookeeper
內部實現的一種方式,而我們如何通過Zookeeper
去做一些典型的應用場景呢?比如說叢集管理,分散式鎖,Master
選舉等等。
這就涉及到如何使用Zookeeper
了,但在使用之前我們還需要掌握幾個概念。比如Zookeeper
的資料模型、會話機制、ACL、Watcher機制等等。
6.1. 資料模型
zookeeper
資料儲存結構與標準的Unix
檔案系統非常相似,都是在根節點下掛很多子節點(樹型)。但是zookeeper
中沒有檔案系統中目錄與檔案的概念,而是使用了znode
作為資料節點。znode
是zookeeper
中的最小資料單元,每個znode
上都可以儲存資料,同時還可以掛載子節點,形成一個樹形化名稱空間。
每個znode
都有自己所屬的節點型別和節點狀態。
其中節點型別可以分為持久節點、持久順序節點、臨時節點和臨時順序節點。
- 持久節點:一旦建立就一直存在,直到將其刪除。
- 持久順序節點:一個父節點可以為其子節點維護一個建立的先後順序,這個順序體現在節點名稱上,是節點名稱後自動新增一個由 10 位數字組成的數字串,從 0 開始計數。
- 臨時節點:臨時節點的生命週期是與客戶端會話繫結的,會話消失則節點消失。臨時節點只能做葉子節點,不能建立子節點。
- 臨時順序節點:父節點可以建立一個維持了順序的臨時節點(和前面的持久順序性節點一樣)。
節點狀態中包含了很多節點的屬性比如czxid
、mzxid
等等,在zookeeper
中是使用Stat
這個類來維護的。下面我列舉一些屬性解釋。
czxid
:Created ZXID
,該資料節點被建立時的事務ID。mzxid
:Modified ZXID
,節點最後一次被更新時的事務ID。ctime
:Created Time
,該節點被建立的時間。mtime
:Modified Time
,該節點最後一次被修改的時間。version
:節點的版本號。cversion
:子節點的版本號。aversion
:節點的ACL
版本號。ephemeralOwner
:建立該節點的會話的sessionID
,如果該節點為持久節點,該值為0。dataLength
:節點資料內容的長度。numChildre
:該節點的子節點個數,如果為臨時節點為0。pzxid
:該節點子節點列表最後一次被修改時的事務ID,注意是子節點的列表,不是內容。
6.2. 會話
我想這個對於後端開發的朋友肯定不陌生,不就是session
嗎?只不過zk
客戶端和服務端是通過TCP
長連線維持的會話機制,其實對於會話來說你可以理解為保持連線狀態。
在zookeeper
中,會話還有對應的事件,比如CONNECTION_LOSS 連線丟失事件
、SESSION_MOVED 會話轉移事件
、SESSION_EXPIRED 會話超時失效事件
。
6.3. ACL
ACL
為Access Control Lists
,它是一種許可權控制。在zookeeper
中定義了5種許可權,它們分別為:
CREATE
:建立子節點的許可權。READ
:獲取節點資料和子節點列表的許可權。WRITE
:更新節點資料的許可權。DELETE
:刪除子節點的許可權。ADMIN
:設定節點 ACL 的許可權。
6.4. Watcher機制
Watcher
為事件監聽器,是zk
非常重要的一個特性,很多功能都依賴於它,它有點類似於訂閱的方式,即客戶端向服務端註冊指定的watcher
,當服務端符合了watcher
的某些事件或要求則會向客戶端傳送事件通知,客戶端收到通知後找到自己定義的Watcher
然後執行相應的回撥方法。
7. Zookeeper的幾個典型應用場景
前面說了這麼多的理論知識,你可能聽得一頭霧水,這些玩意有啥用?能幹啥事?別急,聽我慢慢道來。
7.1. 選主
還記得上面我們的所說的臨時節點嗎?因為Zookeeper
的強一致性,能夠很好地在保證在高併發的情況下保證節點建立的全域性唯一性(即無法重複建立同樣的節點)。
利用這個特性,我們可以讓多個客戶端建立一個指定的節點,建立成功的就是master
。
但是,如果這個master
掛了怎麼辦???
你想想為什麼我們要建立臨時節點?還記得臨時節點的生命週期嗎?master
掛了是不是代表會話斷了?會話斷了是不是意味著這個節點沒了?還記得watcher
嗎?我們是不是可以讓其他不是master
的節點監聽節點的狀態,比如說我們監聽這個臨時節點的父節點,如果子節點個數變了就代表master
掛了,這個時候我們觸發回撥函式進行重新選舉,或者我們直接監聽節點的狀態,我們可以通過節點是否已經失去連線來判斷master
是否掛了等等。
總的來說,我們可以完全利用 臨時節點、節點狀態 和watcher
來實現選主的功能,臨時節點主要用來選舉,節點狀態和watcher
可以用來判斷master
的活性和進行重新選舉。
7.2. 分散式鎖
分散式鎖的實現方式有很多種,比如Redis
、資料庫 、zookeeper
等。個人認為zookeeper
在實現分散式鎖這方面是非常非常簡單的。
上面我們已經提到過了zk在高併發的情況下保證節點建立的全域性唯一性,這玩意一看就知道能幹啥了。實現互斥鎖唄,又因為能在分散式的情況下,所以能實現分散式鎖唄。
如何實現呢?這玩意其實跟選主基本一樣,我們也可以利用臨時節點的建立來實現。
首先肯定是如何獲取鎖,因為建立節點的唯一性,我們可以讓多個客戶端同時建立一個臨時節點,建立成功的就說明獲取到了鎖。然後沒有獲取到鎖的客戶端也像上面選主的非主節點建立一個watcher
進行節點狀態的監聽,如果這個互斥鎖被釋放了(可能獲取鎖的客戶端宕機了,或者那個客戶端主動釋放了鎖)可以呼叫回撥函式重新獲得鎖。
zk
中不需要向redis
那樣考慮鎖得不到釋放的問題了,因為當客戶端掛了,節點也掛了,鎖也釋放了。是不是很簡答?
那能不能使用zookeeper
同時實現共享鎖和獨佔鎖呢?答案是可以的,不過稍微有點複雜而已。
還記得有序的節點嗎?
這個時候我規定所有建立節點必須有序,當你是讀請求(要獲取共享鎖)的話,如果沒有比自己更小的節點,或比自己小的節點都是讀請求,則可以獲取到讀鎖,然後就可以開始讀了。若比自己小的節點中有寫請求,則當前客戶端無法獲取到讀鎖,只能等待前面的寫請求完成。
如果你是寫請求(獲取獨佔鎖),若沒有比自己更小的節點,則表示當前客戶端可以直接獲取到寫鎖,對資料進行修改。若發現有比自己更小的節點,無論是讀操作還是寫操作,當前客戶端都無法獲取到寫鎖,等待所有前面的操作完成。
這就很好地同時實現了共享鎖和獨佔鎖,當然還有優化的地方,比如當一個鎖得到釋放它會通知所有等待的客戶端從而造成羊群效應。此時你可以通過讓等待的節點只監聽他們前面的節點。
具體怎麼做呢?其實也很簡單,你可以讓讀請求監聽比自己小的最後一個寫請求節點,寫請求只監聽比自己小的最後一個節點,感興趣的小夥伴可以自己去研究一下。
7.3. 命名服務
如何給一個物件設定ID,大家可能都會想到UUID
,但是UUID
最大的問題就在於它太長了。。。(太長不一定是好事,嘿嘿嘿)。那麼在條件允許的情況下,我們能不能使用zookeeper
來實現呢?
我們之前提到過zookeeper
是通過樹形結構來儲存資料節點的,那也就是說,對於每個節點的全路徑,它必定是唯一的,我們可以使用節點的全路徑作為命名方式了。而且更重要的是,路徑是我們可以自己定義的,這對於我們對有些有語意的物件的ID設定可以更加便於理解。
7.4. 叢集管理和註冊中心
看到這裡是不是覺得zookeeper
實在是太強大了,它怎麼能這麼能幹!
別急,它能幹的事情還很多呢。可能我們會有這樣的需求,我們需要了解整個叢集中有多少機器在工作,我們想對及群眾的每臺機器的執行時狀態進行資料採集,對叢集中機器進行上下線操作等等。
而zookeeper
天然支援的watcher
和 臨時節點能很好的實現這些需求。我們可以為每條機器建立臨時節點,並監控其父節點,如果子節點列表有變動(我們可能建立刪除了臨時節點),那麼我們可以使用在其父節點繫結的watcher
進行狀態監控和回撥。
至於註冊中心也很簡單,我們同樣也是讓服務提供者在zookeeper
中建立一個臨時節點並且將自己的ip、port、呼叫方式
寫入節點,當服務消費者需要進行呼叫的時候會通過註冊中心找到相應的服務的地址列表(IP埠什麼的),並快取到本地(方便以後呼叫),當消費者呼叫服務時,不會再去請求註冊中心,而是直接通過負載均衡演算法從地址列表中取一個服務提供者的伺服器呼叫服務。
當服務提供者的某臺伺服器宕機或下線時,相應的地址會從服務提供者地址列表中移除。同時,註冊中心會將新的服務地址列表傳送給服務消費者的機器並快取在消費者本機(當然你可以讓消費者進行節點監聽,我記得Eureka
會先試錯,然後再更新)。
8. 總結
看到這裡的同學實在是太有耐心了,如果覺得我寫得不錯的話點個贊哈。
不知道大家是否還記得我講了什麼。
這篇文章中我帶大家入門了zookeeper
這個強大的分散式協調框架。現在我們來簡單梳理一下整篇文章的內容。
-
分散式與叢集的區別
-
2PC
、3PC
以及paxos
演算法這些一致性框架的原理和實現。 -
zookeeper
專門的一致性演算法ZAB
原子廣播協議的內容(Leader
選舉、崩潰恢復、訊息廣播)。 -
zookeeper
中的一些基本概念,比如ACL
,資料節點,會話,watcher
機制等等。 -
zookeeper
的典型應用場景,比如選主,註冊中心等等。如果忘了可以回去看看再次理解一下,如果有疑問和建議歡迎提出。
作者:Snailclimb
連結:【進階】ZooKeeper 相關概念總結
來源:github