程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> JSP編程 >> 關於JSP >> tomcat集群實現源碼級別剖析

tomcat集群實現源碼級別剖析

編輯:關於JSP

tomcat集群實現源碼級別剖析


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

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

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

tomcat集群節點自身完成各自的數據同步,不管訪問到哪個節點都能找到對應的會話,如下圖,客戶端第一次訪問生成會話,tomcat自身會將會話增量信息同步到其他節點上,而且是每次請求完成都會同步此次請求過程中對session的所有操作,這樣一來下一次請求到集群中任意節點都能找到響應的會話信息,且能保證信息的及時性。

\

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

\

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包含的事件如下{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(ClusterMessagecmsg)方法,此方法會在本節點接收到其他節點發送過來的消息後被調用,且傳入的參數為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命名的節點上(這種方式稱為SessionStick,由apachejk模塊實現)。每個會話存在一個原件和一個備份,且備份與原件不會保存在同一個節點上,如下圖,例如當客戶端發起請求後通過負載均衡被分發到tomcat1實例節點上,生成一個包含.tomcat1後綴的會話標識,並且tomcat1節點根據一定策略選出此次會話對象備份的節點,然後將包含了{會話id,備份ip}的信息發送給tomcat2、tomcat3、tomcat4,如圖中虛線所示,這樣每個節點都有一個會話id、備份ip列表,即每個節點都有每個會話的備份ip地址。

完成上面一步後就是將會話內容備份到備份節點上,假如tomcat1的s1、s2兩個會話的備份地址為tomcat2,則把會話對象備份到tomcat2中,類似的有tomcat2把s3會話備份到tomcat4,tomcat4把s4、s5兩個對話備份到tomcat3,這樣集群中所有的會話都已經有了一份備份。當tomcat1一直不出故障,由於SessionStick技術客戶端將一直訪問到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則把n2、n3、n4添加到自己的列表,接著按某一時間間隔繼續向其他節點發心跳,如下圖右,假如n2未給n1響應信息,n1則把n2從自己的列表中刪除。BackupManager使用經典的Roundrobin算法用於備份節點的選擇,它屬於平均分配算法,按順序依次選擇節點,例如集群一共有node1、node2、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能力,①假如剛好源節點宕掉,請求落到備份節點則能獲取到會話對象,此時備份節點變為源節點,再從node2、node4中選一個作為備份節點,並且把會話對象拷貝到新備份節點上;②假如備份節點宕掉了,請求一樣能從源節點獲取到會話對象,但此時會從node2、node4中選一個新備份節點,並把會話對象拷貝到新備份節點上;③假如代理節點宕掉了,一切沒影響,正常工作。

\

搞清楚上面介紹的基本原理後再看看LazyReplicatedMap具體是如何實現將會話對象既在本地存儲又跨節點備份。

首先看下如何它是如何通過調用put方法實現保存,第一步,先實例化用於保存會話相關信息的MapEntry對象,傳入的參數key為會話id,value為會話對象,設置當前結點為源節點;第二步,判斷會話集中是否已經包含了此會話,如已存在則要刪除本地及備份節點上的會話;第三步,使用Roundrobin算法選出一個備份節點,並賦值到MapEntry對象的備份節點屬性;第四步,組裝包含MSG_BACKUP標識的MapMessage對象發到備份節點告訴備份節點要備份我傳過來的這個會話信息;第五步,組裝包含MSG_PROXY標識的MapMessage對象發送到除備份節點外的其他節點,告訴他們“你們是代理,請把此會話的id、源節點、備份節點等信息記錄下”;第六步,把MapEntry對象放入本地緩存;

publicObjectput(Objectkey,Objectvalue){

①實例化MapEntry,將key和value傳入,並設置源節點為目前節點。

②判斷本地內存是否已包含key,如是則不僅要本地remove掉,還要跨節點remove。

③通過Roundrobin算法從MapMember中選擇一個作為備份節點。

④實例化一個包含MSG_BACKUP標識的MapMessage對象並發送給備份節點。

⑤實例化一個包含MSG_PROXY標識的MapMessage對象並發送給除了備份節點外的其他(代理)節點。

⑥put進本地緩存。

}

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

publicObjectget(Objectkey){

①獲取本地的MapEntry對象,它或許直接包含了會話對象,或許包含了會話對象的存放位置信息。

②判斷本節點是否屬於源節點,如為源節點則直接獲取MapEntry對象裡面的會話對象並返回。

③判斷本節點是否屬於備份節點,若為備份節點則直接獲取MapEntry對象裡面的會話對象作為返回對象,並且還要將本節點升為源節點、重新選取一個新備份節點,把MapEntry對象拷貝到新備份節點。

④判斷本節點是否屬於代理節點,若為代理節點則向其他節點發送會話對象拷貝請求,“集群中誰有此會話對象請發送給我”,把接收到的會話對象放到本節點並作為返回對象,最後將本節點升為源節點。

}

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

publicObjectremove(Objectkey){

①刪除本地此MapEntry對象。

②廣播其他節點刪除此MapEntry對象。

}

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

 

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved