天天看點

ZooKeeper實作原理

分享知識 傳遞快樂

什麼是ZooKeeper

​ZooKeeper​

​​ 由 ​

​Yahoo​

​​ 開發,後來捐贈給了 ​

​Apache​

​​ ,現已成為 ​

​Apache​

​​ 頂級項目。​

​ZooKeeper​

​​ 是一個開源的分布式應用程式協調伺服器,其為分布式系統提供一緻性服務。其一緻性是通過基于 ​

​Paxos​

​​ 算法的 ​

​ZAB​

​ 協定完成的。其主要功能包括:配置維護、分布式同步、叢集管理、分布式事務等。

ZooKeeper實作原理

zookeeper

簡單來說, ​

​ZooKeeper​

​ 是一個 分布式協調服務架構 。分布式?協調服務?這啥玩意?

其實解釋到分布式這個概念的時候,我發現有些同學并不是能把 分布式和叢集 這兩個概念很好的了解透。前段時間有同學和我探讨起分布式的東西,他說分布式不就是加機器嗎?一台機器不夠用再加一台抗壓呗。當然加機器這種說法也無可厚非,你一個分布式系統必定涉及到多個機器,但是你别忘了,計算機學科中還有一個相似的概念—— ​

​Cluster​

​ ,叢集不也是加機器嗎?但是 叢集 和 分布式 其實就是兩個完全不同的概念。

比如,我現在有一個秒殺服務,并發量太大單機系統承受不住,那我加幾台伺服器也 一樣 提供秒殺服務,這個時候就是 ​

​Cluster​

​ 叢集 。

ZooKeeper實作原理

cluster

但是,我現在換一種方式,我将一個秒殺服務 拆分成多個子服務 ,比如建立訂單服務,增加積分服務,扣優惠券服務等等,然後我将這些子服務都部署在不同的伺服器上 ,這個時候就是  ​

​Distributed​

​ 分布式 。

ZooKeeper實作原理

distributed

而我為什麼反駁同學所說的分布式就是加機器呢?因為我認為加機器更加适用于建構叢集,因為它真是隻有加機器。而對于分布式來說,你首先需要将業務進行拆分,然後再加機器(不僅僅是加機器那麼簡單),同時你還要去解決分布式帶來的一系列問題。

比如各個分布式元件如何協調起來,如何減少各個系統之間的耦合度,分布式事務的處理,如何去配置整個分布式系統等等。​

​ZooKeeper​

​ 主要就是解決這些問題的。

一緻性問題

設計一個分布式系統必定會遇到一個問題—— 因為分區容忍性(partition tolerance)的存在,就必定要求我們需要在系統可用性(availability)和資料一緻性(consistency)中做出權衡 。這就是著名的 ​

​CAP​

​ 定理。

了解起來其實很簡單,比如說把一個班級作為整個系統,而學生是系統中的一個個獨立的子系統。這個時候班裡的小紅小明偷偷談戀愛被班裡的大嘴巴小花發現了,小花欣喜若狂告訴了周圍的人,然後小紅小明談戀愛的消息在班級裡傳播起來了。當在消息的傳播(散布)過程中,你抓到一個同學問他們的情況,如果回答你不知道,那麼說明整個班級系統出現了資料不一緻的問題(因為小花已經知道這個消息了)。而如果他直接不回答你,因為整個班級有消息在進行傳播(為了保證一緻性,需要所有人都知道才可提供服務),這個時候就出現了系統的可用性問題。

而上述前者就是 ​

​Eureka​

​​ 的處理方式,它保證了AP(可用性),後者就是我們今天所要将的 ​

​ZooKeeper​

​ 的處理方式,它保證了CP(資料一緻性)。

一緻性協定和算法

而為了解決資料一緻性問題,在科學家和程式員的不斷探索中,就出現了很多的一緻性協定和算法。比如 2PC(兩階段送出),3PC(三階段送出),Paxos算法等等。

