高可用與Zookeeper設計原理

本文已整理致我的 github地址,歡迎大家 star 支持一下

前言

上文中我們瞭解到, canal 可以通過訂閱 binlog 日誌來提供增量數據訂閱和消費,通過這種方式可以實現數據庫的實時備份,實時索引構建等

我們再來詳細看看它的工作原理

如圖示,每個 server 會啓動多個實例(instance),每個實例會訂閱不同表的 binlog,實例主要負責將 binlog 日誌解析成程序易讀的結構化數據(解析後包含變更記錄的主鍵,字段變更後的值等),Canal Client 拿到結構化數據後再將其同步給其他 DB, MQ 等,同一時間某張表的 binlog 只能被一個 Instance 訂閱處理,這是爲了保障 binlog 處理的順序性

現在問題來了,如果這臺 Canal Server 掛掉了該怎麼辦,換句話說如何實現 server 的 HA(High Availability,即高可用)呢。

最容易想到的當然是準備幾個備份的 Canal Server(以下簡稱備機),這樣的話當主 Canal Server(工作中的 Server,以下簡稱主機)宕機了,備機就可以頂上去工作了。那麼這裏就涉及到兩個問題

  1. 如果有三臺機器(一臺充當主機,兩臺充當備機),啓動後到底哪個 Canal Server 爲主機?
  2. 如果主機掛了,備機如何及時發現並接手主機的工作呢

顯然這兩個問題都涉及到多進程通信,所以最好引入一箇中間層來處理,我們需要設計一個分佈式協調系統來完成這兩個需求,先給這個系統取個名字吧,姑且將其稱爲 Zookeeper,簡稱 ZK。

接下來我們來看看 ZK 需要如何設計才能滿足我們上述的兩個需求

設計分佈式鎖來解決主備角色問題

先來看問題一

如果有三臺或更多機器啓動的話,到底哪個 Canal Server 爲主機?

顯然分佈式鎖是可以滿足這個需求的,三臺機器啓動後主動去獲取這個分佈式鎖,獲取成功的則充當主機,獲取失敗的則作爲備機。

所以 ZK 必須要具有分佈式鎖的功能,可能有人說我可以引入 Redis,MySQL,因爲這兩者都具有分佈式鎖的功能,但這樣相當於在一個系統裏又引入了額外的組件,系統的複雜度上升了,而且也要保證新引入組件的高可用,不太可取,所以這次我們打算另闢蹊徑直接在 ZK 中設計這樣的分佈式鎖,首先要爲 ZK 設計一個數據結構。

我們用類似 Linux 文件系統的樹狀結構來作爲此分佈式系統的數據結構。

根節點爲 /,每個節點下的子節點名稱是唯一的(也就意味着根節點下的所有節點名稱唯一),這樣的話三個 Canal Server 啓動後,會首先嚐試着在根節點下去創建同樣名字的節點(假設爲 /lock1),那麼由於這個節點名稱是唯一的,只能創建一個 /lock1 這樣的節點,其他的再申請創建會失敗,於是給我們一個思路:誰先創建成功,誰就爲主節點,其餘的爲備機,這樣的話主備問題就解決了。

如何讓備機發現主機宕機了

再來看第二個問題

備機如何知道主機宕機?

前文提到主機會在 ZK 中創建一個節點 /lock1,創建成功即爲主機,在創建節點之前,主機首先要通過 TCP 與分佈式協調系統建立連接,這個連接會長期保活,主機會定期發送心跳來讓 ZK 感知到它的存在,這樣 ZK 就會知道主機還存活着,如果在指定的時間內(比如 2s )ZK 沒有收到主機發來的心跳,就會認爲主機宕機了,此時就會發通知給備機了。

這個通知該怎麼發呢,ZK 不僅是爲 Canal 服務的,還有很多其他的服務可能也需要在 ZK 上創建節點,不同的服務對應創建的節點都是不一樣的,比如 canal 服務創建的節點是 /lock1,庫存服務創建的節點爲 /lock2,總不可能 canal 服務的主機宕機了,卻要通知庫存服務的備機吧。我們應該只通知當時創建 /lock1 失敗的那些機器。

於是我們需要建立一個/lock1 與對應機器的映射列表,如下

每個創建 /lock1 節點失敗的備機都被放到 key 爲 /lock1 對應的列表裏

