分佈式專題(五)Zookeeper實現原理

一、Zookeeper 的由來

        從上面的案例可以看出,分佈式系統的很多難題,都是由於缺少協調機制造成的。在分佈式協調這塊做得比較好的,有 Google 的 Chubby 以及 Apache 的 Zookeeper。Google Chubby 是一個分佈式鎖服務,通過 Google Chubby 來解決分佈式協作、Master 選舉等與分佈式鎖服務相關的問題。
        Zookeeper 也是類似,因爲當時在雅虎內部的很多系統都需要依賴一個系統來進行分佈式協調,但是谷歌的Chubby
是不開源的,所以後來雅虎基於 Chubby 的思想開發了zookeeper,並捐贈給了 Apache。

       在上面這個架構下 zookeeper 以後,可以用來解決 task 執行問題,各個服務先去 zookeeper 上去註冊節點,然後獲
得權限以後再來訪問 task

二、Zookeeper 的設計猜想

      zookeeper 主要是解決分佈式環境下的服務協調問題而產生的,如果我們要去實現一個 zookeeper 這樣的中間件,我們需要做什麼?
      1. 防止單點故障:如果要防止 zookeeper 這個中間件的單點故障,那就勢必要做集羣。而且這個集羣如果要滿足高性能要求的話,還得是一個高性能高可用的集羣。高性能意味着這個集羣能夠分擔客戶端的請求流量,高可用意味着集羣中的某一個節點宕機以後,不影響整個集羣的數據和繼續提供服務的可能性。
      結論: 所以這個中間件需要考慮到集羣,而且這個集羣還需要分攤客戶端的請求流量
      2. 接着上面那個結論再來思考,如果要滿足這樣的一個高性能集羣,我們最直觀的想法應該是,每個節點都能接收到請求,並且每個節點的數據都必須要保持一致。要實現各個節點的數據一致性,就勢必要一個 leader 節點負責協調和數據同步操作。這個我想大家都知道,如果在這樣一個集羣中沒有 leader 節點,每個節點都可以接收所有請求,那麼這個集羣的數據同步的複雜度是非常大。
      結論:所以這個集羣中涉及到數據同步以及會存在leader 節點

      3. 繼續思考,如何在這些節點中選舉出 leader 節點,以及leader 掛了以後,如何恢復呢?
      結論:所以 zookeeper 用了基於 paxos 理論所衍生出來的 ZAB 協議
      4. leader 節點如何和其他節點保證數據一致性,並且要求是強一致的。在分佈式系統中,每一個機器節點雖然都能夠明確知道自己進行的事務操作過程是成功和失敗,但是卻無法直接獲取其他分佈式節點的操作結果。所以當一個事務操作涉及到跨節點的時候,就需要用到分佈式事務,分佈式事務的數據一致性協議有 2PC 協議和3PC 協議。
       基於這些猜想,我們基本上知道 zookeeper 爲什麼要用到zab 理論來做選舉、爲什麼要做集羣、爲什麼要用到分佈式事務來實現數據一致性了。接下來我們逐步去剖析zookeeper 裏面的這些內容

三、關於 2PC 提交

       (Two Phase Commitment Protocol)當一個事務操作需要跨越多個分佈式節點的時候,爲了保持事務處理的 ACID特性,就需要引入一個“協調者”(TM)來統一調度所有分佈式節點的執行邏輯,這些被調度的分佈式節點被稱爲 AP。TM 負責調度 AP 的行爲,並最終決定這些 AP 是否要把事務真正進行提交;因爲整個事務是分爲兩個階段提交,所以叫 2pc

階段一:提交事務請求(投票)

1. 事務詢問
        協調者向所有的參與者發送事務內容,詢問是否可以執行事務提交操作,並開始等待各參與者的響應
2. 執行事務
        各個參與者節點執行事務操作,並將 Undo 和 Redo 信息記錄到事務日誌中,儘量把提交過程中所有消耗時間的操作和
準備都提前完成確保後面 100%成功提交事務
3. 各個參與者向協調者反饋事務詢問的響應
        如果各個參與者成功執行了事務操作,那麼就反饋給參與者yes 的響應,表示事務可以執行;如果參與者沒有成功執行事務,就反饋給協調者 no 的響應,表示事務不可以執行,上面這個階段有點類似協調者組織各個參與者對一次事務操作的投票表態過程,因此 2pc 協議的第一個階段稱爲“投票階段”,即各參與者投票表名是否需要繼續執行接下去的事務提交操作。