這時候請你思考一個問題,同學之間如果采用傳紙條的方式去傳播消息,那麼就會出現一個問題——我咋知道我的小紙條有沒有傳到我想要傳遞的那個人手中呢?萬一被哪個小家夥給劫持篡改了呢,對吧?

這個時候就引申出一個概念—— 拜占庭将軍問題 。它意指 在不可靠信道上試圖通過消息傳遞的方式達到一緻性是不可能的, 是以所有的一緻性算法的 必要前提 就是安全可靠的消息通道。

而為什麼要去解決資料一緻性的問題?你想想,如果一個秒殺系統将服務拆分成了下訂單和加積分服務,這兩個服務部署在不同的機器上了,萬一在消息的傳播過程中積分系統當機了,總不能你這邊下了訂單卻沒加積分吧?你總得保證兩邊的資料需要一緻吧?

2PC(兩階段送出)

兩階段送出是一種保證分布式系統資料一緻性的協定,現在很多資料庫都是采用的兩階段送出協定來完成 分布式事務 的處理。

在介紹2PC之前,我們先來想想分布式事務到底有什麼問題呢?

還拿秒殺系統的下訂單和加積分兩個系統來舉例吧(我想你們可能都吐了),我們此時下完訂單會發個消息給積分系統告訴它下面該增加積分了。如果我們僅僅是發送一個消息也不收回複,那麼我們的訂單系統怎麼能知道積分系統的收到消息的情況呢?如果我們增加一個收回複的過程,那麼當積分系統收到消息後傳回給訂單系統一個 ​

​Response​

​ ,但在中間出現了網絡波動,那個回複消息沒有發送成功,訂單系統是不是以為積分系統消息接收失敗了?它是不是會復原事務?但此時積分系統是成功收到消息的,它就會去處理消息然後給使用者增加積分,這個時候就會出現積分加了但是訂單沒下成功。

是以我們所需要解決的是在分布式系統中,整個調用鍊中,我們所有服務的資料處理要麼都成功要麼都失敗,即所有服務的 原子性問題 。

在兩階段送出中,主要涉及到兩個角色,分别是協調者和參與者。

第一階段:當要執行一個分布式事務的時候,事務發起者首先向協調者發起事務請求,然後協調者會給所有參與者發送 ​

​prepare​

​​ 請求(其中包括事務内容)告訴參與者你們需要執行事務了,如果能執行我發的事務内容那麼就先執行但不送出,執行後請給我回複。然後參與者收到 ​

​prepare​

​​ 消息後,他們會開始執行事務(但不送出),并将 ​

​Undo​

​​ 和 ​

​Redo​

​ 資訊記入事務日志中,之後參與者就向協調者回報是否準備好了。

第二階段:第二階段主要是協調者根據參與者回報的情況來決定接下來是否可以進行事務的送出操作,即送出事務或者復原事務。

比如這個時候 所有的參與者 都傳回了準備好了的消息,這個時候就進行事務的送出,協調者此時會給所有的參與者發送 ​

​Commit​

​ 請求 ,當參與者收到 ​

​Commit​

​ 請求的時候會執行前面執行的事務的 送出操作 ,送出完畢之後将給協調者發送送出成功的響應。

而如果在第一階段并不是所有參與者都傳回了準備好了的消息,那麼此時協調者将會給所有參與者發送 復原事務的 ​

​rollback​

​ 請求,參與者收到之後将會 復原它在第一階段所做的事務處理 ,然後再将處理情況傳回給協調者,最終協調者收到響應後便給事務發起者傳回處理失敗的結果。

ZooKeeper實作原理

2PC流程

個人覺得 2PC 實作得還是比較雞肋的,因為事實上它隻解決了各個事務的原子性問題,随之也帶來了很多的問題。

  • 單點故障問題,如果協調者挂了那麼整個系統都處于不可用的狀态了。
  • 阻塞問題,即當協調者發送 ​

    ​prepare​

    ​ 請求,參與者收到之後如果能處理那麼它将會進行事務的處理但并不送出,這個時候會一直占用着資源不釋放,如果此時協調者挂了,那麼這些資源都不會再釋放了,這會極大影響性能。
  • 資料不一緻問題,比如當第二階段,協調者隻發送了一部分的 ​

    ​commit​

    ​ 請求就挂了,那麼也就意味着,收到消息的參與者會進行事務的送出,而後面沒收到的則不會進行事務送出,那麼這時候就會産生資料不一緻性問題。