我們把這個流程稱爲註冊,這樣的話每個節點對應的機器就可以查到了,如果創建 /lock1 節點的主機宕機了,ZK 發現後只要通過映射列表通知/lock1對應的備用機器列表就行啦(同時 ZK 要把 /lock1 這個節點給刪掉,代表分佈式鎖釋放了。)我們把這種備用機器能感知到節點被刪除的機制稱爲 watch 機制,它的運行機制如下

1、 註冊: client 創建節點 /lock1 失敗後會監聽此結點,一旦此節點消失會收到 ZK 的通知
2、 存儲:將此 watcher 保存在客戶端的 watcherManager 中,我們可以簡單地認爲 watcher 的格式如下

注:爲了簡化講解流程,我們給 watcher 加了一個 path 字段,實際 watcher 並沒有,不過原理其實是一樣的

path 即我們監聽的結點,如本例中的 /lock1,keepState 即事件狀態,比如連接成功,連接斷開等,EventType 即節點對應的事件,如創建,刪除,子節點變更等事件。

3、通知:ZK 將節點及其刪除/創建事件通知到 client,client 通過這些信息在 watchManager 中找到相應的 watcher,就可以做相應的處理,比如監聽到節點被刪了,就可以感知到主機宕機了,它就可以嘗試着去創建 /lock1 了,哪個創建成功則哪個備機就成爲主機了。

通過以上的 watch 工作機制不難設計出我們的高可用方案,如下:

  1. 假設現在有 A,B 兩臺 Canal Server,爲了成爲主節點,它們首先向 zk 申請創建 /lock1節點
  2. A 創建成功後即成爲主節點開始工作, B 再去創建節點則失敗,會作爲備機,但同時 B 會使用 watch 機制來監聽 /lock1 節點(即將 B 機器註冊到 ZK 中 /lock1 對應的機器列表中,/local1被刪除後會通過此機器列表,B 就能感知到 A 宕機了)
  3. 一旦 A 不可用,則 ZK 會刪除 /lock1 節點,同時也會通知 B,B 收到通知後,通過本地的 watchManager 找到此 watcher,得知是 /lock1 這個節點被刪除事件後會再次嘗試創建/lock1 節點,創建成功後則啓動作爲主節點工作,此刻 Canal Client 也會注意到 /lock1 創建了(Canal Client 啓動後也用 watch 機制監聽 /lock1 節點的創建事件),lock1 節點可以存儲 Canal Server 地址的,這樣的話通知 Canal Client 時可以把 B 地址傳過來,Canal Client 就會與此機器建立連接了。

驚羣效應與解決方案

按照上述的設計方案其實已經能滿足 Canal 的高可用設計了,不過我們目前設計的 ZK 系統實現的分佈式鎖有兩個問題

  1. 假設有幾十臺備機,當主機宕機後,這幾十臺備機都會嘗試着去創建 /lock1 這個節點,但只有一臺機器創建成功併成爲主機,另外幾十臺機器在創建節點失敗後則會馬上處於等待狀態,這就是我們所說的驚羣效應(也叫羊羣效應),不難發現驚羣效應會造成資源的極大浪費,那麼宕機後能否只通知一個備用節點響應,這樣其他備用節點就不用羣驚羣乍了,能極大地節省資源。
  2. 當前的分佈式鎖是非公平鎖,這樣會造成飢餓現象,可能一些備機永遠沒有機會獲取這個鎖了,如何讓它成爲公平鎖,讓每個備用節點都有機會獲取這個鎖以讓它們都有機會成爲主機呢。

解決方案如下:

每個機器都會在 /lock1 下創建一個子節點,子節點的編號會按申請順序遞增,編號最小的那個節點表示其對應的機器持有了分佈式鎖,其餘機器只會監聽比它小一級的那個節點,這樣當某個節點宕機了,只會通知這一個監聽機器,避免了驚羣效應

如圖示,工作機制如下:

  1. 一開始,多臺機器都在 /lock1 節點下創建以 sub-xxx 依序遞增的節點,假設現在有一臺機器創建了一個 sub-000001,則由於它的序號最小,表示它佔有的分佈式鎖,其他機器則會依次創建 sub-000002sub-000003這樣依序遞增的節點。
  2. 每個節點對應的機器只會監聽(watch)比它小一號的節點
  3. 這樣的話,如果分佈式鎖釋放了,則只會通知比它大一號的節點,如 sub-000001 節點對應的主機宕機了,只會通知機器 B(此時 ZK 也會把 sub-000001 這個臨時節點給刪了)這樣 B 持有的節點序號最小,它也就持有了分佈式鎖。

