關(guān)于ZooKeeper的會(huì)話機(jī)制Session解讀
一、為什么會(huì)有會(huì)話機(jī)制Session

首先我們看下ZooKeeper的架構(gòu)圖,client跟ZooKeeper集群中的某一臺(tái)server保持連接,發(fā)送讀/寫請(qǐng)求,讀請(qǐng)求直接由當(dāng)前連接的server處理,寫請(qǐng)求由于是事務(wù)請(qǐng)求,由當(dāng)前server轉(zhuǎn)發(fā)給leader進(jìn)行處理。同時(shí),client還能接收來(lái)自server端的watcher通知。
而所有的這些交互,都是基于client和ZooKeeper的server之間的TCP長(zhǎng)連接,也稱之為Session會(huì)話。
ZooKeeper對(duì)外的服務(wù)端口默認(rèn)是2181,客戶端啟動(dòng)時(shí),首先會(huì)與服務(wù)器建立一個(gè)TCP連接,從第一次連接建立開始,客戶端會(huì)話的生命周期也開始了,通過(guò)這個(gè)連接,客戶端能夠通過(guò)心跳檢測(cè)和服務(wù)器保持有效的會(huì)話,也能夠向ZooKeeper服務(wù)器發(fā)送請(qǐng)求并接受響應(yīng),同時(shí)還能通過(guò)該連接接收來(lái)自服務(wù)器的Watch事件通知。
Session的SessionTimeout值用來(lái)設(shè)置一個(gè)客戶端會(huì)話的超時(shí)時(shí)間。當(dāng)由于服務(wù)器壓力太大、網(wǎng)絡(luò)故障或是客戶端主動(dòng)斷開連接等各種原因?qū)е驴蛻舳诉B接斷開時(shí),只要在SessionTimeout規(guī)定的時(shí)間內(nèi)能夠重新連接上集群中任意一臺(tái)服務(wù)器,那么之前創(chuàng)建的會(huì)話仍然有效。
說(shuō)點(diǎn)題外話,長(zhǎng)連接、短連接、數(shù)據(jù)庫(kù)連接池:
短連接 :連接->傳輸數(shù)據(jù)->關(guān)閉連接
也可以這樣說(shuō):短連接是指SOCKET連接后發(fā)送后接收完數(shù)據(jù)后馬上斷開連接。
長(zhǎng)連接:連接->傳輸數(shù)據(jù)->保持連接 -> 傳輸數(shù)據(jù)-> 。。。 ->關(guān)閉連接。
長(zhǎng)連接指建立SOCKET連接后不管是否使用都保持連接,但安全性較差。
網(wǎng)絡(luò)中不同節(jié)點(diǎn)使用TCP協(xié)議通過(guò)SOCKET進(jìn)行通信,首先需要3次握手建立連接,數(shù)據(jù)傳輸,4次握手?jǐn)嚅_連接,因此如果頻繁的創(chuàng)建、關(guān)閉,是很耗費(fèi)系統(tǒng)資源的,就像短連接那樣;使用長(zhǎng)連接貌似彌補(bǔ)了短連接的缺點(diǎn),但是,如果并發(fā)量過(guò)大,會(huì)有大量的長(zhǎng)連接,同樣會(huì)耗費(fèi)大量系統(tǒng)資源,因此具體選用長(zhǎng)連接還是短連接,是要根據(jù)具體的場(chǎng)景來(lái)選擇。
ZooKeeper中一個(gè)client只會(huì)跟一個(gè)server進(jìn)行交互(除非與當(dāng)前server連接失敗,會(huì)切換到下個(gè)server),不管這種交互有多頻繁,只需要一個(gè)TCP長(zhǎng)連接就足以應(yīng)對(duì),因選擇一個(gè)TCP長(zhǎng)連接,不失為一種最好的方案。
數(shù)據(jù)庫(kù)連接池:我們?cè)谑褂肑DBC進(jìn)行數(shù)據(jù)庫(kù)連接的時(shí)候,其實(shí)是建立了一個(gè)數(shù)據(jù)庫(kù)連接池,它本身是一種短連接+長(zhǎng)連接的方案,我們通過(guò)JDBC的3個(gè)關(guān)鍵配置來(lái)說(shuō)明下:
| 參數(shù)名稱 | 參數(shù)說(shuō)明 | 默認(rèn)值 | 備注 |
|---|---|---|---|
| minPoolSize | 連接池中保留的最小連接數(shù) | 5 | 長(zhǎng)連接 |
| maxPoolSize | 連接池中保留的最大連接數(shù) | 15 | 短連接 |
| maxIdleTime | 最大空閑時(shí)間,如果超出空閑時(shí)間未使用,連接被收回 |
超過(guò)最小連接數(shù)后創(chuàng)建的連接,在最大空閑時(shí)間后如果未使用,是會(huì)被回收的,因此可以被理解為短連接。但是保留的最小連接數(shù),即使未被使用也會(huì)一直存在,等待被使用,因此可以理解為長(zhǎng)連接。
好了,扯了這么遠(yuǎn),我們還是回到ZooKeeper是如何通過(guò)TCP長(zhǎng)連接來(lái)管理它的Session會(huì)話的吧。
二、會(huì)話(Session)如何管理
2.1)SessionID的初始化
首先了解3個(gè)基本概念:
sessionID:會(huì)話ID,用來(lái)唯一標(biāo)識(shí)一個(gè)會(huì)話,每次客戶端創(chuàng)建會(huì)話的時(shí)候,ZooKeeper都會(huì)為其分配一個(gè)全局唯一的sessionIDTimeOut:會(huì)話超時(shí)時(shí)間,如果客戶端與服務(wù)器之間因?yàn)榫W(wǎng)絡(luò)閃斷導(dǎo)致斷開連接,并在TimeOut時(shí)間內(nèi)未連上其他server,則此次會(huì)話失效,此次會(huì)話創(chuàng)建的臨時(shí)節(jié)點(diǎn)將被清理ExpirationTime:下次會(huì)話超時(shí)時(shí)間點(diǎn)。ZooKeeper會(huì)為每個(gè)會(huì)話標(biāo)記一個(gè)下次會(huì)話超時(shí)時(shí)間點(diǎn),便于對(duì)會(huì)話進(jìn)行“分桶管理”,同時(shí)也是為了搞笑低耗的實(shí)現(xiàn)會(huì)話的超時(shí)檢查與清理。其值接近于當(dāng)前時(shí)間+TimeOut,但不完全相等,稍后會(huì)介紹。
在每次client向server發(fā)起“會(huì)話創(chuàng)建”請(qǐng)求時(shí),服務(wù)端都會(huì)為其分配一個(gè)sessionID,現(xiàn)在看下sessionID是如何生成的。
在SessionTrackerImpl初始化的時(shí)候,會(huì)調(diào)用initializeNextSession來(lái)生成一個(gè)初始化的sessionID,之后在該sessionID的基礎(chǔ)上為每個(gè)會(huì)話進(jìn)行分配,其初始化算法如下:
//是ZooKeeper服務(wù)器的會(huì)話管理器,負(fù)責(zé)會(huì)話的創(chuàng)建、管理和清理等工作
public class SessionTrackerImpl extends Thread implements SessionTracker {
{...}
//參數(shù)id為當(dāng)前服務(wù)器的myid
public static long initializeNextSession(long id) {
long nextSid = 0;
//此處采用無(wú)符號(hào)右移,是為了防止出現(xiàn)負(fù)數(shù)的情況
nextSid = (System.currentTimeMillis() << 24) >>> 8;
nextSid = nextSid | (id <<56);
return nextSid;
}
{...}
}
該邏輯計(jì)算后得到的sessionID的前8位確定了所在的機(jī)器,后56位使用當(dāng)前時(shí)間的毫秒表示進(jìn)行隨機(jī)。
2.2)分桶策略
SessionTrackerImpl通過(guò)**“分桶策略”來(lái)進(jìn)行會(huì)話的管理,分桶的原則是將每個(gè)會(huì)話的“下次超時(shí)時(shí)間點(diǎn)”(ExpirationTime)**相同的會(huì)話放在同一區(qū)塊中進(jìn)行管理,以便于ZooKeeper對(duì)會(huì)話進(jìn)行不同區(qū)塊的隔離處理,以及同一區(qū)塊的統(tǒng)一處理,如下圖,橫坐標(biāo)是一個(gè)個(gè)的超時(shí)時(shí)間點(diǎn)ExpirationTime:

每個(gè)會(huì)話創(chuàng)建完畢后,ZooKeeper就會(huì)為其計(jì)算ExpirationTime,計(jì)算方式大體如下:
ExpirationTime = CurrentTime(當(dāng)前時(shí)間) + SessionTimeOut(會(huì)話超時(shí)時(shí)間)
但圖中標(biāo)識(shí)的ExpirationTime并不是以上公式簡(jiǎn)單的算出來(lái)的時(shí)間。因?yàn)樵赯ooKeeper的實(shí)際實(shí)現(xiàn)中,還做了一個(gè)處理。
ZooKeeper的Leader服務(wù)器在運(yùn)行期間會(huì)定時(shí)的進(jìn)行會(huì)話超時(shí)檢查,其時(shí)間間隔為ExpirationInterval(默認(rèn)值2000毫秒),每隔2000毫秒進(jìn)行一次會(huì)話超時(shí)檢查。
為了方便同時(shí)對(duì)多個(gè)會(huì)話進(jìn)行超時(shí)檢查,完整的ExpirationTime計(jì)算方式如下:
ExpirationTime_ = CurrentTime + SessionTimeOut ExpirationTime = ( ExpirationTime_/ExpirationInterval + 1 ) * ExpirationInterval
注意不要使用小學(xué)的乘法分配律把小括號(hào)給消化掉,它存在的目的就是為了保證ExpirationTime是ExpirationInterval的整數(shù)倍,那為什么要這樣做???
提高會(huì)話檢查的效率。讓創(chuàng)建時(shí)間臨近的會(huì)話,分配在一個(gè)桶中,實(shí)際生產(chǎn)環(huán)境中一個(gè)服務(wù)端會(huì)有很多客戶端會(huì)話,逐個(gè)檢查過(guò)期時(shí)間會(huì)非常耗時(shí),把它們放在一個(gè)桶中批量處理,可以大大提高效率。
比如CurrentTime為1547046000、1547046001這樣的會(huì)話就會(huì)被分配在一個(gè)桶中。
其次,Leader每隔ExpirationInterval 毫秒進(jìn)行會(huì)話的清理,而剛好 ExpirationTime 這個(gè)時(shí)間點(diǎn)是會(huì)話的失效時(shí)間點(diǎn),如果發(fā)現(xiàn)失效,直接清理掉就OK,避免了檢查時(shí)未失效,但沒過(guò)幾毫秒又失效了這種情況。
比如,ExpirationTime 是1547046000,如果在1547045998的時(shí)刻檢查,發(fā)現(xiàn)還有效,但過(guò)了2ms之后就無(wú)效了。而如果會(huì)話超時(shí)檢查和會(huì)話超時(shí)時(shí)間在同一個(gè)時(shí)間節(jié)點(diǎn)的話,就會(huì)避免這種情況。
2.3)會(huì)話激活
為了保持client會(huì)話的有效性,在ZooKeeper運(yùn)行過(guò)程中,client會(huì)在會(huì)話超時(shí)時(shí)間過(guò)期范圍內(nèi)向server發(fā)送PING請(qǐng)求來(lái)保持會(huì)話的有效性,俗稱“心跳檢測(cè)”。
同時(shí)server重新激活client對(duì)應(yīng)的會(huì)話,這段邏輯是在SessionTrackerImpl的touchSession中實(shí)現(xiàn)的。
先看下流程,再看源碼:

再看下源碼實(shí)現(xiàn):
//sessionId為發(fā)起會(huì)話激活的client的sessionId,timeout為會(huì)話超時(shí)時(shí)間
synchronized public boolean touchSession(long sessionId, int timeout) {
/*
* sessionsById的結(jié)構(gòu)為 HashMap<Long, SessionImpl>(),每個(gè)sessionid都有一個(gè)對(duì)應(yīng)的session實(shí)現(xiàn)
* 這里取出對(duì)應(yīng)的session實(shí)現(xiàn)
*/
SessionImpl s = sessionsById.get(sessionId);
// Return false, if the session doesn't exists or marked as closing
if (s == null || s.isClosing()) {
return false;
}
//計(jì)算當(dāng)前會(huì)話的下一個(gè)失效時(shí)間,可以理解為ExpirationTime_New
long expireTime = roundToInterval(System.currentTimeMillis() + timeout);
//tickTime是上一次計(jì)算的超時(shí)時(shí)間,可以理解為ExpirationTime_Old
if (s.tickTime >= expireTime) {
// Nothing needs to be done
return true;
}
//將ExpirationTime_Old對(duì)應(yīng)的桶中的會(huì)話取出,SessionSet 是SessionImpl的集合
SessionSet set = sessionSets.get(s.tickTime);
if (set != null) {
//將舊桶中的會(huì)話移除
set.sessions.remove(s);
}
//更新當(dāng)前會(huì)話的下一次超時(shí)時(shí)間
s.tickTime = expireTime;
//從新桶中取出該會(huì)話,無(wú)則創(chuàng)建,有則更新
set = sessionSets.get(s.tickTime);
if (set == null) {
set = new SessionSet();
sessionSets.put(expireTime, set);
}
set.sessions.add(s);
return true;
}
好了,我們了解了是會(huì)話是如何激活的,那在什么時(shí)候會(huì)發(fā)起激活呢,也就是touchSession這個(gè)方法什么時(shí)候被觸發(fā)呢?
分以下兩種情況:
- 只要client向server發(fā)送請(qǐng)求,包括讀或?qū)懻?qǐng)求,就會(huì)觸發(fā)一次激活;
- 如果client發(fā)現(xiàn)在sessionTimeOut / 3 時(shí)間內(nèi)未尚和server進(jìn)行任何通信,就會(huì)主動(dòng)發(fā)起一次PING請(qǐng)求,進(jìn)而觸發(fā)激活;
關(guān)于會(huì)話激活,可以舉個(gè)非常腦洞的例子:就像你跟房東租房,進(jìn)行續(xù)簽一樣。合同是一年一年的續(xù)簽,這是理論情況下,但是中間免不了要跟房東打交道,比如洗衣機(jī)壞了,問問房東如何處理,這一問,糟了,從問的這一天開始,重新簽一年的合同吧(當(dāng)然是把之前的租金結(jié)算一下);另外一種就是 租期一年 / 3 = 每個(gè)季度,主動(dòng)的跟房東續(xù)簽下合同(當(dāng)然也是把之前的租金結(jié)算一下)……
租房傷不起啊,個(gè)稅申報(bào)抵消房租,房東還不愿意[此處一個(gè)欲哭無(wú)淚的表情]
三、過(guò)期會(huì)話(Session)如何清理
一言蔽之吧,會(huì)話過(guò)期后,集群中所有server都刪除由該會(huì)話創(chuàng)建的臨時(shí)節(jié)點(diǎn)(EPHEMERAL)信息
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
spring boot+redis 監(jiān)聽過(guò)期Key的操作方法
這篇文章主要介紹了spring boot+redis 監(jiān)聽過(guò)期Key,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08
Spring注解驅(qū)動(dòng)開發(fā)實(shí)現(xiàn)屬性賦值
這篇文章主要介紹了Spring注解驅(qū)動(dòng)開發(fā)實(shí)現(xiàn)屬性賦值,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04
基于eclipse.ini內(nèi)存設(shè)置的問題詳解
本篇文章是對(duì)eclipse.ini內(nèi)存設(shè)置的問題進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05
Java原生服務(wù)器接收上傳文件 不使用MultipartFile類
這篇文章主要為大家詳細(xì)介紹了Java原生服務(wù)器接收上傳文件,不使用MultipartFile類,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-09-09
Spring Boot分段處理List集合多線程批量插入數(shù)據(jù)的解決方案
大數(shù)據(jù)量的List集合,需要把List集合中的數(shù)據(jù)批量插入數(shù)據(jù)庫(kù)中,本文給大家介紹Spring Boot分段處理List集合多線程批量插入數(shù)據(jù)的解決方案,感興趣的朋友跟隨小編一起看看吧2024-04-04
Kafka消費(fèi)客戶端協(xié)調(diào)器GroupCoordinator詳解
這篇文章主要為大家介紹了Kafka消費(fèi)客戶端協(xié)調(diào)器GroupCoordinator使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10