階段二:執行事務提交

       在這個階段,協調者會根據各參與者的反饋情況來決定最終是否可以進行事務提交操作,正常情況下包含兩種可能:執行事務、中斷事務

四、Zookeeper 的集羣

        在 zookeeper 中,客戶端會隨機連接到 zookeeper 集羣中的一個節點,如果是讀請求,就直接從當前節點中讀取數據,如果是寫請求,那麼請求會被轉發給leader提交事務,然後 leader 會廣播事務,只要有超過半數節點寫入成功,那麼寫請求就會被提交(類 2PC 事務)

       所有事務請求必須由一個全局唯一的服務器來協調處理,這個服務器就是 Leader 服務器,其他的服務器就是follower。leader 服務器把客戶端的失去請求轉化成一個事務 Proposal(提議),並把這個 Proposal 分發給集羣中的所有 Follower 服務器。之後 Leader 服務器需要等待所有Follower 服務器的反饋,一旦超過半數的 Follower 服務器進行了正確的反饋,那麼Leader 就會再次向所有的Follower 服務器發送 Commit 消息,要求各個 follower 節點對前面的一個 Proposal 進行提交;

集羣角色

Leader 角色

Leader 服務器是整個 zookeeper 集羣的核心,主要的工作
任務有兩項
       1. 事物請求的唯一調度和處理者,保證集羣事物處理的順序性
       2. 集羣內部各服務器的調度者

Follower 角色

Follower 角色的主要職責是
        1. 處理客戶端非事物請求、轉發事物請求給 leader 服務器
        2. 參與事物請求 Proposal 的投票(需要半數以上服務器通過才能通知 leader commit 數據; Leader 發起的提案,要求 Follower 投票)
        3. 參與 Leader 選舉的投票

Observer 角色

         Observer 是 zookeeper3.3 開始引入的一個全新的服務器角色,從字面來理解,該角色充當了觀察者的角色。觀察zookeeper集羣中的最新狀態變化並將這些狀態變化同步到 observer 服務器上。Observer 的工作原理與follower 角色基本一致,而它和 follower 角色唯一的不同在於 observer 不參與任何形式的投票,包括事物請求Proposal的投票和leader選舉的投票。簡單來說,observer服務器只提供非事物請求服務,通常在於不影響集羣事物處理能力的前提下提升集羣非事物處理的能力

集羣組成

       通常 zookeeper 是由 2n+1 臺 server 組成,每個 server 都知道彼此的存在。對於 2n+1 臺 server,只要有 n+1 臺(大
多數)server 可用,整個系統保持可用。我們已經瞭解到,一個 zookeeper 集羣如果要對外提供可用的服務,那麼集羣中必須要有過半的機器正常工作並且彼此之間能夠正常通信,基於這個特性,如果向搭建一個能夠允許 F 臺機器down 掉的集羣,那麼就要部署 2*F+1 臺服務器構成的zookeeper 集羣。因此 3 臺機器構成的 zookeeper 集羣,能夠在掛掉一臺機器後依然正常工作。一個 5 臺機器集羣的服務,能夠對 2 臺機器怪調的情況下進行容災。如果一臺由 6 臺服務構成的集羣,同樣只能掛掉 2 臺機器。因此,5 臺和 6 臺在容災能力上並沒有明顯優勢,反而增加了網絡通信負擔。系統啓動時,集羣中的 server 會選舉出一臺server 爲 Leader,其它的就作爲 follower(這裏先不考慮observer 角色)。

       之所以要滿足這樣一個等式,是因爲一個節點要成爲集羣中的 leader,需要有超過及羣衆過半數的節點支持,這個涉及到 leader 選舉算法。同時也涉及到事務請求的提交投票

五、ZAB 協議

       ZAB(Zookeeper Atomic Broadcast) 協議是爲分佈式協調服務 ZooKeeper 專門設計的一種支持崩潰恢復的原子廣播協議。在 ZooKeeper 中,主要依賴 ZAB 協議來實現分佈式數據一致性,基於該協議,ZooKeeper 實現了一種主備模式的系統架構來保持集羣中各個副本之間的數據一致性。

ZAB 協議介紹

