本文作者:HelloGitHub-老荀
Hi,這裡是 HelloGitHub 推出的 HelloZooKeeper 系列,免費開源、有趣、入門級的 ZooKeeper 教程,面向有程式設計基礎的新手。
前一篇文章我們介紹了 Follower 或 Observer 是如何同 Leader 同步資料的,以及 ACL 的介紹、使用和原理。這章我們將正式學習有關 session 的內容,具體客戶端怎麼同服務端保持心跳?服務端不同節點之間是如何保持心跳?
一、客戶端會話的祕密
會話,即 session,這個詞語或者說概念很多地方都有用到,在 ZK 中會話指的是兩個不同的機器建立了網路連線後,就可以說他們之間建立了一個會話。 ZK 的會話是有超時的概念的,當會話超時後,會由服務端主動關閉,當然客戶端也可以主動請求服務端想要關閉會話。你可能會問,為什麼要搞這個麻煩,直接兩邊連上一直用不就好了嗎?有了會話這個概念就是為了防止,在建立連線後,有些客戶端不常使用,早點關閉連線可以節省資源。
1.1 雞太美的一天
我發現我好久沒有 cue 雞太美了,這次就讓他再 C 位出道一次吧。
我們的雞太美每天起床後,日常發微博、直播、跳舞、打籃球,很多事務都需要去辦事處辦理。
所以第一件事情就是去辦事處找馬果果(現在就假設馬果果一個辦事處)申請使用辦事處(建立連線,建立會話)
而馬果果會為雞太美建立一個 ID,就是會話 ID,這個 ID (我這裡假設是 19980802) 和雞太美會進行繫結,而雞太美在申請的同時還需要告訴馬果果自己最長的超時時間是多久,我這裡假設是 6000 毫秒。
而馬果果這邊會記錄下來:
在馬果果開張的時候自己本身也有一個會話的檢查間隔,就是配置在 zoo.cfg
中的 tickTime
選項,我這裡假設是 3000 毫秒。馬果果在開張的時候會計算出一個時間軸,這個時間軸的間隔是固定的,並且不會改變。
然後馬果果會通過雞太美的 6000 以及當前的時間戳結合時間軸,計算出一個雞太美會話超時時間點
然後會記錄下來:
記錄完,就算雞太美會話建立成功了。
而馬果果這邊會遵循這個時間軸的節點定期對會話進行檢查,假設現在的時間進行到雞太美的時間點了
馬果果會把在這個時間點的會話全部取出(記得我們上面說過,可以是多個嗎?)
然後會根據 ID 資訊找到對應的村民,一個個通知他們會話關閉了。
你可能會問現在因為雞太美超時時間是 6000,而馬果果超時檢查是 3000,正好是整數倍,如果超時時間不是整數倍呢?要不說我們的馬果果同志好學上進呢,他早就想到啦,所以設計了一個演算法,無論村民的超時時間是怎麼樣,都會向下取整找到馬果果設定的檢查點。
假設雞太美的超時間是 5900
再比如雞太美的超時時間是 6500
所以看到了吧,以馬果果的 3000 為例,只要小於 3000 的都按照 0 來算,小於 6000 的按照 3000 來算,小於 9000 的按照 6000 來算,以此類推,所以只要馬果果自己的檢查時間間隔確定了之後,無論是哪個村民設定了什麼樣的超時時間都能被向下取整至最近的統一檢查點。這樣馬果果檢查的時候就不會有太大的負擔,可以統一對村民的超時時間進行檢查。
但是這麼做一定會造成客戶端的超時時間是有誤差的(通常是比設定的要短一點),減少這個誤差的方式就是減小馬果果的檢查間隔,也就是 tickTime
引數(預設是 2000,已經夠用了我覺得)。
而馬果果的會話管理不會只有雞太美一個人,我們來看看有多個村民的會話管理頁長什麼樣吧
可以看到使用了三個雜湊表去記錄這些對映關係,畫到時間軸是這樣的
所以當時間進行到 25317000 的時候,對應三個村民就超時了,25320000 時另外兩個村民就超時了。
這裡我還得說下其實會話 ID 在馬果果這邊辦事處開張後就會根據當前時間戳和 myid 初始化出一個基數,舉個例子可能是 987434245 類似這種數字,之後每一個村民過來分配會話 ID 的時候,只是對這個數字不停的加 1,所以不會出現亂七八糟無序的數字,圖中的數字舉例僅僅是我個人的玩梗癖好,和實際情況不符~
但是這樣的話,雞太美豈不是每次 6000 毫秒就超時了嗎?這當然不可能,因為村民的每一次任意的操作(增刪改查)都會重新整理該超時時間戳,具體怎麼做的呢?我們一起來看下,假設紅色箭頭是會話剛建立時馬果果替雞太美計算出來的超時時間,假設在綠色箭頭時間戳的地方,雞太美執行了任意操作。
馬果果會根據當前時間戳(綠色箭頭處)加上雞太美之前設定的超時時間(6000),重新計算出新的超時時間:
然後對會話管理頁的資料進行修改,我仍然以多個村民的例子講解
更新前:
更新後:
這個更新的過程可以被稱為會話啟用。
1.2 心跳檢測
猿話一下,除了客戶端每次的正常操作會重新整理超時時間以外,客戶端仍然需要一個機制去保持住這個會話,這個機制就是我們平時聽到過的心跳檢測,原理是每次客戶端啟動的時候也會設定一個心跳檢測的間隔時間,在後臺一直會去判斷最後一次傳送的時間戳和當前時間是否超過了該心跳檢測的間隔,如果超過了就會傳送一個名為 PING 的請求,由於剛剛我們說了客戶端的任意操作都會重新整理該超時時間,PING 也不例外,有了這個心跳機制就可以讓客戶端保持住和服務端的會話狀態。而服務端收到 PING,除了重新整理超時時間會簡單的回覆一個 PING 給客戶端,而客戶端收到服務端的 PING 會直接丟棄不需要任何其他操作。
我們以 Java 客戶端為例
ZooKeeper client = new ZooKeeper("127.0.0.1:2181", 12000, null);
假設超時時間設定 12000 毫秒,那麼客戶端的心跳間隔就是 4000 毫秒,計算過程如下
12000 * 2 / 3 / 2 = 4000 // 這個公式是程式碼中的寫死邏輯,其實就是 / 3
所以只要客戶端空閒時間超過 4000 毫秒,就會傳送一個 PING 給服務端,如果客戶端的超時時間設定的非常大的話,比如半小時,那每隔 10 秒也會強制傳送一個 PING(這個 10 秒是 Java 客戶端寫死的邏輯)。
客戶端和服務端之間的會話先講到這裡,接下來我們聊聊服務端之間的會話。
二、服務端會話的祕密
如果村裡是同時有多個辦事處的時候(我這裡先假設兩個),情況就不太一樣了。
假設雞太美第一次連線的時候找到的作為 Follower 的馬小云:
而 Follower 是不能獨自處理非讀請求的,所以此次馬小云會為雞太美分配好 ID 之後,將建立會話操作轉發給馬果果,這樣就好像是雞太美找到馬果果一樣,流程和上面是一樣的,在會話管理頁中記錄下來。
而馬小云自己也會簡單的維護一個會話 ID 和超時時間的對映關係,以多個村民為例,每次收到請求都會對其進行記錄
現在雞太美是連線的馬小云辦事處(包括每次心跳傳送),但是全域性的會話管理資料在馬果果這裡,這樣是怎麼維持住會話狀態的呢?
這裡我們就得先聊聊服務端之間是怎麼進行心跳的。
服務端有一個重要的配置 tickTime
(預設是 2000),還有另一個重要的配置 syncLimit
(預設是 5),我就以這兩個預設值來舉例:
- 首先 Leader 會以 1000 (
tickTime / 2
) 毫秒的頻率去對各個 Follower 發起 PING 的請求 - 每次檢查 Follower 返回的 PING 的超時時間是否超過 10000 (
tickTime * syncLimit
),超過這個時間沒有收到該 Follower 的 ACK 響應就關閉和該 Follower 的 socket 連線
那 Follower 收到 PING 的訊息後會回覆一個 PING 給 Leader 並且會把自己記錄的會話對映關係一起發過去
還會立即清空自己本地的對映關係!
然後 Leader 收到 Follower 的這個 PING 響應後,因為之前所有客戶端的會話管理資料其實都在 Leader 這裡,所以 Leader 可以對發過來的會話 ID 和超時時間進行會話啟用,具體方法和之前的例子中是一樣的,通過服務端之間的 PING,既可以完成服務端之間的心跳檢測,又可以對客戶端的會話進行啟用,又是一次一魚兩吃。
小結一下:
- 會話是 ZK 中的重要概念,會話的狀態會影響,服務端對客戶端請求的處理
- 客戶端的每次操作都會延長會話的超時時間,並且客戶端會主動發起 PING 請求來保持住會話,以免在空閒時會話超時被服務端關閉
- 客戶端的會話資料是儲存在 Leader 端的,Follower 只是在每次操作的時候簡單的記錄下會話 ID 和超時時間的對映關係
- 服務端之間的心跳 PING 是由 Leader 主動向 Follower 發起的
- Follower 收到 PING 後會將自己儲存的會話對映資料傳送給 Leader
- Leader 收到 Follower 的 PING 響應後會對傳送過來的會話資料進行啟用
我們現在已經知道了會話的概念,就可以聊聊臨時節點了。
三、臨時節點
我們先來看下臨時節點的建立程式碼
client.create("/HelloZooKeeper/niubi", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
這次的建立操作和其他的持久節點建立並無區別,需要在小紅本上寫下記錄,而這個記錄中有一個欄位是 ephemeralOwner
當節點是持久節點這個欄位值是 0,但當節點是臨時節點時這個欄位記錄的就是持有該節點的會話 ID。
除了在小紅本上建立記錄以外,由於是臨時節點,還需要額外在一個專門的地方也記錄一下,假設還是雞太美建立了 3 個臨時節點:
19980802 => ["/雞太美/我真美", "/雞太美/我真帥", "/雞太美/我真秀"]
在雞太美會話超時的時候,可能是會話真超時了(由於有心跳機制,所以這個可能性其實不大),也可能是雞太美主動關閉的會話。
馬果果就會從這個記錄臨時節點的地方根據雞太美的會話 ID 取出對應的臨時節點的路徑,然後根據路徑刪除即可,效果和雞太美主動刪除是一樣的,這樣就達到了,當客戶端關閉之後,對應的臨時節點會自動清除的特點。這個臨時節點的特性就會被用在 ZK 實現分散式鎖的時候,防止了客戶端因意外退出沒法執行釋放鎖的邏輯!
四、協議
還有一個東西我一直就沒提過,就是 ZK 的協議。
眾所周知,ZK 是一個 CS 架構的應用,有客戶端和服務端之分,那既然這樣就免不了需要進行網路通訊,而且不光是客戶端和服務端之間,服務端和服務端之間也需要通訊,有了網路通訊就離不開協議,但是協議既是最重要的東西,也是最不重要的東西。
- 最重要是因為,ZK 本身就是基於該協議去通訊的,無論是客戶端還是服務端之間,我之前提到的各種暗號,如:REQUEST、ACK、COMMIT、PING 等。都屬於協議中的一個欄位,用來區分不同的訊息。協議構成了整個 ZK 通訊的基礎,能夠通訊了才能完成整個元件的功能。
- 最不重要是因為,除非你想開發 ZK 的客戶端,主動去請求 ZK 服務端,不然即使你完全不知道協議的具體格式,也不會影響你理解整個 ZK 的原理,而且協議的介紹非常的枯燥和無用,容易勸退。
所以我把這個概念留到了最後才提起,並且我也不打算去講解 ZK 中不同請求的協議具體長什麼樣。這次我就換一個角度簡單的介紹下協議。
首先,我介紹的 ZK 都是 Java 程式,無論客戶端還是服務端,所以協議的本質是規定如何把 Java 物件轉成位元組流,方便在網路中傳輸,以及拿到位元組流的那一方,如何再把這個位元組流轉換回 Java 物件,這其實就是序列化和反序列化的過程。而為了方便序列化,ZK 中定義的各種物件,如 XxxRequest 、 XxxResponse、XxxPacket 等,它們的欄位型別通常就幾種:int
、long
、String
、byte[]
、List
、boolean
以及其他巢狀的型別。
4.1 int、long、boolean
對於這三種型別來說最簡單,直接用輸出流寫即可,區別就是一個是 4 位元組,一個是 8 位元組,一個是 1 位元組
4.2 String、byte[]
這兩種是類似,如果欄位為空,則就寫入一個 -1,不為空就先寫一個 int
表示長度,之後緊跟 byte[]
表示具體資料即可
4.3 巢狀型別、List
碰到 List
和 4.2 是一樣,如果為空就寫 -1,不為空就先寫 List
長度,之後遍歷 List
根據泛型(也只可能是上面這幾種)決定如何繼續寫入,巢狀物件的話就把這個寫入操作委託給它就行了,因為它的欄位也只可能是上面這幾種。
4.4 小結
ZK 的序列化協議採用的緊湊書寫的方式,根據不同的欄位型別依次寫入最終的位元組流即可。
五、總結
今天我們介紹了 ZK 會話相關的知識:會話是什麼,客戶端和服務端的會話如何保持,服務端和服務端的會話如何保持,以及介紹了臨時節點是如何利用會話機制在會話結束後被自動刪除的,最後再用很短的篇幅帶大家瞭解了下 ZK 的協議,不知不覺已經寫了九篇了,我決定這一篇是本系列中最後一篇講解原理的,之後的文章不講原理介紹下 ZK 中的一些隱藏功能,還有整理下重要的資料,如配置資訊,面試大全,目標是打造收藏向的三篇重磅文章。期待一下吧~
老規矩,如果你有任何對文章中的疑問也可以是建議或者是對 ZK 原理部分的疑問,歡迎來倉庫中提 issue 給我們,或者來語雀話題討論。