zookeeper原理篇-Zookeeper會話機制

前言

上篇文章我們學習了Zookeeper的選舉流程和FastLeaderElection選舉算法的實現過程,瞭解了Zookeeper從初始化開始到選舉的過程,本篇文章我們開始研究Zookeeper中的會話機制體系

會話

在Zookeeper中,會話是個很重要的概念之一,客戶端與服務端之間的任何交互操作都和會話息息相關,其中包含zookeeper的臨時節點的生命週期、客戶端請求執行以及Watcher通知機制等。接下來,我們從全局的會話狀態變化創建會話再到會話管理三個方面來看看Zookeeper是如何處理會話相關的操作

會話狀態

客戶端需要與服務端創建一個會話,這個時候客戶端需要提供一個服務端地址列表,“ host1 : port,host2: port ,host3:port ” ,根據地址開始創建zookeeper對象,這個時候客戶端的狀態則變更爲CONNECTION,同時客戶端會根據上述的地址列表,按照順序的方式獲取IP來嘗試建立網絡連接,直到成功連接上服務器,這個時候客戶端的狀態就可以變更爲CONNECTED。在zookeeper服務端提供服務的過程中,有可能遇到網絡波動等原因,導致客戶端與服務端斷開了連接,這個時候客戶端會進行重新連接操作,這個時候的狀態爲CONNECTION,當連接再次建立後,客戶端的狀態會再次更改爲CONNECTED,也就是說只要在zookeeper運行期間,客戶端的狀態總是能保持在CONNECTION或者是CONNECTED。當然在建立連接的過程中,如果出現了連接超時、權限檢查失敗或者是在建立連接的過程中,我們主動退出連接操作,這個時候客戶端的狀態都會變成CLOSE狀態。

會話創建

Session

Session是Zookeeper中會話的實例載體,一個Session則是指代一個客戶端會話。一個會話必須包含以下幾個基本的屬性:

  • SessionID : 會話的ID,用來唯一標識一個會話,每一次客戶端建立連接的時候,Zookeeper服務端都會給其分配一個全局唯一的sessionID

  • TimeOut:一次會話的超時時間,客戶端在構造Zookeeper實例的時候,會配置一個sessionTimeOut參數用於指定會話的超時的時間。Zookeeper服務端會按照連接的客戶端發來的TimeOut參數來計算並確定超時的時間

  • TickTime:下一次會話超時的時間點,爲了方便Zookeeper對會話進行所謂的分桶策略進行管理,同時也可以實現高效的對會話的一個檢查和清理。TickTime是一個13位的Long類型的數值,一般情況下這個值接近TimeOut,但是並不完全相等

  • isCloseing:用來標記當前會話是否已經處於被關閉的狀態。如果服務端檢測到當前會話的超時時間已經到了,就會將isCloseing屬性標記爲已經關閉,這樣以後即使再有這個會話的請求訪問也不會被處理

SessionID

SessionID作爲一個全局唯一的標識,我們可以來探究下Zookeeper是如何保證Session會話在集羣環境下依然能保證全局唯一性的:

sessionTracker初始化的時候,會調用initializeNextSession來生成session,算法大概如下:


1.  `public s ta tic long initializeNextSession(long id) {`

2.  `long nextSid = 0;`

3.  `nextSid = (System.currentTim eM illis() « 24) » 8;`

4.  `nextSid = nextSid | (id « 56);`

5.  `return nextSid;`

6.  `}
     }`

從這段代碼,我們可以看到session的創建大概分爲以下幾個步驟:

1.獲取當前時間的毫秒錶示

我們假設當前System.currentTimeMills()獲取的值是1380895182327,其64位二進制表示爲:

00000000 00000000 00000001 01000001 10000011 11000100 01001101 11110111

2.接下來左移24位,我們可以得到結果:

01000001 100000011 11000100 01001101 11110111 00000000 00000000 00000000,可以看到低位已經把高位補齊,剩下的低位都使用了0補齊

3.右移8位,結果變成了:

00000000 01000001 100000011 11000100 01001101 11110111 00000000 00000000

4.計算機器碼標識ID

在initializeNextSession方法中,出現了一個id變量,這個變量就是生成的SID的值,而SID在部署的時候就是我們在myid中配置的值,一般是一個整數,假設此時的值爲2,轉爲64位二進制表示:

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000010

此時發現高位幾乎都是0,進行左移56位以後,得到值如下:

00000010 00000000 00000000 00000000 00000000 00000000 00000000 00000000

5.將前面第三步和第四步得到的結果進行 | 操作:

可以得到結果爲:

00000010 01000001 10000011 11000100 01001101 11110111 00000000 00000000

這個時候我們可以得到一個單機中唯一的序列號ID,整個算法大概可以理解爲,先通過高8位確定機器以後,後面的56位按照毫秒進行隨機,可以看出來當前的算法!還是蠻嚴謹的,基本上看不出來什麼明顯的問題,但是其實也有問題的,其中我們可以看到,zk選擇了當前機器時間內的毫秒作爲基數,但是如果時間到了2022年4月8號以後, System . currentTimeMillis ()的值會是多少呢?


1.  `Date d = newDate(2022-1900 f 3,8);`

2.  `System. out. p rin tln ( Long. toBinaryString(d .getTime()));`

打印出來的結果爲:


1.  `0000000000000000000000011000000000000100110000010000010000000000`

接着我們左移24位以後會發現,這個時候的值依然是個負數,所以我們爲了保證不會出現負數的情況,解決方案如下:


1.  `publicstaticlong initializeNextSession(long id { ) {`

2.  `long nextSid = 0;`

3.  `nextSid = (System.currentTim eM illis() « 24) > » 8;`

4.  `nextSid = nextSid | (id « 56);`

5.  `return nextSid;`

6.  `} }`

這樣就可以避免生成的時候出現負數了

SessionTracker

SessionTracker是Zookeeper中的會話管理器,負責整個zk生命週期中會話的創建管理清理操作,而每一個會話在Sessiontracker內部都保留了三份,大體如下:

1.sessionsWithTimeout這是一個ConcurrentHashMap<long,integer style="margin: 0px; padding: 0px; box-sizing: border-box;">類型的數據結構,用來管理會話的超時時間,這個參數會被持久化到快照文件中去</long,integer>

2.sessionsById是一個HashMap<long,integer style="margin: 0px; padding: 0px; box-sizing: border-box;">類型的數據結構,用於根據sessionId來管理session實體</long,integer>

3.sessionsSets同樣也是一個HashMap<long,integer style="margin: 0px; padding: 0px; box-sizing: border-box;">類型的數據結構,用來會話超時的時候進行歸檔,便於進行會話恢復和管理</long,integer>

會話創建

創建會話的過程,大體可以分爲幾個步驟,分別是處理ConnectRequest請求、創建會話、處理器鏈路處理和響應,在zk服務端中,首先是NIOServerCnxn來負責接受來自客戶端的會話創建請求,並且進行反序列化工作,然後開始分配超時時間。分配完畢後,會開始創建sessionId,並且將其註冊到SessionsById和sessionsWithTimeOut,進行激活,這個時候就可以考慮處理流轉。

會話管理

Zookeeper中的會話管理主要是SesssionTracker負責的,內部使用了一個特殊的機制,稱之爲分桶策略,所謂分桶策略,其實是將類似的會話放在一個區塊中進行管理,以便於zookeeper對會話進行不同區塊的隔離以及同一區塊的統一處理

從圖中我們可以看到,所有的會話都分配在了不同的區塊中,分配原則是每個會話的下個超時的時間點,ExpiractionTime是指最近一次可能過期的時間點,每一個會話的ExpiractionTime的計算方式如下:

ExpiractionTime = CurrentTime + SessionTimeout

但是不要忘記了,Zookeeper的Leader服務器在運行期間會定期檢查是否超時,這個定期的時間間隔爲ExpiractionInterval,單位是秒,默認情況下是tickTime的值,即2000毫秒進行一次檢查,完整的ExpiractionTime的計算方式如下:


1.  `ExpirationTime_= CurrentTime+ SessionTimeout;`

2.  `ExpirationTime=  (ExpirationTime_/ Expirationlnterval+ 1) x Expirationlnterval;`

會話激活

同樣的,在整個zookeeper運行過程中,客戶端會在超時時間內向服務端發送PING請求來保持時效性,俗稱心跳檢測,而服務端在接受到了客戶端的心跳請求後需要再次激活會話狀態,這個過程稱之爲TouchSession,流程如下:

1.檢驗會話是否已經被關閉,Leader會去檢查會話是否被關閉,如果已經關閉,不會再去激活該會話

2.如果會話沒有被關閉,則開始計算下一次的超時時間Expiration_New,而計算的過程則是使用上面的公式

3.計算完新的超時時間以後,會去獲取會員原來的超時時間,並且根據時間來定位原來存放的區塊

4.接着,從該區塊中找到會話,進行會話遷移,放入新的Expiration_New對應的區塊中,如圖所示:

經過以上的步驟,基本已經完成了會話的激活,而每一次心跳的檢測,則是進行了一次會話激活操作,在整個Zookeeper運行過程中,一般如下兩個操作纔會導致會話激活:

1.當客戶端向服務端發送請求的時候,包括讀寫請求,都會主動觸發一次會話激活

2.如果客戶端在sessionTimeOut / 3時間範圍內尚未和服務器之間進行通信,即沒有發送任何請求,就會主動發起一個PING請求,去觸發服務端的會話激活操作

除此之外,由於會話之間的激活是按照分桶策略進行保存的,因此我們可以利用此策略優化對於會話的超時檢查,在Zookeeper中,會話超時檢查也是由SessionTracker負責的,內部有一個線程專門進行會話的超時檢查,只要依次的對每一個區塊的會話進行檢查,由於分桶是按照ExpriationInterval 的倍數來進行會話分佈的,因此只要在這些時間點檢查即可,這樣可以減少檢查的次數,並且批量清理會話,實現較高的效率。

會話清理

會話檢查操作以後,當發現有超時的會話的時候,會進行會話清理操作,而Zookeeper中的會話清理操作,主要是以下幾個步驟:

1.由於會話清理過程需要一定的時間,爲了保證在清理的過程中,該會話不會再去接受和處理髮來的請求,因此,在會話檢查完畢後,SessionTracker會先將其會話的isClose標記爲true,接着爲了保證在進行會話關閉的過程中,在整個集羣中都生效,Zookeeper使用了提交的方式,交給PreRequestProcessor處理器進行處理

2.在某個會話失效後,這個會話創建的相關臨時節點列表都應該被刪除,因此在刪除會話之前,需要先找到與改會話相對應的臨時節點列表,在Zookeeper的內存數據庫中,會爲每一個會話單獨保存一份由該會話維護的臨時節點集合,但是我們需要考慮一些特殊情況,例如在刪除會話的時候,有沒處理完畢的刪除節點的請求,而這個被刪除的節點剛好又是會話對應的臨時節點,或者這個時候正在處理臨時節點創建的請求,而且也是當前會話的請求。這個時候我們必須考慮處理方案,防止出現數據不一致的情況,而第一種情況,則是防止重複刪除,我們只需要先把請求對應的節點刪除,再去刪除對應的列表即可,而第二種情況,我們也需要先執行添加節點的請求,保證節點不會出現刪除遺漏即可。

3.當會話對應的臨時節點列表找到後,Zookeeper會將列表中所有的節點變成刪除節點的請求,並且丟給事物變更隊列OutStandingChanges中,接着FinalRequestProcessor處理器會觸發刪除節點的操作,從內存數據庫中刪除。

4.當會話對應的臨時節點被刪除以後,就需要將會話從SessionTracker中移除了,主要從SessionByIdsessionsWithTimeOut以及sessionsSets中將會話移除掉,當一切操作完成後,清理會話操作完成,這個時候將會關閉最終的連接NioServerCnxn

會話重連

在Zookeeper運行過程中,也可能會出現會話斷開後重連的情況,這個時候客戶端會從連接列表中按照順序的方式重新建立連接,直到連接上其中一臺機器爲止,這個時候可能出現兩種狀態,一種是正常的連接CONNECTED,這種情況是Zookeeper客戶端在超時時間內連接上了服務端,而超時以後才連接上服務端的話,這個時候的客戶端會話狀態則爲EXPIRED,被視爲非法會話。

而在重連之前,可能因爲其他原因導致的斷開連接,即CONNECTION_LESS,會拋出異常org.apache.zookeeper.KeeperException$ConnectionLossException,我們需要捕獲異常,並且在重連成功後,收到None-SyncConnection通知裏面進行setData的處理操作即可。而在這個過程中,會話可能會出現兩種情況:

會話失效:SESSION_EXPIRED

會話失效一般發生在ConnectionLoss期間,客戶端嘗試開始重連,但是在超時時間以後,才與服務端建立連接的情況,這個時候服務端就會通知客戶端當前會話已經失效,我們只能選擇重新創建一個會話,進行數據的處理操作

會話轉移:SESSION_MOVED

會話轉移也是在重連過程中常發生的一種情況,例如在斷開連接之前,會話是在服務端A上,但是在斷開連接重連以後,最終與服務端B重新恢復了會話,這種情況就稱之爲會話轉移。而會話轉移可能會帶來一個新的問題,例如在斷開連接之前,可能剛剛發送一個創建節點的請求,請求發送完畢後斷開了,很短時間內再次重連上了另一臺服務端,這個時候又發送了一個一樣的創建節點請求,這個時候一樣的事物請求可能會被執行了多次。因此在Zookeeper3.2版本開始,就有了會話轉移的概念,並且封裝了一個SessionMovedExection異常出來,在處理客戶端請求之前,會檢查一遍,請求的會話是不是當前服務端的,如果不存在當前服務端的會話,會直接拋出SessionMovedExection異常,當然這個時候客戶端已經斷開了連接,接受不到服務端的異常響應了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章