ZAB 協議包含兩種基本模式,分別是
1. 崩潰恢復
2. 原子廣播

       當整個集羣在啓動時,或者當 leader 節點出現網絡中斷、崩潰等情況時,ZAB 協議就會進入恢復模式並選舉產生新的 Leader,當 leader 服務器選舉出來後,並且集羣中有過半的機器和該 leader 節點完成數據同步後(同步指的是數據同步,用來保證集羣中過半的機器能夠和 leader 服務器的數據狀態保持一致),ZAB 協議就會退出恢復模式。當集羣中已經有過半的Follower 節點完成了和 Leader 狀態同步以後,那麼整個集羣就進入了消息廣播模式。這個時候,在 Leader 節點正常工作時,啓動一臺新的服務器加入到集羣,那這個服務器會直接進入數據恢復模式,和leader 節點進行數據同步。同步完成後即可正常對外提供非事務請求的處理。

消息廣播的實現原理

      如果大家瞭解分佈式事務的 2pc 和 3pc 協議的話(不瞭解也沒關係,我們後面會講),消息廣播的過程實際上是一個
簡化版本的二階段提交過程
       1. leader 接收到消息請求後,將消息賦予一個全局唯一的64 位自增 id,叫:zxid,通過 zxid 的大小比較既可以實
現因果有序這個特徵
       2. leader 爲每個 follower 準備了一個 FIFO 隊列(通過 TCP協議來實現,以實現了全局有序這一個特點)將帶有 zxid
的消息作爲一個提案(proposal)分發給所有的 follower
       3. 當 follower 接收到 proposal,先把 proposal 寫到磁盤,寫入成功以後再向 leader 回覆一個 ack
      4. 當 leader 接收到合法數量(超過半數節點)的 ACK 後,leader 就會向這些 follower 發送 commit 命令,同時會在本地執行該消息
      5. 當 follower 收到消息的 commit 命令以後,會提交該消息

       leader 的投票過程,不需要 Observer 的 ack,也就是Observer 不需要參與投票過程,但是 Observer 必須要同步 Leader 的數據從而在處理請求的時候保證數據的一致性

崩潰恢復(數據恢復)

       ZAB 協議的這個基於原子廣播協議的消息廣播過程,在正常情況下是沒有任何問題的,但是一旦 Leader 節點崩潰,或者由於網絡問題導致 Leader 服務器失去了過半的Follower 節點的聯繫(leader 失去與過半 follower 節點聯繫,可能是 leader 節點和 follower 節點之間產生了網絡分區,那麼此時的 leader 不再是合法的 leader 了),那麼就會進入到崩潰恢復模式。在 ZAB 協議中,爲了保證程序的正確運行,整個恢復過程結束後需要選舉出一個新的Leader爲了使 leader 掛了後系統能正常工作,需要解決以下兩個問題
      1. 已經被處理的消息不能丟失當 leader 收到合法數量 follower 的 ACKs 後,就向各個 follower 廣播 COMMIT 命令,同時也會在本地執行 COMMIT 並向連接的客戶端返回「成功」。但是如果在各個 follower 在收到 COMMIT 命令前 leader就掛了,導致剩下的服務器並沒有執行都這條消息。

leader 對事務消息發起 commit 操作,但是該消息在follower1 上執行了,但是 follower2 還沒有收到 commit,就已經掛了,而實際上客戶端已經收到該事務消息處理成功的回執了。所以在 zab 協議下需要保證所有機器都要執行這個事務消息

      2. 被丟棄的消息不能再次出現 當 leader 接收到消息請求生成 proposal 後就掛了,其他 follower 並沒有收到此 proposal,因此經過恢復模式重新選了 leader 後,這條消息是被跳過的。 此時,之前掛了的 leader 重新啓動並註冊成了 follower,他保留了
被跳過消息的 proposal 狀態,與整個系統的狀態是不一致的,需要將其刪除。
ZAB 協議需要滿足上面兩種情況,就必須要設計一個leader 選舉算法:能夠確保已經被 leader 提交的事務Proposal能夠提交、同時丟棄已經被跳過的事務Proposal。
      針對這個要求
       如果 leader 選舉算法能夠保證新選舉出來的 Leader 服務器擁有集羣中所有機器最高編號(ZXID 最大)的事務Proposal,那麼就可以保證這個新選舉出來的 Leader 一定具有已經提交的提案。因爲所有提案被 COMMIT 之前必須有超過半數的 follower ACK,即必須有超過半數節點的服務器的事務日誌上有該提案的 proposal,因此,只要有合法數量的節點正常工作,就必然有一個節點保存了所有被 COMMIT 消息的 proposal 狀態另外一個,zxid 是 64 位,高 32 位是 epoch 編號,每經過一次 Leader 選舉產生一個新的 leader,新的 leader 會將epoch 號+1,低 32 位是消息計數器,每接收到一條消息這個值+1,新 leader 選舉後這個值重置爲 0.這樣設計的好處在於老的leader掛了以後重啓,它不會被選舉爲leader,因此此時它的 zxid 肯定小於當前新的 leader。當老的leader 作爲 follower 接入新的 leader 後,新的 leader 會讓它將所有的擁有舊的 epoch 號的未被 COMMIT 的
