Tomcat集羣實現源碼級別剖析

隨着互聯網快速發展,各類各樣供外部訪問的系統愈來愈多且訪問量愈來愈大,之前Web容器能夠包攬接收-邏輯處理-響應整個請求生命週期的工做,如今爲了構建讓更多用戶訪問更強大的系統,人們經過不斷地業務解耦、架構解耦將web容器的邏輯處理抽離交由其餘中間件處理,例如緩存中間件、消息隊列中間件、數據存儲中間件等等。Web容器負責的工做可能愈來愈少,可是它確實必不可少的部分,它負責接收用戶請求並分別調用各個服務最後響應。能夠說目前最受歡迎的web容器是用Java寫的tomcat小貓,因爲生產上的tomcat考慮負載均衡及高可用性,它通常以集羣模式運行,因此這篇文章主要探討的是tomcat的集羣功能如何實現且生產部署如何選型。javascript

若是說一個web應用不涉及會話的話,那麼作集羣是至關簡單的,由於節點都是無狀態的,集羣內各個節點無需互相通訊,只須要將各個請求均勻分配到集羣節點便可。但基本全部web應用都會使用會話機制,因此作web應用集羣時整個難點在於會話數據的同步,固然你能夠經過一些策略規避複雜的額數據同步操做,例如把會話信息保存在分佈式緩存或數據庫中統一集中管理,避免了tomcat集羣之間的通訊。但這種方式也有不足,要額外引入數據庫或緩存服務,同時也要保證它們的高可用性,增長了機器和維護成本。本文假設不使用統一管理會話的模式而是將會話交由tomcat自身集羣管理。java

集羣增量會話管理器——DeltaManager

tomcat集羣節點自身完成各自的數據同步,無論訪問到哪一個節點都能找到對應的會話,以下圖,客戶端第一次訪問生成會話,tomcat自身會將會話增量信息同步到其餘節點上,並且是每次請求完成都會同步這次請求過程當中對session的全部操做,這樣一來下一次請求到集羣中任意節點都能找到響應的會話信息,且能保證信息的及時性。
node

這就是tomcat默認的集羣會話管理器——DeltaManager。它主要用於集羣中各個節點之間會話狀態的同步維護。DeltaManager的職責是將某節點的會話該變同步到集羣內其餘成員節點上,它屬於全節點複製模式,所謂全節點複製是指集羣中某個節點的狀態變化後須要同步到集羣中剩餘的節點,非全節點方式可能只是同步到其中某個或若干節點。在集羣中全節點會話複製的一個大體步驟以下圖所示,客戶端發起一個請求,假設經過必定的負載均衡設備分發策略分到其中一個結點node1,若是還未存在session對象的話web容器將會建立一個會話對象,接着執行一些邏輯處理,在對客戶端響應以前有個重要的事情是要把session對象同步到集羣中其餘節點上,最後再響應客戶端。當客戶端第二次發起請求時,假如分發到node3節點上,因爲同步了node1的session會話,因此在執行邏輯時並不會取不到session的值。若是刪除某個會話對象則要同時通知其餘節點把相應會話刪除,若是修改了某個會話的某些屬性也一樣要更新到其餘節點的會話中。web

DeltaManager其實就是一個會話同步通訊解決方案,除了具有上面提到的全節點複製外,它還有具備只複製會話增量的特性,增量是以一個完整請求爲週期,即會將一個請求過程當中全部會話修改量在響應前進行集羣同步。往下看Tomcat具體實現方案。算法

爲區分不一樣的動做必需要先定義好各類事件,例如會話建立事件、會話訪問事件、會話失效事件、獲取全部會話事件、會話增量事件、會話ID改變事件等等,實際上tomcat集羣會有9種事件,集羣根據這些不一樣的事件就能夠彼此進行通訊,接收方對不一樣事件作不一樣的操做。以下圖,例如node1節點建立完一個會話後,即向其餘三個節點發送EVT_SESSION_CREATED事件,其餘三個節點接收到此事件後則各自在本身本地建立一個會話,會話包含了兩個很重要的屬性——會話ID和建立時間,這兩個屬性都必須由node1節點跟着EVT_SESSION_CREATED一塊兒發送出去,本地會話建立成功後即完成了會話建立同步工做,此時你經過會話ID查找集羣中任意一個節點均可以找到對應的會話。一樣對於會話訪問事件,node1向其餘節點發送EVT_SESSION_ACCESSED事件及會話ID,其餘節點根據會話ID找到對應會話並更新會話最後訪問時間,以避免被認爲是過時會話而被清理。相似的還有會話失效事件(同步集羣銷燬某會話)、會話ID改變事件(同步集羣更改會話ID)等等操做。數據庫

