1. 程式人生 > 其它 >ZooKeeper會話Session (秒懂+圖解+史上最全)

ZooKeeper會話Session (秒懂+圖解+史上最全)

什麼是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對應的會話,這段邏輯是在SessionTrackerImpltouchSession中實現的。先看下流程,再看原始碼:


再看下原始碼實現:

//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