3PC(三階段送出)

因為2PC存在的一系列問題,比如單點,容錯機制缺陷等等,進而産生了 3PC(三階段送出) 。那麼這三階段又分别是什麼呢?

千萬不要吧PC了解成個人電腦了,其實他們是 phase-commit 的縮寫,即階段送出。
  1. CanCommit階段:協調者向所有參與者發送 ​

    ​CanCommit​

    ​ 請求,參與者收到請求後會根據自身情況檢視是否能執行事務,如果可以則傳回 YES 響應并進入預備狀态,否則傳回 NO 。
  2. PreCommit階段:協調者根據參與者傳回的響應來決定是否可以進行下面的 ​

    ​PreCommit​

    ​ 操作。如果上面參與者傳回的都是 YES,那麼協調者将向所有參與者發送 ​

    ​PreCommit​

    ​ 預送出請求,參與者收到預送出請求後,會進行事務的執行操作,并将 ​

    ​Undo​

    ​ 和 ​

    ​Redo​

    ​ 資訊寫入事務日志中 ,最後如果參與者順利執行了事務則給協調者傳回成功的響應。如果在第一階段協調者收到了 任何一個 NO 的資訊,或者 在一定時間内 并沒有收到全部的參與者的響應,那麼就會中斷事務,它會向所有參與者發送中斷請求(abort),參與者收到中斷請求之後會立即中斷事務,或者在一定時間内沒有收到協調者的請求,它也會中斷事務。
  3. DoCommit階段:這個階段其實和 ​

    ​2PC​

    ​ 的第二階段差不多,如果協調者收到了所有參與者在 ​

    ​PreCommit​

    ​ 階段的 YES 響應,那麼協調者将會給所有參與者發送 ​

    ​DoCommit​

    ​ 請求,參與者收到 ​

    ​DoCommit​

    ​ 請求後則會進行事務的送出工作,完成後則會給協調者傳回響應,協調者收到所有參與者傳回的事務送出成功的響應之後則完成事務。若協調者在 ​

    ​PreCommit​

    ​ 階段 收到了任何一個 NO 或者在一定時間内沒有收到所有參與者的響應 ,那麼就會進行中斷請求的發送,參與者收到中斷請求後則會 通過上面記錄的復原日志 來進行事務的復原操作,并向協調者回報復原狀況,協調者收到參與者傳回的消息後,中斷事務。
ZooKeeper實作原理

3PC流程

這裡是 ​

​3PC​

​​ 在成功的環境下的流程圖,你可以看到 ​

​3PC​

​ 在很多地方進行了逾時中斷的處理,比如協調者在指定時間内為收到全部的确認消息則進行事務中斷的處理,這樣能 減少同步阻塞的時間 。還有需要注意的是,​

​3PC​

​ 在 ​

​DoCommit​

​ 階段參與者如未收到協調者發送的送出事務的請求,它會在一定時間内進行事務的送出。為什麼這麼做呢?是因為這個時候我們肯定保證了在第一階段所有的協調者全部傳回了可以執行事務的響應,這個時候我們有理由相信其他系統都能進行事務的執行和送出,是以不管協調者有沒有發消息給參與者,進入第三階段參與者都會進行事務的送出操作。

總之,​

​3PC​

​​ 通過一系列的逾時機制很好的緩解了阻塞問題,但是最重要的一緻性并沒有得到根本的解決,比如在 ​

​PreCommit​

​ 階段,當一個參與者收到了請求之後其他參與者和協調者挂了或者出現了網絡分區,這個時候收到消息的參與者都會進行事務送出,這就會出現資料不一緻性問題。

是以,要解決一緻性問題還需要靠 ​

​Paxos​

​ 算法。

​Paxos​

​ 算法

​Paxos​

​ 算法是基于消息傳遞且具有高度容錯特性的一緻性算法,是目前公認的解決分布式一緻性問題最有效的算法之一,其解決的問題就是在分布式系統中如何就某個值(決議)達成一緻 。

在 ​

​Paxos​

​​ 中主要有三個角色,分别為 ​

​Proposer提案者​

​​、​

​Acceptor表決者​

​​、​

​Learner學習者​

​​。​

​Paxos​

​​ 算法和 ​

​2PC​

​​ 一樣,也有兩個階段,分别為 ​

​Prepare​

​​ 和 ​

​accept​

​ 階段。

prepare 階段

  • ​Proposer提案者​

    ​​:負責提出 ​

    ​proposal​

    ​,每個提案者在提出提案時都會首先擷取到一個 具有全局唯一性的、遞增的提案編号N,即在整個叢集中是唯一的編号 N,然後将該編号賦予其要提出的提案,在第一階段是隻将提案編号發送給所有的表決者。
  • ​Acceptor表決者​

    ​​:每個表決者在 ​

    ​accept​

    ​ 某提案後,會将該提案編号N記錄在本地,這樣每個表決者中儲存的已經被 accept 的提案中會存在一個編号最大的提案,其編号假設為 ​

    ​maxN​

    ​。每個表決者僅會 ​

    ​accept​

    ​ 編号大于自己本地 ​

    ​maxN​

    ​ 的提案,在準許提案時表決者會将以前接受過的最大編号的提案作為響應回報給 ​

    ​Proposer​

    ​ 。
下面是 ​

​prepare​

​ 階段的流程圖,你可以對照着參考一下。
ZooKeeper實作原理

paxos第一階段

accept 階段

當一個提案被 ​

​Proposer​

​​ 提出後,如果 ​

​Proposer​

​​ 收到了超過半數的 ​

​Acceptor​

​​ 的準許(​

​Proposer​

​​ 本身同意),那麼此時 ​

​Proposer​

​​ 會給所有的 ​

​Acceptor​

​​ 發送真正的提案(你可以了解為第一階段為試探),這個時候 ​

​Proposer​

​ 就會發送提案的内容和提案編号。

表決者收到提案請求後會再次比較本身已經準許過的最大提案編号和該提案編号,如果該提案編号 大于等于 已經準許過的最大提案編号,那麼就 ​

​accept​

​​ 該提案(此時執行提案内容但不送出),随後将情況傳回給 ​

​Proposer​

​ 。如果不滿足則不回應或者傳回 NO 。

ZooKeeper實作原理

paxos第二階段1

當 ​

​Proposer​

​​ 收到超過半數的 ​

​accept​

​​ ,那麼它這個時候會向所有的 ​

​acceptor​

​​ 發送提案的送出請求。需要注意的是,因為上述僅僅是超過半數的 ​

​acceptor​

​ 準許執行了該提案内容,其他沒有準許的并沒有執行該提案内容,是以這個時候需要向未準許的 ​

​acceptor​

​ 發送提案内容和提案編号并讓它無條件執行和送出,而對于前面已經準許過該提案的 ​

​acceptor​

​ 來說 僅僅需要發送該提案的編号 ,讓 ​

​acceptor​

​ 執行送出就行了。

ZooKeeper實作原理

paxos第二階段2

而如果 ​

​Proposer​

​​ 如果沒有收到超過半數的 ​

​accept​

​  那麼它将會将 遞增 該 ​

​Proposal​

​ 的編号,然後 重新進入 ​

​Prepare​

​ 階段 。

對于 ​

​Learner​

​​ 來說如何去學習 ​

​Acceptor​

​ 準許的提案内容,這有很多方式,讀者可以自己去了解一下,這裡不做過多解釋。

​paxos​

​ 算法的死循環問題

其實就有點類似于兩個人吵架,小明說我是對的,小紅說我才是對的,兩個人據理力争的誰也不讓誰🤬🤬。

比如說,此時提案者 P1 提出一個方案 M1,完成了 ​

​Prepare​

​​ 階段的工作,這個時候 ​

​acceptor​

​​ 則準許了 M1,但是此時提案者 P2 同時也提出了一個方案 M2,它也完成了 ​

​Prepare​

​​ 階段的工作。然後 P1 的方案已經不能在第二階段被準許了(因為 ​

​acceptor​

​​ 已經準許了比 M1 更大的 M2),是以 P1 自增方案變為 M3 重新進入 ​

​Prepare​

​​ 階段,然後 ​

​acceptor​

​​ ,又準許了新的 M3 方案,它又不能準許 M2 了,這個時候 M2 又自增進入 ​

​Prepare​

​ 階段。。。

就這樣無休無止的永遠提案下去,這就是 ​

​paxos​

​ 算法的死循環問題。

那麼如何解決呢?很簡單,人多了容易吵架,我現在 就允許一個能提案 就行了。

引出 ​

​ZAB​

​Zookeeper​

​ 架構

作為一個優秀高效且可靠的分布式協調架構,​

​ZooKeeper​

​​ 在解決分布式資料一緻性問題時并沒有直接使用 ​

​Paxos​

​​ ,而是專門定制了一緻性協定叫做 ​

​ZAB(ZooKeeper Automic Broadcast)​

​ 原子廣播協定,該協定能夠很好地支援 崩潰恢複 。

ZooKeeper實作原理

Zookeeper架構

​ZAB​

​ 中的三個角色

和介紹 ​

​Paxos​

​​ 一樣,在介紹 ​

​ZAB​

​​ 協定之前,我們首先來了解一下在 ​

​ZAB​

​​ 中三個主要的角色,​

​Leader 上司者​

​​、​

​Follower跟随者​

​​、​

​Observer觀察者​

​ 。

  • ​Leader​

    ​ :叢集中 唯一的寫請求處理者 ,能夠發起投票(投票也是為了進行寫請求)。
  • ​Follower​

    ​:能夠接收用戶端的請求,如果是讀請求則可以自己處理,如果是寫請求則要轉發給 ​

    ​Leader​

    ​ 。在選舉過程中會參與投票,有選舉權和被選舉權 。
  • ​Observer​

    ​​ :就是沒有選舉權和被選舉權的 ​

    ​Follower​

    ​ 。

在 ​

​ZAB​

​​ 協定中對 ​

​zkServer​

​(即上面我們說的三個角色的總稱) 還有兩種模式的定義,分别是 消息廣播 和 崩潰恢複 。

消息廣播模式

說白了就是 ​

​ZAB​

​​ 協定是如何處理寫請求的,上面我們不是說隻有 ​

​Leader​

​​ 能處理寫請求嘛?那麼我們的 ​

​Follower​

​​ 和 ​

​Observer​

​ 是不是也需要 同步更新資料 呢?總不能資料隻在 ​

​Leader​

​ 中更新了,其他角色都沒有得到更新吧?

不就是 在整個叢集中保持資料的一緻性 嘛?如果是你,你會怎麼做呢?

廢話,第一步肯定需要 ​

​Leader​

​ 将寫請求 廣播 出去呀,讓 ​

​Leader​

​​ 問問 ​

​Followers​

​​ 是否同意更新,如果超過半數以上的同意那麼就進行 ​

​Follower​

​​ 和 ​

​Observer​

​​ 的更新(和 ​

​Paxos​

​ 一樣)。當然這麼說有點虛,畫張圖了解一下。

ZooKeeper實作原理

消息廣播

嗯。。。看起來很簡單,貌似懂了。這兩個 ​

​Queue​

​​ 哪冒出來的?答案是 ​

​ZAB​

​ 需要讓 ​

​Follower​

​ 和 ​

​Observer​

​ 保證順序性 。何為順序性,比如我現在有一個寫請求A,此時 ​

​Leader​

​​ 将請求A廣播出去,因為隻需要半數同意就行,是以可能這個時候有一個 ​

​Follower​

