本文作者:HelloGitHub-老荀
Hi,這裡是 HelloGitHub 推出的 HelloZooKeeper 系列,免費開源、有趣、入門級的 ZooKeeper 教程,面向有程式設計基礎的新手。
項目位址:https://github.com/HelloGitHub-Team/HelloZooKeeper
前一篇文章我們介紹了 Follower 或 Observer 是如何同 Leader 同步資料的,以及 ACL 的介紹、使用和原理。這章我們将正式學習有關 session 的内容,具體用戶端怎麼同服務端保持心跳?服務端不同節點之間是如何保持心跳?
會話,即 session,這個詞語或者說概念很多地方都有用到,在 ZK 中會話指的是兩個不同的機器建立了網絡連接配接後,就可以說他們之間建立了一個會話。 ZK 的會話是有逾時的概念的,當會話逾時後,會由服務端主動關閉,當然用戶端也可以主動請求服務端想要關閉會話。你可能會問,為什麼要搞這個麻煩,直接兩邊連上一直用不就好了嗎?有了會話這個概念就是為了防止,在建立連接配接後,有些用戶端不常使用,早點關閉連接配接可以節省資源。
我發現我好久沒有 cue 雞太美了,這次就讓他再 C 位出道一次吧。
我們的雞太美每天起床後,日常發微網誌、直播、跳舞、打籃球,很多事務都需要去辦事處辦理。
是以第一件事情就是去辦事處找馬果果(現在就假設馬果果一個辦事處)申請使用辦事處(建立連接配接,建立會話)

