免費開源三維工具 Blender:譴責但不制裁俄羅斯,捍衛藝術家和開發者的自由
1 基礎
ZooKeeper 基礎知識基本分為三大模組:
- 資料模型
- ACL 許可權控制
- Watch 監控
資料模型是最重要的,很多 ZooKeeper 中典型的應用場景都是利用這些基礎模組實現的。比如我們可以利用資料模型中的臨時節點和 Watch 監控機制來實現一個釋出訂閱的功能。
1.1 資料模型
資料模型就是 ZooKeeper 用來儲存和處理資料的一種邏輯結構。ZooKeeper 資料模型最根本的功能就像一個數據庫。
ZooKeeper 中的資料模型是一種樹形結構,非常像電腦中的檔案系統,有一個根資料夾,下面還有很多子資料夾。ZooKeeper 的資料模型也具有一個固定的根節點(/),我們可以在根節點下建立子節點,並在子節點下繼續建立下一級節點。ZooKeeper 樹中的每一層級用斜槓(/)分隔開,且只能用絕對路徑(如“get /work/task1”)的方式查詢 ZooKeeper 節點,而不能使用相對路徑。
ZooKeeper 中的資料是由多個數據節點最終構成的一個層級的樹狀結構,和我們在建立 MySOL 資料表時會定義不同型別的資料列欄位,ZooKeeper 中的資料節點也分為持久節點、臨時節點和有序節點三種類型:
1、持久節點
我們第一個介紹的是持久節點,這種節點也是在 ZooKeeper 最為常用的,幾乎所有業務場景中都會包含持久節點的建立。之所以叫作持久節點是因為一旦將節點建立為持久節點,該資料節點會一直儲存在 ZooKeeper 伺服器上,即使建立該節點的客戶端與服務端的會話關閉了,該節點依然不會被刪除。如果我們想刪除持久節點,就要顯式呼叫 delete 函式進行刪除操作。
2、臨時節點
所謂臨時性是指,如果將節點建立為臨時節點,那麼該節點資料不會一直儲存在 ZooKeeper 伺服器上。當建立該臨時節點的客戶端會話因超時或發生異常而關閉時,該節點也相應在 ZooKeeper 伺服器上被刪除。同樣,我們可以像刪除持久節點一樣主動刪除臨時節點。
在平時的開發中,可以利用臨時節點的這一特性來做伺服器叢集內機器執行情況的統計,將叢集設定為“/servers”節點,併為叢集下的每臺伺服器建立一個臨時節點“/servers/host”,當伺服器下線時該節點自動被刪除,最後統計臨時節點個數就可以知道叢集中的執行情況。
3、有序節點
有序節點並不算是一種單獨種類的節點,而是在之前提到的持久節點和臨時節點特性的基礎上,增加了一個節點有序的性質。所謂節點有序是說在我們建立有序節點的時候,ZooKeeper 伺服器會自動使用一個單調遞增的數字作為字尾,追加到我們建立節點的後邊。例如一個客戶端建立了一個路徑為 works/task- 的有序節點,那麼 ZooKeeper 將會生成一個序號並追加到該節點的路徑後,最後該節點的路徑為 works/task-1。通過這種方式我們可以直觀的檢視到節點的建立順序。
上述這幾種資料節點雖然型別不同,但 ZooKeeper 中的每個節點都維護有這些內容:一個二進位制陣列(byte data[]),用來儲存節點的資料、ACL 訪問控制資訊、子節點資料(因為臨時節點不允許有子節點,所以其子節點欄位為 null),除此之外每個資料節點還有一個記錄自身狀態資訊的欄位 stat。
1.1.1 使用 ZooKeeper 實現鎖
悲觀鎖
悲觀鎖認為程序對臨界區的競爭總是會出現,為了保證程序在操作資料時,該條資料不被其他程序修改。資料會一直處於被鎖定的狀態。
我們假設一個具有 n 個程序的應用,同時訪問臨界區資源,我們通過程序建立 ZooKeeper 節點 /locks 的方式獲取鎖。執行緒 a 通過成功建立 ZooKeeper 節點“/locks”的方式獲取鎖後繼續執行,這時程序 b 也要訪問臨界區資源,於是程序 b 也嘗試建立“/locks”節點來獲取鎖,因為之前程序 a 已經建立該節點,所以程序 b 建立節點失敗無法獲得鎖。
問題:當程序 a 因為異常中斷導致 /locks 節點始終存在,其他執行緒因為無法再次建立節點而無法獲取鎖,這就產生了一個死鎖問題。針對這種情況我們可以通過將節點設定為臨時節點的方式避免。並通過在伺服器端新增監聽事件來通知其他程序重新獲取鎖。
樂觀鎖
程序對臨界區資源的競爭不會總是出現,所以相對悲觀鎖而言。加鎖方式沒有那麼激烈,不會全程的鎖定資源,而是在資料進行提交更新的時候,對資料的衝突與否進行檢測,如果發現衝突了,則拒絕操作。
樂觀鎖基本可以分為讀取、校驗、寫入三個步驟。CAS(Compare-And-Swap),即比較並替換,就是一個樂觀鎖的實現。CAS 有 3 個運算元,記憶體值 V,舊的預期值 A,要修改的新值 B。當且僅當預期值 A 和記憶體值 V 相同時,將記憶體值 V 修改為 B,否則什麼都不做。
在 ZooKeeper 中的 version 屬性就是用來實現樂觀鎖機制中的“校驗”的,ZooKeeper 每個節點都有資料版本的概念,在呼叫更新操作的時候,假如有一個客戶端試圖進行更新操作,它會攜帶上次獲取到的 version 值進行更新。而如果在這段時間內,ZooKeeper 伺服器上該節點的數值恰好已經被其他客戶端更新了,那麼其資料版本一定也會發生變化,因此肯定與客戶端攜帶的 version 無法匹配,便無法成功更新,因此可以有效地避免一些分散式更新的併發問題。
在 ZooKeeper 的底層實現中,當服務端處理 setDataRequest 請求時,首先會呼叫 checkAndIncVersion 方法進行資料版本校驗。ZooKeeper 會從 setDataRequest 請求中獲取當前請求的版本 version,同時通過 getRecordForPath 方法獲取伺服器資料記錄 nodeRecord, 從中得到當前伺服器上的版本資訊 currentversion。如果 version 為 -1,表示該請求操作不使用樂觀鎖,可以忽略版本對比;如果 version 不是 -1,那麼就對比 version 和 currentversion,如果相等,則進行更新操作,否則就會丟擲 BadVersionException 異常中斷操作。
1.2 Watch 監控
ZooKeeper 的客戶端也可以通過 Watch 機制來訂閱當伺服器上某一節點的資料或狀態發生變化時收到相應的通知,我們可以通過向 ZooKeeper 客戶端的構造方法中傳遞 Watcher 引數的方式實現:
new ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)
上面程式碼的意思是定義了一個了 ZooKeeper 客戶端物件例項,並傳入三個引數:
connectString 服務端地址
sessionTimeout:超時時間
Watcher:監控事件
這個 Watcher 將作為整個 ZooKeeper 會話期間的上下文 ,一直被儲存在客戶端 ZKWatchManager 的 defaultWatcher 中。
除此之外,ZooKeeper 客戶端也可以通過 getData、exists 和 getChildren 三個介面來向 ZooKeeper 伺服器註冊 Watcher,從而方便地在不同的情況下新增 Watch 事件:
getData(String path, Watcher watcher, Stat stat)
一個物件或者資料節點可能會被多個客戶端監控,當對應事件被觸發時,會通知這些物件或客戶端。我們可以將 Watch 機制理解為是分散式環境下的觀察者模式。
通常我們在實現觀察者模式時,最核心或者說關鍵的程式碼就是建立一個列表來存放觀察者。
而在 ZooKeeper 中則是在客戶端和伺服器端分別實現兩個存放觀察者列表,即:ZKWatchManager 和 WatchManager。其核心操作就是圍繞著這兩個展開的。
客戶端 Watch 註冊實現過程
我們先看一下客戶端的實現過程,在傳送一個 Watch 監控事件的會話請求時,ZooKeeper 客戶端主要做了兩個工作:
- 標記該會話是一個帶有 Watch 事件的請求
- 將 Watch 事件儲存到 ZKWatchManager
服務端 Watch 註冊實現過程
Zookeeper 服務端處理 Watch 事件基本有 2 個過程:
- 解析收到的請求是否帶有 Watch 註冊事件
- 將對應的 Watch 事件儲存到 WatchManager
ZooKeeper 實現的方式是通過客服端和服務端分別建立有觀察者的資訊列表。客戶端呼叫 getData、exist 等介面時,首先將對應的 Watch 事件放到本地的 ZKWatchManager 中進行管理。服務端在接收到客戶端的請求後根據請求型別判斷是否含有 Watch 事件,並將對應事件放到 WatchManager 中進行管理。
在事件觸發的時候服務端通過節點的路徑資訊查詢相應的 Watch 事件通知給客戶端,客戶端在接收到通知後,首先查詢本地的 ZKWatchManager 獲得對應的 Watch 資訊處理回撥操作。這種設計不但實現了一個分散式環境下的觀察者模式,而且通過將客戶端和服務端各自處理 Watch 事件所需要的額外資訊分別儲存在兩端,減少彼此通訊的內容。大大提升了服務的處理效能。
應用
在系統開發的過程中會用到各種各樣的配置資訊,如資料庫配置項、第三方介面、服務地址等,這些配置操作在我們開發過程中很容易完成,但是放到一個大規模的叢集中配置起來就比較麻煩了。通常這種叢集中,我們可以用配置管理功能自動完成伺服器配置資訊的維護,利用ZooKeeper 的釋出訂閱功能就能解決這個問題。
我們可以把諸如資料庫配置項這樣的資訊儲存在 ZooKeeper 資料節點中。伺服器叢集客戶端對該節點新增 Watch 事件監控,當叢集中的服務啟動時,會讀取該節點資料獲取資料配置資訊。而當該節點資料發生變化時,ZooKeeper 伺服器會發送 Watch 事件給各個客戶端,叢集中的客戶端在接收到該通知後,重新讀取節點的資料庫配置資訊。
使用 Watch 機制實現了一個分散式環境下的配置管理功能,通過對 ZooKeeper 伺服器節點新增資料變更事件,實現當資料庫配置項資訊變更後,叢集中的各個客戶端能接收到該變更事件的通知,並獲取最新的配置資訊。要注意一點是,我們提到 Watch 具有一次性,所以當我們獲得伺服器通知後要再次新增 Watch 事件。
1.3 ACL許可權控制
如何使用 ZooKeeper 的 ACL 機制來實現客戶端對資料節點的訪問控制?
一個 ACL 許可權設定通常可以分為 3 部分,分別是:許可權模式(Scheme)、授權物件(ID)、許可權資訊(Permission)。最終組成一條例如“scheme:id:permission”格式的 ACL 請求資訊。
許可權模式(Scheme)
許可權模式就是用來設定 ZooKeeper 伺服器進行許可權驗證的方式。ZooKeeper 的許可權驗證方式大體分為兩種型別,一種是範圍驗證,另外一種是口令驗證。所謂的範圍驗證就是說 ZooKeeper 可以針對一個 IP 或者一段 IP 地址授予某種許可權。比如我們可以讓一個 IP 地址為“ip:192.168.0.1”的機器對伺服器上的某個資料節點具有寫入的許可權。
另一種許可權模式就是口令驗證,也可以理解為使用者名稱密碼的方式,這是我們最熟悉也是日常生活中經常使用的模式。
在 ZooKeeper 中這種驗證方式是 Digest 認證,我們知道通過網路傳輸相對來說並不安全,所以“絕不通過明文在網路傳送密碼”也是程式設計中很重要的原則之一,而 Digest 這種認證方式首先在客戶端傳送“username:password”這種形式的許可權表示符後,ZooKeeper 服務端會對密碼 部分使用 SHA-1 和 BASE64 演算法進行加密,以保證安全性。另一種許可權模式 Super 可以認為是一種特殊的 Digest 認證。具有 Super 許可權的客戶端可以對 ZooKeeper 上的任意資料節點進行任意操作。
授權物件(ID)
所謂的授權物件就是說我們要把許可權賦予誰,而對應於 4 種不同的許可權模式來說,如果我們選擇採用 IP 方式,使用的授權物件可以是一個 IP 地址或 IP 地址段;而如果使用 Digest 或 Super 方式,則對應於一個使用者名稱。如果是 World 模式,是授權系統中所有的使用者。
許可權資訊(Permission)
在 ZooKeeper 中已經定義好的許可權有 5 種:
- 資料節點(create)建立許可權,授予許可權的物件可以在資料節點下建立子節點;
- 資料節點(wirte)更新許可權,授予許可權的物件可以更新該資料節點;
- 資料節點(read)讀取許可權,授予許可權的物件可以讀取該節點的內容以及子節點的資訊;
- 資料節點(delete)刪除許可權,授予許可權的物件可以刪除該資料節點的子節點;
- 資料節點(admin)管理者許可權,授予許可權的物件可以對該資料節點體進行 ACL 許可權設定。
每個節點都有維護自身的 ACL 許可權資料,即使是該節點的子節點也是有自己的 ACL 許可權而不是直接繼承其父節點的許可權。
客戶端在 ACL 許可權請求傳送過程的步驟比較簡單:首先是封裝該請求的型別,之後將許可權資訊封裝到 request 中併發送給服務端。而伺服器的實現比較複雜,首先分析請求型別是否是許可權相關操作,之後根據不同的許可權模式(scheme)呼叫不同的實現類驗證許可權最後儲存許可權資訊。
2 序列化
序列化是指將我們定義好的 Java 型別轉化成資料流的形式。之所以這麼做是因為在網路傳輸過程中,TCP 協議採用“流通訊”的方式,提供了可以讀寫的位元組流。而這種設計的好處在於避免了在網路傳輸過程中經常出現的問題:比如訊息丟失、訊息重複和排序等問題。那麼什麼時候需要序列化呢?如果我們需要通過網路傳遞物件或將物件資訊進行持久化的時候,就需要將該物件進行序列化。
在 ZooKeeper 中並沒有採用和 Java 一樣的序列化方式,而是採用了一個 Jute 的序列解決方案作為 ZooKeeper 框架自身的序列化方式,說到 Jute 框架,它最早作為 Hadoop 中的序列化元件。之後 Jute 從 Hadoop 中獨立出來,成為一個獨立的序列化解決方案。ZooKeeper 從最開始就採用 Jute 作為其序列化解決方案,直到其最新的版本依然沒有更改。
ZooKeeper 一直將 Jute 框架作為序列化解決方案,但這並不意味著 Jute 相對其他框架效能更好,反倒是 Apache Avro、Thrift 等框架在效能上優於前者。之所以 ZooKeeper 一直採用 Jute 作為序列化解決方案,主要是新老版本的相容等問題,也許在之後的版本中,ZooKeeper 會選擇更加高效的序列化解決方案。
在 ZooKeeper 中進行序列化的具體實現:首先,我們定義了一個 test_jute 類,為了能夠對它進行序列化,需要該 test_jute 類實現 Record 介面,並在對應的 serialize 序列化方法和 deserialize 反序列化方法中編輯具體的實現邏輯。
class test_jute implements Record{ private long ids; private String name; ... public void serialize(OutpurArchive a_,String tag){ ... } public void deserialize(INputArchive a_,String tag){ ... } }
而在序列化方法 serialize 中,我們要實現的邏輯是,首先通過字元型別引數 tag 傳遞標記序列化識別符號,之後使用 writeLong 和 writeString 等方法分別將物件屬性欄位進行序列化。
public void serialize(OutpurArchive a_,String tag) throws ...{ a_.startRecord(this.tag); a_.writeLong(ids,"ids"); a_.writeString(type,"name"); a_.endRecord(this,tag); }
而呼叫 derseralize 在實現反序列化的過程則與我們上邊說的序列化過程正好相反。
public void deserialize(INputArchive a_,String tag) throws { a_.startRecord(tag); ids = a_.readLong("ids"); name = a_.readString("name"); a_.endRecord(tag); }
深入底層,看一下 ZooKeeper 框架具體是如何實現序列化操作的。正如上邊所提到的,通過簡單的實現 Record 介面就可以實現序列化。Record 介面可以理解為 ZooKeeper 中專門用來進行網路傳輸或本地儲存時使用的資料型別。因此所有我們實現的類要想傳輸或者儲存到本地都要實現該 Record 介面。
Record 介面的內部實現邏輯非常簡單,只是定義了一個 序列化方法 serialize 和一個反序列化方法 deserialize 。而在 Record 起到關鍵作用的則是兩個重要的類:OutputArchive 和 InputArchive ,其實這兩個類才是真正的序列化和反序列化工具類。
在 OutputArchive 中定義了可進行序列化的引數型別,根據不同的序列化方式呼叫不同的實現類進行序列化操作。如下圖所示,Jute 可以通過 Binary 、 Csv 、Xml 等方式進行序列化操作。
Binary 二進位制序列化方式的底層實現相對簡單,只是採用將對應的 Java 物件轉化成二進位制位元組流的方式。Binary 方式序列化的優點有很多:無論是 Windows 作業系統、還是 Linux 作業系統或者是蘋果的 macOS 作業系統,其底層都是對二進位制檔案進行操作,而且所有的系統對二進位制檔案的編譯與解析也是一樣的,所有作業系統都能對二進位制檔案進行操作,跨平臺的支援性更好。而缺點則是會存在不同作業系統下,產生大端小端的問題。
採用 XML 方式序列化的實現類:XmlOutPutArchive 中的底層實現過程分析,我們可以瞭解到其實現的基本原理,也就是根據 XML 格式的要求,解析傳入的序列化引數,並將引數按照 Jute 定義好的格式,採用設定好的預設標籤封裝成對應的序列化檔案。而採用 XML 方式進行序列化的優點則是,通過可擴充套件標記協議,不同平臺或作業系統對序列化和反序列化的方式都是一樣的,不存在因為平臺不同而產生的差異性,也不會出現如 Binary 二進位制序列化方法中產生的大小端的問題。而缺點則是序列化和反序列化的效能不如二進位制方式。在序列化後產生的檔案相比與二進位制方式,同樣的資訊所產生的檔案更大。
Csv,它和 XML 方式很像,只是所採用的轉化格式不同,Csv 格式採用逗號將文字進行分割,我們日常使用中最常用的 Csv 格式檔案就是 Excel 檔案。
在 Jute 框架中實現 Csv 序列化的類是 CsvOutputArchive,我們還是以 String 字元物件序列化為例,在呼叫 CsvOutputArchive 的 writeString 方法時,writeString 方法首先呼叫 printCommaUnlessFirst 方法生成一個逗號分隔符,之後將要序列化的字串值轉換成 CSV 編碼格式追加到位元組陣列中。
這 3 種方式相比,二進位制底層的實現方式最為簡單,效能也最好。而 XML 作為可擴充套件的標記語言跨平臺性更強。而 CSV 方式介於兩者之間實現起來也相比 XML 格式更加簡單。
3 ZooKeeper 伺服器的啟動管理與初始化
3.1 單機模式
在 ZooKeeper 服務的初始化之前,首先要對配置檔案等資訊進行解析和載入。也就是在真正開始服務的初始化之前需要對服務的相關引數進行準備,而 ZooKeeper 服務的準備階段大體上可分為啟動程式入口、zoo.cfg 配置檔案解析、建立歷史檔案清理器等
QuorumPeerMain 類是 ZooKeeper 服務的啟動介面,可以理解為 Java 中的 main 函式。 通常我們在控制檯啟動 ZooKeeper 服務的時候,輸入 zkServer.cm 或 zkServer.sh 命令就是用來啟動這個 Java 類的。
在 ZooKeeper 啟動過程中,首先要做的事情就是解析配置檔案 zoo.cfg。zoo.cfg 是服務端的配置檔案,在這個檔案中我們可以配置資料目錄、埠號等資訊。所以解析 zoo.cfg 配置檔案是 ZooKeeper 服務啟動的關鍵步驟。
面對大流量的網路訪問,ZooKeeper 會因此產生海量的資料,如果磁碟資料過多或者磁碟空間不足,則會導致 ZooKeeper 伺服器不能正常執行,進而影響整個分散式系統。所以面對這種問題,ZooKeeper 採用了 DatadirCleanupManager 類作為歷史檔案的清理工具類。在 3.4.0 版本後的 ZooKeeper 中更是增加了自動清理歷史資料的功能以儘量避免磁碟空間的浪費。
經過了配置檔案解析等準備階段後, ZooKeeper 開始服務的初始化階段。初始化階段可以理解為根據解析準備階段的配置資訊,例項化服務物件。服務初始化階段的主要工作是建立用於服務統計的工具類,如下圖所示主要有以下幾種:
- ServerStats 類,它可以用於服務執行資訊統計;
- FileTxnSnapLog 類,可以用於資料管理。
- 會話管理類,設定伺服器 TickTime 和會話超時時間、建立啟動會話管理器等操作。
在完成了 ZooKeeper 服務的啟動後,ZooKeeper 會初始化一個請求處理邏輯上的相關類。這個操作就是初始化請求處理鏈。所謂的請求處理鏈是一種責任鏈模式的實現方式,根據不同的客戶端請求,在 ZooKeeper 伺服器上會採用不同的處理邏輯。而為了更好地實現這種業務場景,ZooKeeper 中採用多個請求處理器類一次處理客戶端請求中的不同邏輯部分.
3.2 叢集模式
在 ZooKeeper 叢集中將伺服器分成 Leader 、Follow 、Observer 三種角色伺服器,在叢集執行期間這三種伺服器所負責的工作各不相同:
- Leader 角色伺服器負責管理叢集中其他的伺服器,是叢集中工作的分配和排程者。
- Follow 伺服器的主要工作是選舉出 Leader 伺服器,在發生 Leader 伺服器選舉的時候,系統會從 Follow 伺服器之間根據多數投票原則,選舉出一個 Follow 伺服器作為新的 Leader 伺服器。
- Observer 伺服器則主要負責處理來自客戶端的獲取資料等請求,並不參與 Leader 伺服器的選舉操作,也不會作為候選者被選舉為 Leader 伺服器。
叢集模式是如何啟動到對外提供服務的?
首先,在 ZooKeeper 服務啟動後,系統會呼叫入口 QuorumPeerMain 類中的 main 函式。在 main 函式中的 initializeAndRun 方法中根據 zoo.cfg 配置檔案,判斷服務啟動方式是叢集模式還是單機模式。在函式中首先根據 arg 引數和 config.isDistributed() 來判斷,如果配置引數中配置了相關的配置項,並且已經指定了叢集模式執行,那麼在服務啟動的時候就會跳轉到 runFromConfig 函式完成之後的叢集模式的初始化工作。
在 ZooKeeper 服務的叢集模式啟動過程中,一個最主要的核心類是 QuorumPeer 類。我們可以將每個 QuorumPeer 類的例項看作叢集中的一臺伺服器。在 ZooKeeper 叢集模式的執行中,一個 QuorumPeer 類的例項通常具有 3 種狀態,分別是參與 Leader 節點的選舉、作為 Follow 節點同步 Leader 節點的資料,以及作為 Leader 節點管理叢集中的 Follow 節點。
在一個 ZooKeeper 服務的啟動過程中,首先呼叫 runFromConfig 函式將服務執行過程中需要的核心工具類註冊到 QuorumPeer 例項中去。
public void runFromConfig(QuorumPeerConfig config){ ServerCnxnFactory cnxnFactory = null; ServerCnxnFactory secureCnxnFactory = null; ... quorumPeer = getQuorumPeer() quorumPeer.setElectionType(config.getElectionAlg()); quorumPeer.setCnxnFactory(cnxnFactory); ... }
leader伺服器啟動過程:在整個 ZooKeeper 叢集啟動過程中,首先要先選舉出叢集中的 Leader 伺服器。在 ZooKeeper 叢集選舉 Leader 節點的過程中,首先會根據伺服器自身的伺服器 ID(SID)、最新的 ZXID、和當前的伺服器 epoch (currentEpoch)這三個引數來生成一個選舉標準。之後,ZooKeeper 服務會根據 zoo.cfg 配置檔案中的引數,選擇引數檔案中規定的 Leader 選舉演算法,進行 Leader 頭節點的選舉操作。而在 ZooKeeper 中提供了三種 Leader 選舉演算法,分別是 LeaderElection 、AuthFastLeaderElection、FastLeaderElection。在我們日常開發過程中,可以通過在 zoo.cfg 配置檔案中使用 electionAlg 引數屬性來制定具體要使用的演算法型別。
follow伺服器啟動過程:在伺服器的啟動過程中,Follow 機器的主要工作就是和 Leader 節點進行資料同步和互動。當 Leader 機器啟動成功後,Follow 節點的機器會收到來自 Leader 節點的啟動通知。而該通知則是通過 LearnerCnxAcceptor 類來實現的。該類就相當於一個接收器。專門用來接收來自叢集中 Leader 節點的通知資訊。
4 服務端收到會話請求內部處理過程
當客戶端需要和 ZooKeeper 服務端進行相互協調通訊時,首先要建立該客戶端與服務端的連線會話,在會話成功建立後,ZooKeeper 服務端就可以接收來自客戶端的請求操作了。ZooKeeper 服務端在處理一次客戶端發起的會話請求時,所採用的處理過程很像是一條工廠中的流水生產線。在這條流水線上,主要參與工作的是三個“工人”,分別是 PrepRequestProcessor 、ProposalRequestProcessor 以及 FinalRequestProcessor。這三個“工人”會協同工作,最終完成一次會話的處理工作,而它的實現方式就是責任鏈模式。
作為第一個處理會話請求的“工人”,PrepRequestProcessor 類主要負責請求處理的準備工作,比如判斷請求是否是事務性相關的請求操作。在 PrepRequestProcessor 完成工作後,ProposalRequestProcessor 類承接接下來的工作,對會話請求是否執行詢問 ZooKeeper 服務中的所有伺服器之後,執行相關的會話請求操作,變更 ZooKeeper 資料庫資料。最後所有請求就會走到 FinalRequestProcessor 類中完成踢出重複會話的操作。
請求前處理器
PrepRequestProcessor 實現了 RequestProcessor 介面,並繼承了執行緒類 Thread,說明其可以通過多執行緒的方式呼叫。在 PrepRequestProcessor 類內部有一個 RequestProcessor 型別的 nextProcessor 屬性欄位,從名稱上就可以看出該屬性欄位的作用是指向下一個處理器。
PrepRequestProcessor 類的主要作用是分辨要處理的請求是否是事務性請求,比如建立節點、更新資料、刪除節點、建立會話等,這些請求操作都是事務性請求,在執行成功後會對伺服器上的資料造成影響。當 PrepRequestProcessor 類收到請求後,如果判斷出該條請求操作是事務性請求,就會針對該條請求建立請求事務頭、事務體、會話檢查、ACL 檢查和版本檢查等一系列的預處理工作。
事務處理器
ProposalRequestProcessor 是繼 PrepRequestProcessor 後,責任鏈模式上的第二個處理器。其主要作用就是對事務性的請求操作進行處理,而從 ProposalRequestProcessor 處理器的名字中就能大概猜出,其具體的工作就是“提議”。所謂的“提議”是說,當處理一個事務性請求的時候,ZooKeeper 首先會在服務端發起一次投票流程,該投票的主要作用就是通知 ZooKeeper 服務端的各個機器進行事務性的操作了,避免因為某個機器出現問題而造成事物不一致等問題。在 ProposalRequestProcessor 處理器階段,其內部又分成了三個子流程,分別是:Sync 流程、Proposal 流程、Commit 流程。
Sync 流程,該流程的底層實現類是 SyncRequestProcess 類。SyncRequestProces 類的作用就是在處理事務性請求時,ZooKeeper 服務中的每臺機器都將該條請求的操作日誌記錄下來,完成這個操作後,每一臺機器都會向 ZooKeeper 服務中的 Leader 機器傳送事物日誌記錄完成的通知。
Proposal 流程:在處理事務性請求的過程中,ZooKeeper 需要取得在叢集中過半數機器的投票,只有在這種情況下才能真正地將資料改變。而 Proposal 流程的主要工作就是投票和統計投票結果。
在完成 Proposal 流程後,ZooKeeper 伺服器上的資料不會進行任何改變,成功通過 Proposal 流程只是說明 ZooKeeper 服務可以執行事務性的請求操作了,而要真正執行具體資料變更,需要在 Commit 流程中實現,這種實現方式很像是 MySQL 等資料庫的操作方式。在 Commit 流程中,它的主要作用就是完成請求的執行。其底層實現是通過 CommitProcessor 實現的。
5 一致性
在 ZooKeeper 叢集中,Leader 伺服器主要負責處理事物性的請求,而在接收到一個客戶端的事務性請求操作時,Leader 伺服器會先向叢集中的各個機器針對該條會話發起投票詢問。ZooKeeper 中實現的一致性也不是強一致性,即叢集中各個伺服器上的資料每時每刻都是保持一致的特性。在 ZooKeeper 中,採用的是最終一致的特性,即經過一段時間後,ZooKeeper 叢集伺服器上的資料最終保持一致的特性。
要想實現 ZooKeeper 叢集中的最終一致性,我們先要確定什麼情況下會對 ZooKeeper 叢集服務產生不一致的情況。在叢集初始化啟動的時候,首先要同步叢集中各個伺服器上的資料。而在叢集中 Leader 伺服器崩潰時,需要選舉出新的 Leader 而在這一過程中會導致各個伺服器上資料的不一致,所以當選舉出新的 Leader 伺服器後需要進行資料的同步操作。
ZooKeeper 在叢集中採用的是多數原則方式,即當一個事務性的請求導致伺服器上的資料發生改變時,ZooKeeper 只要保證叢集上的多數機器的資料都正確變更了,就可以保證系統資料的一致性。 這是因為在一個 ZooKeeper 叢集中,每一個 Follower 伺服器都可以看作是 Leader 伺服器的資料副本,需要保證叢集中大多數機器資料是一致的,這樣在叢集中出現個別機器故障的時候,ZooKeeper 叢集依然能夠保證穩定執行。
在 ZooKeeper 中,重新選舉 Leader 伺服器會經歷一段時間,因此理論上在 ZooKeeper 叢集中會短暫的沒有 Leader 伺服器,在這種情況下接收到事務性請求操作的時候,ZooKeeper 服務會先將這個會話進行掛起操作,掛起的會話不會計算會話的超時時間,之後在 Leader 伺服器產生後系統會同步執行這些會話操作。
ZooKeeper 叢集在處理一致性問題的時候基本採用了兩種方式來協調叢集中的伺服器工作,分別是恢復模式和廣播模式。
-
恢復模式:當 ZooKeeper 叢集中的 Leader 伺服器崩潰後,ZooKeeper 叢集就採用恢復模式的方式進行工作,在這個工程中,ZooKeeper 叢集會首先進行 Leader 節點伺服器的重新選擇,之後在選舉出 Leader 伺服器後對系統中所有的伺服器進行資料同步進而保證叢集中伺服器上的資料的一致性。
-
廣播模式:當 ZooKeeper 叢集中具有 Leader 伺服器,並且可以正常工作時,叢集中又有新的 Follower 伺服器加入 ZooKeeper 中參與工作,這種情況常常發生在系統性能到達瓶頸,進而對系統進行動態擴容的使用場景。在這種情況下,如果不做任何操作,那麼新加入的伺服器作為 Follower 伺服器,其上的資料與 ZooKeeper 叢集中其他伺服器上的資料不一致。當有新的查詢會話請求傳送到 ZooKeeper 叢集進行處理,而恰巧該請求實際被分發給這臺新加入的 Follower 機器進行處理,就會導致明明在叢集中存在的資料,在這臺伺服器上卻查詢不到,導致資料查詢不一致的情況。因此,在當有新的 Follower 伺服器加入 ZooKeeper 叢集中的時候,該臺伺服器會在恢復模式下啟動,並找到叢集中的 Leader 節點伺服器,並同該 Leader 伺服器進行資料同步。
底層實現:
LearnerHandler 是一個多執行緒的類,在 ZooKeeper 叢集服務執行過程中,一個 Follower 或 Observer 伺服器就對應一個 LearnerHandler 。在叢集伺服器彼此協調工作的過程中,Leader 伺服器會與每一個 Learner 伺服器維持一個長連線,並啟動一個單獨的 LearnerHandler 執行緒進行處理。
在 LearnerHandler 執行緒類中,最核心的方法就是 run 方法,處理資料同步等功能都在該方法中進行呼叫。首先通過 syncFollower 函式判斷資料同步的方式是否是快照方式。如果是快照方式,就將 Leader 伺服器上的資料操作日誌 dump 出來傳送給 Follower 等伺服器,在 Follower 等伺服器接收到資料操作日誌後,在本地執行該日誌,最終完成資料的同步操作。
6 leader選舉
Leader 伺服器的選舉操作主要發生在兩種情況下。第一種就是 ZooKeeper 叢集服務啟動的時候,第二種就是在 ZooKeeper 叢集中舊的 Leader 伺服器失效時,這時 ZooKeeper 叢集需要選舉出新的 Leader 伺服器。
在 ZooKeeper 叢集服務最初啟動的時候,Leader 伺服器是如何選舉的。在 ZooKeeper 叢集啟動時,需要在叢集中的伺服器之間確定一臺 Leader 伺服器。當 ZooKeeper 叢集中的三臺伺服器啟動之後,首先會進行通訊檢查,如果叢集中的伺服器之間能夠進行通訊。叢集中的三臺機器開始嘗試尋找叢集中的 Leader 伺服器並進行資料同步等操作。如何這時沒有搜尋到 Leader 伺服器,說明叢集中不存在 Leader 伺服器。這時 ZooKeeper 叢集開始發起 Leader 伺服器選舉。在整個 ZooKeeper 叢集中 Leader 選舉主要可以分為三大步驟分別是:發起投票、接收投票、統計投票。
發起投票
在 ZooKeeper 伺服器叢集初始化啟動的時候,叢集中的每一臺伺服器都會將自己作為 Leader 伺服器進行投票。也就是每次投票時,傳送的伺服器的 myid(伺服器識別符號)和 ZXID (叢集投票資訊識別符號)等選票資訊欄位都指向本機伺服器。 而一個投票資訊就是通過這兩個欄位組成的。以叢集中三個伺服器 Serverhost1、Serverhost2、Serverhost3 為例,三個伺服器的投票內容分別是:Severhost1 的投票是(1,0)、Serverhost2 伺服器的投票是(2,0)、Serverhost3 伺服器的投票是(3,0)。
接收投票
叢集中各個伺服器在發起投票的同時,也通過網路接收來自叢集中其他伺服器的投票資訊。
在接收到網路中的投票資訊後,伺服器內部首先會判斷該條投票資訊的有效性。檢查該條投票資訊的時效性,是否是本輪最新的投票,並檢查該條投票資訊是否是處於 LOOKING 狀態的伺服器發出的。
統計投票
在接收到投票後,ZooKeeper 叢集就該處理和統計投票結果了。對於每條接收到的投票資訊,叢集中的每一臺伺服器都會將自己的投票資訊與其接收到的 ZooKeeper 叢集中的其他投票資訊進行對比。主要進行對比的內容是 ZXID,ZXID 數值比較大的投票資訊優先作為 Leader 伺服器。如果每個投票資訊中的 ZXID 相同,就會接著比對投票資訊中的 myid 資訊欄位,選舉出 myid 較大的伺服器作為 Leader 伺服器。
在 ZooKeeper 叢集服務的執行過程中,Leader 伺服器作為處理事物性請求以及管理其他角色伺服器,在 ZooKeeper 叢集中起到關鍵的作用。在前面的課程中我們提到過,當 ZooKeeper 叢集中的 Leader 伺服器發生崩潰時,叢集會暫停處理事務性的會話請求,直到 ZooKeeper 叢集中選舉出新的 Leader 伺服器。而整個 ZooKeeper 叢集在重新選舉 Leader 時也經過了四個過程,分別是變更伺服器狀態、發起投票、接收投票、統計投票。其中,與初始化啟動時 Leader 伺服器的選舉過程相比,變更狀態和發起投票這兩個階段的實現是不同的。