Tomcat使用SessionMessageImpl類定義了各類集羣通訊事件及操做方法,在整個集羣通訊過程當中就是按照此類定義好的事件進行通訊,SessionMessageImpl包含的事件以下apache

{ 
EVT_SESSION_CREATED、
EVT_SESSION_EXPIRED、
EVT_SESSION_ACCESSED、
EVT_GET_ALL_SESSIONS、
EVT_SESSION_DELTA、
EVT_ALL_SESSION_DATA、
EVT_ALL_SESSION_TRANSFERCOMPLETE、
EVT_CHANGE_SESSION_ID、
EVT_ALL_SESSION_NOCONTEXTMANAGER 
}複製代碼

,除此以外它繼承了序列化接口(方便序列化)、集羣消息接口(集羣的操做)、會話消息接口(事件定義及會話操做)。後端

集羣增量會話管理器DeltaManager能夠說是經過SessionMessageImpl消息來管理DeltaSession,即根據SessionMessageImpl裏面的事件響應不一樣的操做。DeltaManager存在一個messageDataReceived(ClusterMessage cmsg)方法,此方法會在本節點接收到其餘節點發送過來的消息後被調用,且傳入的參數爲ClusterMessage類型,可轉化爲SessionMessage類型,而後根據SessionMessage定義的9種事件作不一樣處理。其中有一個事件須要關注的是EVT_SESSION_DELTA,它是對會話增量同步處理的事件,某個節點在一個完整的請求過程當中對某會話相關屬性的全部操做被抽象到了DeltaRequest對象中,而DeltaRequest被序列化後會放到SessionMessage中,因此EVT_SESSION_DELTA事件處理邏輯就是從SessionMessage獲取並反序列化出DeltaRequest對象,再將DeltaRequest包含的對某個會話的全部操做同步到本地該會話中,至此完成會話增量同步。數組

總的來講DeltaManager就是DeltaSession的管理器,它提供了會話增量的同步方式而不是全量同步,極大提升了同步效率。緩存

集羣備份會話管理器——BackupManager

全節點複製的網絡流量隨節點數量增長呈平方趨勢增加,也正是由於這個因素致使沒法構建較大規模的集羣,爲了使集羣節點能更加大,首要解決的就是數據複製時流量增加的問題,因而tomcat提出了另一種會話管理方式,每一個會話只會有一個備份,它使會話備份的網絡流量隨節點數量的增長呈線性趨勢增加,大大減小了網絡流量和邏輯操做,可構建較大的集羣。

下面看看這種方式具體的工做機制,集羣通常是經過負載均衡對外提供總體服務,全部節點被隱藏在後端組成一個總體。前面各類模式的實現都無需負載均衡協助,因此圖中都把負載均衡省略了。最多見的負載方式是前面用apache拖全部節點,它支持將相似「326257DA6DB76F8D2E38F2C4540D1DEA.tomcat1」的會話id進行分解,定位到tomcat集羣中以tomcat1命名的節點上(這種方式稱爲Session Stick,由apache jk模塊實現)。

每一個會話存在一個原件和一個備份,且備份與原件不會保存在同一個節點上,以下圖,例如當客戶端發起請求後經過負載均衡被分發到tomcat1實例節點上,生成一個包含.tomcat1後綴的會話標識,而且tomcat1節點根據必定策略選出這次會話對象備份的節點,而後將包含了{會話id,備份ip}的信息發送給tomcat二、tomcat三、tomcat4,如圖中虛線所示,這樣每一個節點都有一個會話id、備份ip列表,即每一個節點都有每一個會話的備份ip地址。