proposal 清除

       關於 ZXID
       zxid,也就是事務 id,爲了保證事務的順序一致性,zookeeper 採用了遞增的事務 id 號(zxid)來標識事務。所有的提議(proposal)都在被提出的時候加上了 zxid。實現中 zxid 是一個 64 位的數字,它高 32 位是 epoch(ZAB 協議通過 epoch 編號來區分 Leader 週期變化的策略)用來標識 leader 關係是否改變,每次一個 leader 被選出來,它都會有一個新的epoch=(原來的 epoch+1),標識當前屬於那個 leader 的統治時期。低 32 位用於遞增計數。
       epoch:可以理解爲當前集羣所處的年代或者週期,每個leader 就像皇帝,都有自己的年號,所以每次改朝換代,leader 變更之後,都會在前一個年代的基礎上加 1。這樣就算舊的 leader 崩潰恢復之後,也沒有人聽他的了,因爲follower 只聽從當前年代的 leader 的命令。

epoch 的變化大家可以做一個簡單的實驗,
      1. 啓動一個 zookeeper 集羣。
      2. 在/tmp/zookeeper/VERSION-2 路徑下會看到一個currentEpoch 文件。文件中顯示的是當前的 epoch
      3. 把 leader 節點停機,這個時候在看 currentEpoch 會有變化。 隨着每次選舉新的 leader,epoch 都會發生變化

六、leader 選舉

      Leader 選舉會分兩個過程
      啓動的時候的 leader 選舉、 leader 崩潰的時候的的選舉

服務器啓動時的 leader 選舉

      每個節點啓動的時候狀態都是 LOOKING,處於觀望狀態,接下來就開始進行選主流程

      進行 Leader 選舉,至少需要兩臺機器,我們選取 3 臺機器組成的服務器集羣爲例。在集羣初始化階段,當有一臺服務器 Server1 啓動時,它本身是無法進行和完成 Leader 選舉,當第二臺服務器 Server2 啓動時,這個時候兩臺機器可以相互通信,每臺機器都試圖找到 Leader,於是進入 Leader 選舉過程。選舉過程如下
      (1) 每個 Server 發出一個投票。由於是初始情況,Server1和 Server2 都會將自己作爲 Leader 服務器來進行投票,每次投票會包含所推舉的服務器的myid和ZXID、epoch,使用(myid, ZXID,epoch)來表示,此時 Server1的投票爲(1, 0),Server2 的投票爲(2, 0),然後各自將這個投票發給集羣中其他機器。
      (2) 接受來自各個服務器的投票。集羣的每個服務器收到投票後,首先判斷該投票的有效性,如檢查是否是本輪投票(epoch)、是否來自LOOKING狀態的服務器。
      (3) 處理投票。針對每一個投票,服務器都需要將別人的投票和自己的投票進行 PK,PK 規則如下
            i. 優先檢查 ZXID。ZXID 比較大的服務器優先作爲Leader
            ii. 如果 ZXID 相同,那麼就比較 myid。myid 較大的服務器作爲 Leader 服務器。對於 Server1 而言,它的投票是(1, 0),接收 Server2的投票爲(2, 0),首先會比較兩者的 ZXID,均爲 0,再比較 myid,此時 Server2 的 myid 最大,於是更新自己的投票爲(2, 0),然後重新投票,對於Server2而言,它不需要更新自己的投票,只是再次向集羣中所有機器發出上一次投票信息即可。
       (4) 統計投票。每次投票後,服務器都會統計投票信息,判斷是否已經有過半機器接受到相同的投票信息,對於 Server1、Server2 而言,都統計出集羣中已經有兩臺機器接受了(2, 0)的投票信息,此時便認爲已經選出了 Leader。
       (5) 改變服務器狀態。一旦確定了 Leader,每個服務器就會更新自己的狀態,如果是 Follower,那麼就變更FOLLOWING,如果是 Leader,就變更爲 LEADING。

