文章很長,而且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 博客園版 為您奉上珍貴的學習資源 :
免費贈送 :《尼恩Java面試寶典》 持續更新+ 史上最全 + 面試必備 2000頁+ 面試必備 + 大廠必備 +漲薪必備
免費贈送 經典圖書:《Java高並發核心編程(卷1)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高並發核心編程(卷2)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高並發核心編程(卷3)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《尼恩Java面試寶典 最新版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取
什么是Zookeeper的會話機制
那對於ZK的服務端來說,如何維護管理這些會話,就是本文要聊的內容啦~
我們在服務器啟動Zookeeper的時候能得知,ZK服務端對外默認端口是2181。而客戶端連接到服務端上,其本質其實就是一個TCP連接(長連接) ,當連接正式建立起來的時候,就開起來該次會話的生命周期了。有了會話之后,后續的請求發送,回應,心跳檢測等機制都是基於會話來實現的。
為什么會有會話機制Session

首先我們看下ZooKeeper的架構圖,client跟ZooKeeper集群中的某一台server保持連接,發送讀/寫請求,讀請求直接由當前連接的server處理,寫請求由於是事務請求,由當前server轉發給leader進行處理。同時,client還能接收來自server端的watcher通知。
而所有的這些交互,都是基於client和ZooKeeper的server之間的TCP長連接,也稱之為Session會話。ZooKeeper對外的服務端口默認是2181,客戶端啟動時,首先會與服務器建立一個TCP連接,從第一次連接建立開始,客戶端會話的生命周期也開始了,通過這個連接,客戶端能夠通過心跳檢測和服務器保持有效的會話,也能夠向ZooKeeper服務器發送請求並接受響應,同時還能通過該連接接收來自服務器的Watch事件通知。Session的SessionTimeout值用來設置一個客戶端會話的超時時間。當由於服務器壓力太大、網絡故障或是客戶端主動斷開連接等各種原因導致客戶端連接斷開時,只要在SessionTimeout規定的時間內能夠重新連接上集群中任意一台服務器,那么之前創建的會話仍然有效。
說點題外話,長連接、短連接、數據庫連接池:
短連接 :連接->傳輸數據->關閉連接
也可以這樣說:短連接是指SOCKET連接后發送后接收完數據后馬上斷開連接。
長連接:連接->傳輸數據->保持連接 -> 傳輸數據-> 。。。 ->關閉連接。
長連接指建立SOCKET連接后不管是否使用都保持連接,但安全性較差。
網絡中不同節點使用TCP協議通過SOCKET進行通信,首先需要3次握手建立連接,數據傳輸,4次握手斷開連接,因此如果頻繁的創建、關閉,是很耗費系統資源的,就像短連接那樣;使用長連接貌似彌補了短連接的缺點,但是,如果並發量過大,會有大量的長連接,同樣會耗費大量系統資源,因此具體選用長連接還是短連接,是要根據具體的場景來選擇。
ZooKeeper中一個client只會跟一個server進行交互(除非與當前server連接失敗,會切換到下個server),不管這種交互有多頻繁,只需要一個TCP長連接就足以應對,因選擇一個TCP長連接,不失為一種最好的方案。
數據庫連接池:我們在使用JDBC進行數據庫連接的時候,其實是建立了一個數據庫連接池,它本身是一種短連接+長連接的方案,我們通過JDBC的3個關鍵配置來說明下:
| 參數名稱 | 參數說明 | 默認值 | 備注 |
|---|---|---|---|
| minPoolSize | 連接池中保留的最小連接數 | 5 | 長連接 |
| maxPoolSize | 連接池中保留的最大連接數 | 15 | 短連接 |
| maxIdleTime | 最大空閑時間,如果超出空閑時間未使用,連接被收回 |
超過最小連接數后創建的連接,在最大空閑時間后如果未使用,是會被回收的,因此可以被理解為短連接。但是保留的最小連接數,即使未被使用也會一直存在,等待被使用,因此可以理解為長連接。
好了,扯了這么遠,我們還是回到ZooKeeper是如何通過TCP長連接來管理它的Session會話的吧。
Session相關的基本概念
當連接建立的時候,Session就已經建立起來,與這個過程相關的有三個重要的值:
- SessionID:會話的唯一標識,由ZK來分配
- TimeOut:會話超時時間。在客戶端與服務端連接的期間,如果因為某些原因斷開了連接(如網絡中斷等等),該次會話以及其相關的臨時節點不會被馬上刪除,而是等待TimeOut耗盡之后,若客戶端沒有重連上來,那本次會話才會失效,相關的一些臨時節點也會被刪除
- Expiration Time:TimeOut是一個相對時間,而Expiration Time則是在時間軸上的一個絕對過期時間。
順便貼一下SessionId生成的源碼,SessionId的生成和兩個東西相關聯,一個是時間戳,一個是機器id
/**其中id是機器id**/
public static long initializeNextSession(long id) {
long nextSid = 0;
nextSid = (System.currentTimeMillis() << 24) >>> 8;
nextSid = nextSid | (id <<56);
return nextSid;
}
分桶機制
Session是由ZK服務端來進行管理的,一個服務端可以為多個客戶端服務,也就是說,有多個Session,那這些Session是怎么樣被管理的呢?而分桶機制可以說就是其管理的一個手段。ZK服務端會維護着一個個"桶",然后把Session們分配到一個個的桶里面。而這個區分的維度,就是ExpirationTime