​​ F1因為網絡原因沒有收到,而 ​

​Leader​

​ 又廣播了一個請求B,因為網絡原因,F1竟然先收到了請求B然後才收到了請求A,這個時候請求處理的順序不同就會導緻資料的不同,進而 産生資料不一緻問題 。

是以在 ​

​Leader​

​​ 這端,它為每個其他的 ​

​zkServer​

​ 準備了一個 隊列 ,采用先進先出的方式發送消息。由于協定是 **通過 ​

​TCP​

​ **來進行網絡通信的,保證了消息的發送順序性,接受順序性也得到了保證。

除此之外,在 ​

​ZAB​

​ 中還定義了一個 全局單調遞增的事務ID ​

​ZXID​

​​ ,它是一個64位long型,其中高32位表示 ​

​epoch​

​​ 年代,低32位表示事務id。​

​epoch​

​​ 是會根據 ​

​Leader​

​​ 的變化而變化的,當一個 ​

​Leader​

​​ 挂了,新的 ​

​Leader​

​​ 上位的時候,年代(​

​epoch​

​)就變了。而低32位可以簡單了解為遞增的事務id。

定義這個的原因也是為了順序性,每個 ​

​proposal​

​​ 在 ​

​Leader​

​ 中生成後需要 通過其 ​

​ZXID​

​ 來進行排序 ,才能得到處理。

崩潰恢複模式

說到崩潰恢複我們首先要提到 ​

​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​

​ 根本沒收到是以會丢棄。

ZooKeeper實作原理

崩潰恢複

那怎麼解決呢?

聰明的同學肯定會質疑,這個時候 ​

​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最終需要被抛棄掉 。

ZooKeeper實作原理

崩潰恢複

Zookeeper的幾個理論知識

了解了 ​

​ZAB​

​​ 協定還不夠,它僅僅是 ​

​Zookeeper​

​​ 内部實作的一種方式,而我們如何通過 ​

​Zookeeper​

​​ 去做一些典型的應用場景呢?比如說叢集管理,分布式鎖,​

​Master​

​ 選舉等等。

這就涉及到如何使用 ​

​Zookeeper​

​​ 了,但在使用之前我們還需要掌握幾個概念。比如 ​

​Zookeeper​

​ 的 資料模型 、會話機制、ACL、Watcher機制 等等。

資料模型

​zookeeper​

​​ 資料存儲結構與标準的 ​

​Unix​

​​ 檔案系統非常相似,都是在根節點下挂很多子節點(樹型)。但是 ​

​zookeeper​

​ 中沒有檔案系統中目錄與檔案的概念,而是 使用了 ​

​znode​

​ 作為資料節點 。​

​znode​

​​ 是 ​

​zookeeper​

​​ 中的最小資料單元,每個 ​

​znode​

​ 上都可以儲存資料,同時還可以挂載子節點,形成一個樹形化命名空間。

ZooKeeper實作原理

zk資料模型

每個 ​

​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,注意是子節點的 清單 ,不是内容。

會話

我想這個對于後端開發的朋友肯定不陌生,不就是 ​

​session​

​​ 嗎?隻不過 ​

​zk​

​​ 用戶端和服務端是通過 ​

​TCP​

​ 長連接配接 維持的會話機制,其實對于會話來說你可以了解為 保持連接配接狀态 。

在 ​

​zookeeper​

​​ 中,會話還有對應的事件,比如 ​

​CONNECTION_LOSS 連接配接丢失事件​

​​ 、​

​SESSION_MOVED 會話轉移事件​

​​ 、​

​SESSION_EXPIRED 會話逾時失效事件​

​ 。

ACL

​ACL​

​​ 為 ​

​Access Control Lists​

​​ ,它是一種權限控制。在 ​

​zookeeper​

​ 中定義了5種權限,它們分别為:

  • ​CREATE​

    ​ :建立子節點的權限。
  • ​READ​

    ​:擷取節點資料和子節點清單的權限。
  • ​WRITE​

    ​:更新節點資料的權限。
  • ​DELETE​

    ​:删除子節點的權限。
  • ​ADMIN​

    ​:設定節點 ACL 的權限。