完成上面一步後就是將會話內容備份到備份節點上,假如tomcat1的s一、s2兩個會話的備份地址爲tomcat2,則把會話對象備份到tomcat2中,相似的有tomcat2把s3會話備份到tomcat4,tomcat4把s四、s5兩個對話備份到tomcat3,這樣集羣中全部的會話都已經有了一份備份。當tomcat1一直不出故障,因爲Session Stick技術客戶端將一直訪問到tomcat1節點上,保證一直能獲取到會話。而當tomcat1出故障了,這時tomcat也提供了一個failover機制,apache感知到後端集羣tomcat1節點被移除了,這時它會把請求隨機分配到其餘任意節點上,接下去會有兩種狀況:

  • 恰好分到了備份節點tomcat2上,此時仍能獲取到s1會話,除此以外,tomcat2還要另外作的事是將這個s1會話標記爲原件且繼續選取一個備份地址備份s1會話,這樣一來又有了備份。
  • 假如分到了非備份節點tomcat3,此時確定找不到s1會話,因而它將向集羣全部節點發問,「請問誰有s1會話的備份ip地址信息?」,由於只有tomcat2有s1的備份地址信息,它接收到詢問後應答告知tomcat3節點s1會話的備份在tomcat2,根據這個信息就能查到s1會話了,而且tomcat3在本身本地生成s1會話並標爲原件,tomcat2上的副本不變,這樣一來一樣能找到s1會話,正常完整整個請求處理。

接着分析Tomcat對上面機制詳細的實現,正常狀況下爲了支持高效的併發操做,tomcat的全部會話集使用ConcurrentHashMap 結構保存,String類型是指SessionId,MapEntry則是對session、源節點成員及備份節點等的封裝(詳細的類結構以下圖所示,備份節點雖然爲數組類型,但實際狀況咱們只會設置一個備份節點),通常session對象由哪一個節點生成則哪一個節點爲源節點,備份節點則爲集羣中其餘任意一節點,因此MapEntry能夠當作是包含了源節點和備份節點信息的會話對象。會話管理器其實就是對會話集操做的封裝,從設計角度看,爲了改變會話集的操做行爲,只需繼承ConcurrentHashMap類並重寫其中一些方法便可實現,例如put、get、remove等等操做實現跨節點操做。因而tomcat的BackupManager對整個會話集的跨節點操做被封裝到一個繼承ConcurrentHashMap類的LazyReplicatedMap子類中,而要實現跨節點的操做要作的事不少,例如備份節點列表的維護、備份節點選擇、通訊協議、序列化&反序列化及複雜的IO操做等等,弄清楚了LazyReplicatedMap的工做原理也就基本清楚BackupManager如何工做。

每一個節點都要維護一份集羣節點信息列表供會話備份路由選擇,信息列表的維護主要經過啓動時向全部節點廣播節點信息及心跳去維護,以下圖左,n1啓動時向其餘節點廣播本身的信息,其餘節點收到信息後把n1添加到本身的列表,而n1則把n二、n三、n4添加到本身的列表,接着按某一時間間隔繼續向其餘節點發心跳,以下圖右,假如n2未給n1響應信息,n1則把n2從本身的列表中刪除。BackupManager使用經典的Round robin算法用於備份節點的選擇,它屬於平均分配算法,按順序依次選擇節點,例如集羣一共有node一、node二、node3三個節點,node1將session1備份到node2,而session2則備份到node3。對於節點信息列表BackupManager是使用HashMap 結構保存,Member是包含了節點信息屬性的節點抽象,Long是指節點最新的存活時間,在作心跳時就是根據最新的存活時間和超時閥值判斷節點是否失效。

通訊的協議及信息載體由MapMessage類定義,通訊協議其實就是通訊雙方約定好的語義,定義的常量包括

{ 
MSG_BACKUP、
MSG_RETRIEVE_BACKUP、
MSG_PROXY、
MSG_REMOVE、
MSG_STATE、
MSG_START、
MSG_STOP、
MSG_INIT、
MSG_COPY、
MSG_STATE_COPY、
MSG_ACCESS
}複製代碼

,這裏每一個值都表明一個語義,例如MSG_BACKUP表示讓接收方把接收到的會話對象進行備份、MSG_REMOVE則表示讓接收方按照接收到的會話id把對應的會話刪除等等。除此以外MapMessage類還包含valuedata(byte[])、keydata(byte[])、nodes(Member[])、primary(Member),分別表示會話對象字節流、會話id字節流、備份節點、源節點。這樣一來全部要素都有了,在備份操做中MapMessage對象就像組成一個句子:「本人會話id爲keydata,會話值爲valuedata,個人源節點爲primary,我如今須要作備份操做」。

另外,序列化&反序列化工做交由jdk的ObjectInputStream、ObjectOutputStream去完成,而複雜的網絡IO則交由tribes通訊框架完成。