運行過程中的 leader 選舉

       當集羣中的 leader 服務器出現宕機或者不可用的情況時,那麼整個集羣將無法對外提供服務,而是進入新一輪的Leader 選舉,服務器運行期間的 Leader 選舉和啓動時期的 Leader 選舉基本過程是一致的。

       (1) 變更狀態。Leader 掛後,餘下的非 Observer 服務器都會將自己的服務器狀態變更爲 LOOKING,然後開始進入 Leader 選舉過程。
       (2) 每個 Server 會發出一個投票。在運行期間,每個服務器上的 ZXID 可能不同,此時假定 Server1 的 ZXID 爲123,Server3的ZXID爲122;在第一輪投票中,Server1和 Server3 都會投自己,產生投票(1, 123),(3, 122),然後各自將投票發送給集羣中所有機器。接收來自各個服務器的投票。與啓動時過程相同。
       (3) 處理投票。與啓動時過程相同,此時,Server1 將會成爲 Leader。
       (4) 統計投票。與啓動時過程相同。
       (5) 改變服務器的狀態。與啓動時過程相同

七、Zookeeper文件系統

每個子目錄項如 NameService 都被稱作爲znode,和文件系統一樣,我們能夠自由的增加、刪除znode,在一個znode下增加、刪除子znode,唯一的不同在於znode是可以存儲數據的。 

有四種類型的znode: 

1、PERSISTENT-持久化目錄節點 

客戶端與zookeeper斷開連接後,該節點依舊存在 

2、PERSISTENT_SEQUENTIAL-持久化順序編號目錄節點 

客戶端與zookeeper斷開連接後,該節點依舊存在,只是Zookeeper給該節點名稱進行順序編號 

3、EPHEMERAL-臨時目錄節點 

客戶端與zookeeper斷開連接後,該節點被刪除 

4、EPHEMERAL_SEQUENTIAL-臨時順序編號目錄節點 

客戶端與zookeeper斷開連接後,該節點被刪除,只是Zookeeper給該節點名稱進行順序編號 

八、Zookeeper監聽事件

  • zookeeper客戶端與zookeeper server連接的狀態
連接狀態 狀態含義
KeeperState.Expired 客戶端和服務器在ticktime的時間週期內,是要發送心跳通知的。這是租約協議的一個實現。客戶端發送request,告訴服務器其上一個租約時間,服務器收到這個請求後,告訴客戶端其下一個租約時間是哪個時間點。當客戶端時間戳達到最後一個租約時間,而沒有收到服務器發來的任何新租約時間,即認爲自己下線(此後客戶端會廢棄這次連接,並試圖重新建立連接)。這個過期狀態就是Expired狀態
KeeperState.Disconnected 就像上面那個狀態所述,當客戶端斷開一個連接(可能是租約期滿,也可能是客戶端主動斷開)這是客戶端和服務器的連接就是Disconnected狀態
KeeperState.SyncConnected 一旦客戶端和服務器的某一個節點建立連接(注意,雖然集羣有多個節點,但是客戶端一次連接到一個節點就行了),並完成一次version、zxid的同步,這時的客戶端和服務器的連接狀態就是SyncConnected
KeeperState.AuthFailed zookeeper客戶端進行連接認證失敗時,發生該狀態

需要說明的是,這些狀態在觸發時,所記錄的事件類型都是:EventType.None

  • zookeeper中的事件。當zookeeper客戶端監聽某個znode節點”/node-x”時:
zookeeper事件 事件含義
EventType.NodeCreated 當node-x這個節點被創建時,該事件被觸發
EventType.NodeChildrenChanged 當node-x這個節點的直接子節點被創建、被刪除、子節點數據發生變更時,該事件被觸發。
EventType.NodeDataChanged 當node-x這個節點的數據發生變更時,該事件被觸發
EventType.NodeDeleted 當node-x這個節點被刪除時,該事件被觸發。
EventType.None 當zookeeper客戶端的連接狀態發生變更時,即KeeperState.Expired、KeeperState.Disconnected、KeeperState.SyncConnected、KeeperState.AuthFailed狀態切換時,描述的事件類型爲EventType.None
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章