Watcher機制

​Watcher​

​​ 為事件監聽器,是 ​

​zk​

​ 非常重要的一個特性,很多功能都依賴于它,它有點類似于訂閱的方式,即用戶端向服務端 注冊 指定的 ​

​watcher​

​​ ,當服務端符合了 ​

​watcher​

​ 的某些事件或要求則會 向用戶端發送事件通知 ,用戶端收到通知後找到自己定義的 ​

​Watcher​

​ 然後 執行相應的回調方法 。

ZooKeeper實作原理

watcher機制

Zookeeper的幾個典型應用場景

前面說了這麼多的理論知識,你可能聽得一頭霧水,這些玩意有啥用?能幹啥事?别急,聽我慢慢道來。

選主

還記得上面我們的所說的臨時節點嗎?因為 ​

​Zookeeper​

​ 的強一緻性,能夠很好地在保證 在高并發的情況下保證節點建立的全局唯一性 (即無法重複建立同樣的節點)。

利用這個特性,我們可以 讓多個用戶端建立一個指定的節點 ,建立成功的就是 ​

​master​

​。

但是,如果這個 ​

​master​

​ 挂了怎麼辦???

你想想為什麼我們要建立臨時節點?還記得臨時節點的生命周期嗎?​

​master​

​​ 挂了是不是代表會話斷了?會話斷了是不是意味着這個節點沒了?還記得 ​

​watcher​

​ 嗎?我們是不是可以 讓其他不是 ​

​master​

​ 的節點監聽節點的狀态 ,比如說我們監聽這個臨時節點的父節點,如果子節點個數變了就代表 ​

​master​

​ 挂了,這個時候我們 觸發回調函數進行重新選舉 ,或者我們直接監聽節點的狀态,我們可以通過節點是否已經失去連接配接來判斷 ​

​master​

​ 是否挂了等等。

ZooKeeper實作原理

選主

總的來說,我們可以完全 利用 臨時節點、節點狀态 和 ​

​watcher​

​ 來實作選主的功能,臨時節點主要用來選舉,節點狀态和​

​watcher​

​​ 可以用來判斷 ​

​master​

​ 的活性和進行重新選舉。

分布式鎖

分布式鎖的實作方式有很多種,比如 ​

​Redis​

​​ 、資料庫 、​

​zookeeper​

​​ 等。個人認為 ​

​zookeeper​

​ 在實作分布式鎖這方面是非常非常簡單的。

上面我們已經提到過了 zk在高并發的情況下保證節點建立的全局唯一性,這玩意一看就知道能幹啥了。實作互斥鎖呗,又因為能在分布式的情況下,是以能實作分布式鎖呗。

如何實作呢?這玩意其實跟選主基本一樣,我們也可以利用臨時節點的建立來實作。

首先肯定是如何擷取鎖,因為建立節點的唯一性,我們可以讓多個用戶端同時建立一個臨時節點,建立成功的就說明擷取到了鎖 。然後沒有擷取到鎖的用戶端也像上面選主的非主節點建立一個 ​

​watcher​

​ 進行節點狀态的監聽,如果這個互斥鎖被釋放了(可能擷取鎖的用戶端當機了,或者那個用戶端主動釋放了鎖)可以調用回調函數重新獲得鎖。

​zk​

​​ 中不需要向 ​

​redis​

​ 那樣考慮鎖得不到釋放的問題了,因為當用戶端挂了,節點也挂了,鎖也釋放了。是不是很簡答?

那能不能使用 ​

​zookeeper​

​ 同時實作 共享鎖和獨占鎖 呢?答案是可以的,不過稍微有點複雜而已。

還記得 有序的節點 嗎?

這個時候我規定所有建立節點必須有序,當你是讀請求(要擷取共享鎖)的話,如果 沒有比自己更小的節點,或比自己小的節點都是讀請求 ,則可以擷取到讀鎖,然後就可以開始讀了。若比自己小的節點中有寫請求 ,則目前用戶端無法擷取到讀鎖,隻能等待前面的寫請求完成。