為什么要如此區分呢?因為ZK的服務端會在運行期間定時地對會話進行超時檢測,如果不對Session進行維護的話,那在檢測的時候豈不是要遍歷所有的Session?這顯然不是一個好辦法,所以才以超時時間為維度來存放Session,這樣在檢測的時候,只需要掃描對應的桶就可以了
那這樣的話,新的問題就來了:每個Session的超時時間是一個很分散的值,假設有1000個Session,很可能就會有1000個不同的超時時間,進而有1000個桶,這樣有啥意義嗎?
可以看到,最終得到的ExpirationTime是ExpirationInterval的倍數,而ExpirationInterval就是ZK服務端定時檢查過期Session的頻率,默認為2000毫秒。所以說,每個Session的ExpirationTime最后都是一個近似值,是ExpirationInterval的倍數,這樣的話,ZK在進行掃描的時候,只需要掃描一個桶即可。
另外讓過期時間是ExpirationInterval的倍數還有一個好處就是,讓檢查時間和每個Session的過期時間在一個時間節點上。否則的話就會出現一個問題:ZK檢查完畢的1毫秒后,就有一個Session新過期了,這種情況肯定是不好。
Session激活(續約)
在客戶端與服務端完成連接之后生成過期時間,這個值並不是一直不變的,而是會隨着客戶端與服務端的交互來更新。過期時間的更新,當然就伴隨着Session在桶上的遷移
為了保持client會話的有效性,在ZooKeeper運行過程中,client會在會話超時時間過期范圍內向server發送PING請求來保持會話的有效性,俗稱“心跳檢測”。同時server重新激活client對應的會話,這段邏輯是在SessionTrackerImpl的touchSession中實現的。先看下流程,再看源碼:

再看下源碼實現:
//sessionId為發起會話激活的client的sessionId,timeout為會話超時時間
synchronized public boolean touchSession(long sessionId, int timeout) {
/*
* sessionsById的結構為 HashMap<Long, SessionImpl>(),每個sessionid都有一個對應的session實現
* 這里取出對應的session實現
*/
SessionImpl s = sessionsById.get(sessionId);
// Return false, if the session doesn't exists or marked as closing
if (s == null || s.isClosing()) {
return false;
}
//計算當前會話的下一個失效時間,可以理解為ExpirationTime_New
long expireTime = roundToInterval(System.currentTimeMillis() + timeout);
//tickTime是上一次計算的超時時間,可以理解為ExpirationTime_Old
if (s.tickTime >= expireTime) {
// Nothing needs to be done
return true;
}
//將ExpirationTime_Old對應的桶中的會話取出,SessionSet 是SessionImpl的集合
SessionSet set = sessionSets.get(s.tickTime);
if (set != null) {
//將舊桶中的會話移除
set.sessions.remove(s);
}
//更新當前會話的下一次超時時間
s.tickTime = expireTime;
//從新桶中取出該會話,無則創建,有則更新
set = sessionSets.get(s.tickTime);
if (set == null) {
set = new SessionSet();
sessionSets.put(expireTime, set);
}
set.sessions.add(s);
return true;
}
最簡單的一點,客戶端每向服務端發送請求,包括讀請求和寫請求,都會觸發一次激活,因為這預示着客戶端處於活躍狀態
而如果客戶端一直沒有讀寫請求,那么它在TimeOut的三分之一時間內沒有發送過請求的話,那么客戶端會發送一次PING,來觸發Session的激活。當然,如果客戶端直接斷開連接的話,那么TimeOut結束后就會被服務端掃描到然后進行清楚了
參考文獻
https://blog.csdn.net/MuErHuoXu/article/details/86218115
https://blog.csdn.net/SCUTJAY/article/details/106889060