而馬果果會為雞太美建立一個 ID,就是會話 ID,這個 ID (我這裡假設是 19980802) 和雞太美會進行綁定,而雞太美在申請的同時還需要告訴馬果果自己最長的逾時時間是多久,我這裡假設是 6000 毫秒。
而馬果果這邊會記錄下來:
在馬果果開張的時候自己本身也有一個會話的檢查間隔,就是配置在 <code>zoo.cfg</code> 中的 <code>tickTime</code> 選項,我這裡假設是 3000 毫秒。馬果果在開張的時候會計算出一個時間軸,這個時間軸的間隔是固定的,并且不會改變。
然後馬果果會通過雞太美的 6000 以及目前的時間戳結合時間軸,計算出一個雞太美會話逾時時間點
然後會記錄下來:
記錄完,就算雞太美會話建立成功了。
而馬果果這邊會遵循這個時間軸的節點定期對會話進行檢查,假設現在的時間進行到雞太美的時間點了
馬果果會把在這個時間點的會話全部取出(記得我們上面說過,可以是多個嗎?)
然後會根據 ID 資訊找到對應的村民,一個個通知他們會話關閉了。
你可能會問現在因為雞太美逾時時間是 6000,而馬果果逾時檢查是 3000,正好是整數倍,如果逾時時間不是整數倍呢?要不說我們的馬果果同志好學上進呢,他早就想到啦,是以設計了一個算法,無論村民的逾時時間是怎麼樣,都會向下取整找到馬果果設定的檢查點。
假設雞太美的逾時間是 5900
再比如雞太美的逾時時間是 6500
是以看到了吧,以馬果果的 3000 為例,隻要小于 3000 的都按照 0 來算,小于 6000 的按照 3000 來算,小于 9000 的按照 6000 來算,以此類推,是以隻要馬果果自己的檢查時間間隔确定了之後,無論是哪個村民設定了什麼樣的逾時時間都能被向下取整至最近的統一檢查點。這樣馬果果檢查的時候就不會有太大的負擔,可以統一對村民的逾時時間進行檢查。
但是這麼做一定會造成用戶端的逾時時間是有誤差的(通常是比設定的要短一點),減少這個誤差的方式就是減小馬果果的檢查間隔,也就是 <code>tickTime</code> 參數(預設是 2000,已經夠用了我覺得)。
而馬果果的會話管理不會隻有雞太美一個人,我們來看看有多個村民的會話管理頁長什麼樣吧
可以看到使用了三個哈希表去記錄這些映射關系,畫到時間軸是這樣的
是以當時間進行到 25317000 的時候,對應三個村民就逾時了,25320000 時另外兩個村民就逾時了。
這裡我還得說下其實會話 ID 在馬果果這邊辦事處開張後就會根據目前時間戳和 myid 初始化出一個基數,舉個例子可能是 987434245 類似這種數字,之後每一個村民過來配置設定會話 ID 的時候,隻是對這個數字不停的加 1,是以不會出現亂七八糟無序的數字,圖中的數字舉例僅僅是我個人的玩梗癖好,和實際情況不符~
但是這樣的話,雞太美豈不是每次 6000 毫秒就逾時了嗎?這當然不可能,因為村民的每一次任意的操作(增删改查)都會重新整理該逾時時間戳,具體怎麼做的呢?我們一起來看下,假設紅色箭頭是會話剛建立時馬果果替雞太美計算出來的逾時時間,假設在綠色箭頭時間戳的地方,雞太美執行了任意操作。
馬果果會根據目前時間戳(綠色箭頭處)加上雞太美之前設定的逾時時間(6000),重新計算出新的逾時時間:
然後對會話管理頁的資料進行修改,我仍然以多個村民的例子講解
更新前:
更新後:
這個更新的過程可以被稱為會話激活。
猿話一下,除了用戶端每次的正常操作會重新整理逾時時間以外,用戶端仍然需要一個機制去保持住這個會話,這個機制就是我們平時聽到過的心跳檢測,原理是每次用戶端啟動的時候也會設定一個心跳檢測的間隔時間,在背景一直會去判斷最後一次發送的時間戳和目前時間是否超過了該心跳檢測的間隔,如果超過了就會發送一個名為 PING 的請求,由于剛剛我們說了用戶端的任意操作都會重新整理該逾時時間,PING 也不例外,有了這個心跳機制就可以讓用戶端保持住和服務端的會話狀态。而服務端收到 PING,除了重新整理逾時時間會簡單的回複一個 PING 給用戶端,而用戶端收到服務端的 PING 會直接丢棄不需要任何其他操作。
我們以 Java 用戶端為例
假設逾時時間設定 12000 毫秒,那麼用戶端的心跳間隔就是 4000 毫秒,計算過程如下
是以隻要用戶端空閑時間超過 4000 毫秒,就會發送一個 PING 給服務端,如果用戶端的逾時時間設定的非常大的話,比如半小時,那每隔 10 秒也會強制發送一個 PING(這個 10 秒是 Java 用戶端寫死的邏輯)。
用戶端和服務端之間的會話先講到這裡,接下來我們聊聊服務端之間的會話。
如果村裡是同時有多個辦事處的時候(我這裡先假設兩個),情況就不太一樣了。
假設雞太美第一次連接配接的時候找到的作為 Follower 的馬小雲:
而 Follower 是不能獨自處理非讀請求的,是以此次馬小雲會為雞太美配置設定好 ID 之後,将建立會話操作轉發給馬果果,這樣就好像是雞太美找到馬果果一樣,流程和上面是一樣的,在會話管理頁中記錄下來。
而馬小雲自己也會簡單的維護一個會話 ID 和逾時時間的映射關系,以多個村民為例,每次收到請求都會對其進行記錄
現在雞太美是連接配接的馬小雲辦事處(包括每次心跳發送),但是全局的會話管理資料在馬果果這裡,這樣是怎麼維持住會話狀态的呢?
這裡我們就得先聊聊服務端之間是怎麼進行心跳的。
服務端有一個重要的配置 <code>tickTime</code>(預設是 2000),還有另一個重要的配置 <code>syncLimit</code>(預設是 5),我就以這兩個預設值來舉例:
首先 Leader 會以 1000 (<code>tickTime / 2</code>) 毫秒的頻率去對各個 Follower 發起 PING 的請求
每次檢查 Follower 傳回的 PING 的逾時時間是否超過 10000 (<code>tickTime * syncLimit</code>),超過這個時間沒有收到該 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 響應後會對發送過來的會話資料進行激活
我們現在已經知道了會話的概念,就可以聊聊臨時節點了。
我們先來看下臨時節點的建立代碼
這次的建立操作和其他的持久節點建立并無差別,需要在小紅本上寫下記錄,而這個記錄中有一個字段是 <code>ephemeralOwner</code> 當節點是持久節點這個字段值是 0,但當節點是臨時節點時這個字段記錄的就是持有該節點的會話 ID。
除了在小紅本上建立記錄以外,由于是臨時節點,還需要額外在一個專門的地方也記錄一下,假設還是雞太美建立了 3 個臨時節點:
在雞太美會話逾時的時候,可能是會話真逾時了(由于有心跳機制,是以這個可能性其實不大),也可能是雞太美主動關閉的會話。
馬果果就會從這個記錄臨時節點的地方根據雞太美的會話 ID 取出對應的臨時節點的路徑,然後根據路徑删除即可,效果和雞太美主動删除是一樣的,這樣就達到了,當用戶端關閉之後,對應的臨時節點會自動清除的特點。這個臨時節點的特性就會被用在 ZK 實作分布式鎖的時候,防止了用戶端因意外退出沒法執行釋放鎖的邏輯!
還有一個東西我一直就沒提過,就是 ZK 的協定。
衆所周知,ZK 是一個 CS 架構的應用,有用戶端和服務端之分,那既然這樣就免不了需要進行網絡通信,而且不光是用戶端和服務端之間,服務端和服務端之間也需要通信,有了網絡通信就離不開協定,但是協定既是最重要的東西,也是最不重要的東西。
最重要是因為,ZK 本身就是基于該協定去通信的,無論是用戶端還是服務端之間,我之前提到的各種暗号,如:REQUEST、ACK、COMMIT、PING 等。都屬于協定中的一個字段,用來區分不同的消息。協定構成了整個 ZK 通信的基礎,能夠通信了才能完成整個元件的功能。
最不重要是因為,除非你想開發 ZK 的用戶端,主動去請求 ZK 服務端,不然即使你完全不知道協定的具體格式,也不會影響你了解整個 ZK 的原理,而且協定的介紹非常的枯燥和無用,容易勸退。
是以我把這個概念留到了最後才提起,并且我也不打算去講解 ZK 中不同請求的協定具體長什麼樣。這次我就換一個角度簡單的介紹下協定。
首先,我介紹的 ZK 都是 Java 程式,無論用戶端還是服務端,是以協定的本質是規定如何把 Java 對象轉成位元組流,友善在網絡中傳輸,以及拿到位元組流的那一方,如何再把這個位元組流轉換回 Java 對象,這其實就是序列化和反序列化的過程。而為了友善序列化,ZK 中定義的各種對象,如 XxxRequest 、 XxxResponse、XxxPacket 等,它們的字段類型通常就幾種:<code>int</code>、<code>long</code>、<code>String</code>、<code>byte[]</code>、<code>List</code>、<code>boolean</code> 以及其他嵌套的類型。
對于這三種類型來說最簡單,直接用輸出流寫即可,差別就是一個是 4 位元組,一個是 8 位元組,一個是 1 位元組
這兩種是類似,如果字段為空,則就寫入一個 -1,不為空就先寫一個 <code>int</code> 表示長度,之後緊跟 <code>byte[]</code> 表示具體資料即可
碰到 <code>List</code> 和 4.2 是一樣,如果為空就寫 -1,不為空就先寫 <code>List</code> 長度,之後周遊 <code>List</code> 根據泛型(也隻可能是上面這幾種)決定如何繼續寫入,嵌套對象的話就把這個寫入操作委托給它就行了,因為它的字段也隻可能是上面這幾種。
ZK 的序列化協定采用的緊湊書寫的方式,根據不同的字段類型依次寫入最終的位元組流即可。
今天我們介紹了 ZK 會話相關的知識:會話是什麼,用戶端和服務端的會話如何保持,服務端和服務端的會話如何保持,以及介紹了臨時節點是如何利用會話機制在會話結束後被自動删除的,最後再用很短的篇幅帶大家了解了下 ZK 的協定,不知不覺已經寫了九篇了,我決定這一篇是本系列中最後一篇講解原理的,之後的文章不講原理介紹下 ZK 中的一些隐藏功能,還有整理下重要的資料,如配置資訊,面試大全,目标是打造收藏向的三篇重磅文章。期待一下吧~
老規矩,如果你有任何對文章中的疑問也可以是建議或者是對 ZK 原理部分的疑問,歡迎來倉庫中提 issue 給我們,或者來語雀話題讨論。
位址:https://www.yuque.com/kaixin1002/yla8hz
作者:削微寒 ·
本作品采用署名-非商業性使用-禁止演繹 4.0 國際 進行許可。