如果你是寫請求(擷取獨占鎖),若 沒有比自己更小的節點 ,則表示目前用戶端可以直接擷取到寫鎖,對資料進行修改。若發現 有比自己更小的節點,無論是讀操作還是寫操作,目前用戶端都無法擷取到寫鎖 ,等待所有前面的操作完成。

這就很好地同時實作了共享鎖和獨占鎖,當然還有優化的地方,比如當一個鎖得到釋放它會通知所有等待的用戶端進而造成 羊群效應 。此時你可以通過讓等待的節點隻監聽他們前面的節點。

具體怎麼做呢?其實也很簡單,你可以讓 讀請求監聽比自己小的最後一個寫請求節點,寫請求隻監聽比自己小的最後一個節點 ,感興趣的小夥伴可以自己去研究一下。

命名服務

如何給一個對象設定ID,大家可能都會想到 ​

​UUID​

​​,但是 ​

​UUID​

​​ 最大的問題就在于它太長了。。。(太長不一定是好事,嘿嘿嘿)。那麼在條件允許的情況下,我們能不能使用 ​

​zookeeper​

​ 來實作呢?

我們之前提到過 ​

​zookeeper​

​ 是通過 樹形結構 來存儲資料節點的,那也就是說,對于每個節點的 全路徑,它必定是唯一的,我們可以使用節點的全路徑作為命名方式了。而且更重要的是,路徑是我們可以自己定義的,這對于我們對有些有語意的對象的ID設定可以更加便于了解。

叢集管理和注冊中心

看到這裡是不是覺得 ​

​zookeeper​

​ 實在是太強大了,它怎麼能這麼能幹!

别急,它能幹的事情還很多呢。可能我們會有這樣的需求,我們需要了解整個叢集中有多少機器在工作,我們想對及群衆的每台機器的運作時狀态進行資料采集,對叢集中機器進行上下線操作等等。

而 ​

​zookeeper​

​​ 天然支援的 ​

​watcher​

​​ 和 臨時節點能很好的實作這些需求。我們可以為每條機器建立臨時節點,并監控其父節點,如果子節點清單有變動(我們可能建立删除了臨時節點),那麼我們可以使用在其父節點綁定的 ​

​watcher​

​ 進行狀态監控和回調。

ZooKeeper實作原理

叢集管理

至于注冊中心也很簡單,我們同樣也是讓 服務提供者 在 ​

​zookeeper​

​​ 中建立一個臨時節點并且将自己的 ​

​ip、port、調用方式​

​ 寫入節點,當 服務消費者 需要進行調用的時候會 通過注冊中心找到相應的服務的位址清單(IP端口什麼的) ,并緩存到本地(友善以後調用),當消費者調用服務時,不會再去請求注冊中心,而是直接通過負載均衡算法從位址清單中取一個服務提供者的伺服器調用服務。

當服務提供者的某台伺服器當機或下線時,相應的位址會從服務提供者位址清單中移除。同時,注冊中心會将新的服務位址清單發送給服務消費者的機器并緩存在消費者本機(當然你可以讓消費者進行節點監聽,我記得 ​

​Eureka​

​ 會先試錯,然後再更新)。

ZooKeeper實作原理

注冊中心

總結

  • 分布式與叢集的差別
  • ​2PC​

    ​​ 、​

    ​3PC​

    ​ 以及 ​

    ​paxos​

    ​ 算法這些一緻性架構的原理和實作。
  • ​zookeeper​

    ​​ 專門的一緻性算法 ​

    ​ZAB​

    ​ 原子廣播協定的内容(​

    ​Leader​

    ​ 選舉、崩潰恢複、消息廣播)。
  • ​zookeeper​

    ​​ 中的一些基本概念,比如 ​

    ​ACL​

    ​,資料節點,會話,​

    ​watcher​

    ​機制等等。
  • ​zookeeper​

    ​ 的典型應用場景,比如選主,注冊中心等等。