通過這種方式我們可以也注意到機器獲取鎖的順序與其創建的節點順序保持了一致,也就實現了公平鎖。

ZK 簡介

以上就是利用 ZK 來實現分佈式鎖的一個簡單案例,當然分佈式鎖只是它衆多功能中的一個而已,作爲一個分佈式協調服務,它還有配置管理名字服務分佈式同步以及集羣管理等功能。先來簡單看一下 ZK 的一些概念。

節點

我們已經知道 ZK 採用的是一種類似 Linux 文件系統的樹形結構,樹上的每個節點我們把它稱爲 Znode,Znode 節點分爲以下四大類

  1. 臨時節點:前文我們提到主機宕機後 /lock1 就會被刪除,這種主機與 ZK 斷開鏈接就被刪除的節點我們稱其爲臨時節點
  2. 臨時順序節點: 前文我們爲了實現公平鎖而創建的按申請次序順序遞增的節點
  3. 永久節點:客戶端與 ZK 的連接斷開後,節點還在,甚至 ZK 重啓後節點也還在
  4. 永久順序節點:在永久節點的基礎上加上了按申請順序遞增的功能的節點

每個 Znode 節點都是可以存儲數據的,默認是 1M,這樣的話可以作爲配置管理中心存儲一些比較重要的數據

watcher 監聽事件

前文我們提到了備份機器可以通過 watcher 機制來監聽節點是否存在,從而可以及時響應這些事件作出處理,除了監聽節點是否存在外,ZK 還提供了以下事件

public enum EventType {
     None (-1), // 客戶端連接狀態發生變化的時候 會受到none事件
     NodeCreated (1), // 節點創建事件
     NodeDeleted (2), // 節點刪除事件
     NodeDataChanged (3), // 節點數據變化
     NodeChildrenChanged (4); // 子節點被創建,刪除觸發該事件
}

ZK 就是通過這些 watcher 事件來實現分佈式協調服務的,接下來我們來看看 ZK 在生產上的兩個應用

ZK應用

作爲 dubbo 註冊中心

假設現在有兩個服務,用戶服務和訂單服務,爲了高可用,訂單服務會部署多臺機器,每個訂單服務的機器都會在 ZK 中註冊,每個節點爲臨時節點,如下

調用方(用戶服務)會獲取 /orders 下的所有子節點,即所有機器列表,也會監聽 /orders 節點的 NodeChildrenChanged(子節點被創建或被刪除)事件,然後通過一定的負載均衡算法選擇一個來連接,假如其中一臺機器如 192.168.11.1 宕機了,則其對應的臨時節點會被刪除,同時用戶服務會能收到此 watch 事件,於是會重新獲取 /orders 的子節點,此時由於宕機對應的子節點被刪除了,所以只會獲取 192.168.11.2 和 192.168.11.3 這兩個子節點,這樣就避免了連接 192.168.11.1 這個不可用的機器了

作爲配置中心

在生產環境上 ,我們經常需要配置一些變動頻繁,需要實時生效的數據,比如某個功能上線,我們需要針對某些用戶做灰度,這個灰度規模是逐漸擴大的,我們就需要配置一下這個百分比來讓每個機器實時生效,這時就可以讓 ZK 作爲配置中心,讓每臺線上的機器監聽配置節點的 NodeDataChanged(節點數據變化) 事件,這樣只要這個節點數據變化了,其他機器可以立即收到通知更新,讓此修改立即生效,我司用的 360 開源的分佈式配置管理系統 QConf 就是基於 ZK 開發的。

總結

通過本文相信大家不難理解 ZK 的工作機制,主要要理解它的樹狀結構,節點及 watch 工作機制,掌握了這些就能理解 ZK 作爲配置中心,分佈式鎖,域名服務的原理,當然如果要更深入地瞭解 ZK,光掌握這些還不夠,比如 ZK 如果只有一臺,那會有單點故障,就要配置 ZK 集羣,既然是集羣,那如何保證數據一致呢,你需要去了解 ZAB 協議,選舉機制等,建議大家看看《ZooKeeper分佈式過程協同技術詳解》這本書,會讓你對 ZK 有更深入的理解。

歡迎大家歡迎我的公號「碼海」,共同進步。

參考:

  1. https://ningg.top/zookeeper-lesson-8-zookeeper-impl-master-and-slave/
  2. https://www.jianshu.com/p/3a218d63cffb
  3. https://www.cnblogs.com/kevingrace/p/12433503.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章