關於源節點、備份節點、代理節點分別表明什麼意思,每一個集羣每一個會話只有一個源節點,一個備份節點,若干個代理節點。以下圖,node1爲源節點,表示會話對象由它建立,保存的是會話對象的原件;node3爲備份節點,保存的是會話對象的備份件;node2和node4爲代理節點,它們保存的僅僅是會話位置信息,例如備份節點node3的機器的ip。這樣分類是爲了提供failover能力,

  1. 假如恰好源節點宕掉,請求落到備份節點則能獲取到會話對象,此時備份節點變爲源節點,再從node二、node4中選一個做爲備份節點,而且把會話對象拷貝到新備份節點上;
  2. 假如備份節點宕掉了,請求同樣能從源節點獲取到會話對象,但此時會從node二、node4中選一個新備份節點,並把會話對象拷貝到新備份節點上;
  3. 假如代理節點宕掉了,一切沒影響,正常工做。

搞清楚上面介紹的基本原理後再看看LazyReplicatedMap具體是如何實現將會話對象既在本地存儲又跨節點備份。
首先看下如何它是如何經過調用put方法實現保存,
第一步,先實例化用於保存會話相關信息的MapEntry對象,傳入的參數key爲會話id,value爲會話對象,設置當前結點爲源節點;
第二步,判斷會話集中是否已經包含了此會話,如已存在則要刪除本地及備份節點上的會話;
第三步,使用Round robin算法選出一個備份節點,並賦值到MapEntry對象的備份節點屬性;
第四步,組裝包含MSG_BACKUP標識的MapMessage對象發到備份節點告訴備份節點要備份我傳過來的這個會話信息;
第五步,組裝包含MSG_PROXY標識的MapMessage對象發送到除備份節點外的其餘節點,告訴他們「大家是代理,請把此會話的id、源節點、備份節點等信息記錄下」;
第六步,把MapEntry對象放入本地緩存;

public Object put(Object key, Object value) {
  ①實例化MapEntry,將key和value傳入,並設置源節點爲目前節點。
  ②判斷本地內存是否已包含key,如是則不只要本地remove掉,還要跨節點remove。
  ③經過Round robin算法從MapMember中選擇一個做爲備份節點。
  ④實例化一個包含MSG_BACKUP標識的MapMessage對象併發送給備份節點。
  ⑤實例化一個包含MSG_PROXY標識的MapMessage對象併發送給除了備份節點外的其餘(代理)節點。
  ⑥put進本地緩存。
}複製代碼

其次,再看看它如何經過get實現獲取會話對象操做:

public Object get(Object key) {
 ①獲取本地的MapEntry對象,它或許直接包含了會話對象,或許包含了會話對象的存放位置信息。
 ②判斷本節點是否屬於源節點,如爲源節點則直接獲取MapEntry對象裏面的會話對象並返回。
 ③判斷本節點是否屬於備份節點,若爲備份節點則直接獲取MapEntry對象裏面的會話對象做爲返回對象,而且還要將本節點升爲源節點、從新選取一個新備份節點,把MapEntry對象拷貝到新備份節點。
 ④判斷本節點是否屬於代理節點,若爲代理節點則向其餘節點發送會話對象拷貝請求,「集羣中誰有此會話對象請發送給我」,把接收到的會話對象放到本節點並做爲返回對象,最後將本節點升爲源節點。
}複製代碼

最後,看看刪除會話對象remove操做的實現:

public Object remove(Object key) {
 ①刪除本地此MapEntry對象。
 ②廣播其餘節點刪除此MapEntry對象。
}複製代碼

經過上面三個方法已經很清晰描述了新的Map是如何進行跨節點的增刪改查的,BackupManager會話管理器就是經過這個新的Map進行會話管理。

以上便是tomcat集羣機制源碼基本的剖析,兩種都有各自的優缺點,全節點模式是兩兩互相複製的,一旦集羣節點數量及訪問量大起來,將致使大量的會話信息須要互相複製同步,很容易致使網絡阻塞,並且這些同步操做極可能會成爲總體性能的瓶頸,根據經驗,此種方案在實際生產上推薦的集羣節點個數爲3-6個,沒法組建更大的集羣,並且冗餘了大量的數據,利用率不高。而會話備份模式則大大減小了網絡流量和邏輯操做,可構建較大的集羣,生產上能夠組成十個以上的節點,雖然這種模式支持更大的集羣,但它也有本身的缺點,例如它只有一個數據備份,假如恰好源數據和備份數據所在的機器同時宕掉了,則沒辦法恢復數據,不過恰好同時宕機的機率很小很小。

歡迎關注:

這裏寫圖片描述
相關文章
相關標籤/搜索