Tomcat叢集Cluster實現原理剖析
在上一篇文章中簡要介紹瞭如何通過簡單的配置來實現tomcat叢集,本文意在介紹對tomcat叢集進行更深入詳細的配置以滿足特定需求。
對於WEB應用叢集的技術實現而言,最大的難點就是如何能在叢集中的多個節點之間保持資料的一致性,會話(Session)資訊是這些資料中最重要的一塊。
要實現這一點,大體上有兩種方式,
一種是把所有Session資料放到一臺伺服器上或者資料庫中,叢集中的所有節點通過訪問這臺Session伺服器來獲取資料;
另一種就是在叢集中的所有節點間進行Session資料的同步拷貝,任何一個節點均儲存了所有的Session資料。
兩種方式都各有優點,
第一種方式簡單、易於實現,但是存在著Session伺服器發生故障會導致全系統不能正常工作的風險;
第二種方式可靠性更高,任一節點的故障不會對整個系統對客戶訪問的響應產生影響,但是技術實現上更復雜一些。
常見的平臺或中介軟體如microsoft asp.net和IBM WAS都會提供對兩種共享方式的支援,tomcat也是這樣,但是一般採用第二種方式。
當採用tomcat預設叢集配置(<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>)時,配置的細節實際上被省略了,
對於大多數應用而言,使用預設配置已經足夠,完整的預設配置應該是這樣:
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="8">
<Manager className="org.apache.catalina.ha.session.DeltaManager"
expireSessionsOnShutdown="false" notifyListenersOnReplication="true"/>
<Channel className="org.apache.catalina.tribes.group.GroupChannel">
<Membership className="org.apache.catalina.tribes.membership.McastService"
address="228.0.0.4"
port="45564"
frequency="500"
dropTime="3000"/>
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="auto"
port="4000"
autoBind="100"
selectorTimeout="5000"
maxThreads="6"/>
<Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
<Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
</Sender>
<Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
<Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>
</Channel>
<Valve className="org.apache.catalina.ha.tcp.ReplicationValve" filter=""/>
<Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/>
<Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
tempDir="/tmp/war-temp/"
deployDir="/tmp/war-deploy/"
watchDir="/tmp/war-listen/"
watchEnabled="false"/>
<ClusterListener className="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener"/>
<ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
</Cluster>
下面筆者對這裡的配置項作詳細解釋,以下內容均是筆者閱讀了tomcat官方文件後自己的理解,有些可能不對,希望讀者能帶著批判的眼光閱讀,並歡迎指正筆者錯誤。
tomcat叢集各節點通過建立tcp連結來完成Session的拷貝,拷貝有同步和非同步兩種模式。
在同步模式下,對客戶端的響應必須在Session拷貝到其他節點完成後進行;
非同步模式無需等待Session拷貝完成就可響應。
非同步模式更高效,但是同步模式可靠性更高。同步非同步模式由channelSendOptions引數控制,預設值是8,為非同步模式,4是同步模式。
在非同步模式下,可以通過加上拷貝確認(Acknowledge)來提高可靠性,此時channelSendOptions設為10。
Manager用來在節點間拷貝Session,預設使用DeltaManager,DeltaManager採用的一種all-to-all的工作方式,
即叢集中的節點會把Session資料向所有其他節點拷貝,而不管其他節點是否部署了當前應用。
當叢集中的節點數量很多並且部署著不同應用時,可以使用BackupManager,BackManager僅向部署了當前應用的節點拷貝Session。
但是到目前為止BackupManager並未經過大規模測試,可靠性不及DeltaManager。
Channel負責對tomcat叢集的IO層進行配置。Membership用於發現叢集中的其他節點,
使用同一個組播地址和埠的多個節點同屬一個子叢集,因此通過自定義組播地址和埠就可將一個大的tomcat叢集分成多個子叢集。
Receiver用於各個節點接收其他節點發送的資料,在預設配置下tomcat會從4000-4100間依次選取一個可用的埠進行接收,
自定義配置時, 如果多個tomcat節點在一臺物理伺服器上注意要使用不同的埠。
Sender用於向其他節點發送資料,具體實現通過Transport配置,
PooledParallelSender是從tcp連線池中獲取連線,可以實現並行傳送,即叢集中的多個節點可以同時向其他所有節點發送資料而互不影響。
Interceptor有點類似下面將要解釋的Valve,起到一個閥門的作用,在資料到達目的節點前進行檢測或其他操作,如TcpFailureDetector用於檢測在資料的傳輸過程中是否發生了tcp錯誤。
Valve用於在節點向客戶端響應前進行檢測或進行某些操作,
ReplicationValve就是用於用於檢測當前的響應是否涉及Session資料的更新,如果是則啟動Session拷貝操作,filter用於過濾請求,如客戶端對圖片,css,js的請求就不會涉及Session,
因此不需檢測,預設狀態下不進行過濾,監測所有的響應。
JvmRouteBinderValve會在前端的Apache mod_jk發生錯誤時保證同一客戶端的請求傳送到叢集的同一個節點,
tomcat官方文件並未解釋如何實現這一點,而且筆者認為這一設定似乎並無多大實用性。
Deployer用於叢集的farm功能,監控應用中檔案的更新,以保證叢集中所有節點應用的一致性,
如某個使用者上傳檔案到叢集中某個節點的應用程式目錄下,Deployer會監測到這一操作並把這一檔案拷貝到叢集中其他節點相同應用的對應目錄下以保持所有應用的一致。
這是一個相當強大的功能,不過很遺憾,tomcat叢集目前並不能做到這一點,開發人員正在努力實現它,這裡的配置只是預留了一個介面。
Listener用於跟蹤叢集中節點發出和收到的資料,也有點類似Valve的功能。
在大體瞭解了tomcat叢集實現模型後,就可以對叢集作出更優化的配置了,
tomcat推薦了一套配置,使用了比DeltaManager更高效的BackupManager,並且對ReplicationValve設定了請求過濾,
注意在一臺伺服器部署多個節點時需要修改Receiver的偵聽埠,另外,為了更高效的在節點間拷貝資料,所有tomcat節點最好採用相同的配置,
具體配置如下:
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="6">
<Manager className="org.apache.catalina.ha.session.BackupManager"
expireSessionsOnShutdown="false"
notifyListenersOnReplication="true"
mapSendOptions="6"/>
<Channel className="org.apache.catalina.tribes.group.GroupChannel">
<Membership className="org.apache.catalina.tribes.membership.McastService"
address="228.0.0.4"
port="45564"
frequency="500"
dropTime="3000"/>
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="auto"
port="5000"
selectorTimeout="100"
maxThreads="6"/>
<Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
<Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
</Sender>
<Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
<Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>
<Interceptor className="org.apache.catalina.tribes.group.interceptors.ThroughputInterceptor"/>
</Channel>
<Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
filter=".*\.gif;.*\.js;.*\.jpg;.*\.png;.*\.htm;.*\.html;.*\.css;.*\.txt;"/>
<Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
tempDir="/tmp/war-temp/"
deployDir="/tmp/war-deploy/"
watchDir="/tmp/war-listen/"
watchEnabled="false"/>
<ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
</Cluster>
Tomcat叢集除了可以進行Session資料的拷貝,還可進行Context屬性的拷貝,通過修改context.xml的Context配置可以實現,
使用<Context className="org.apache.catalina.ha.context.ReplicatedContext"/>替換預設Context即可,當然也可再加上distributable="true"屬性。
下面通過假想的一組場景來描述tomcat叢集如何工作,叢集採用預設配置,由t1和t2兩個tomcat例程組成,場景按照時間順序排列。
1. t1啟動
t1按照標準的tomcat啟動,當Host物件被建立時,一個Cluster物件(預設配置下是SimpleTcpCluster)也同時被關聯到這個Host物件。
當某個應用在web.xml中設定了distributable時,Tomcat將為此應用的上下文環境建立一個DeltaManager。
SimpleTcpCluster啟動membership服務和Replication服務(用於建立tcp連線)。
2. t2啟動(待t1啟動完成後)
首先t2會執行和t1一樣的操作,然後SimpleTcpCluster會建立一個由t1和t2組成的membership。
接著t2向叢集中已啟動的伺服器即t1請求Session資料,如果t1沒有響應t2的拷貝請求,t2會在60秒後time out。
在Session資料拷貝完成之前t2不會接收客戶端的http或mod_jk/ajp請求。
3. t1接收http請求,建立Session s1
t1正常響應客戶請求,但是在t1把結果傳送回客戶端時,ReplicationValve會攔截當前請求(如果filter中配置了不需攔截的請求型別,這一步就不會進行,預設配置下攔截所有請求),
如果發現當前請求更新了Session,呼叫Replication服務建立tcp連線把Session拷貝到membership列表中的其他節點即t2,
返回結果給客戶端(注意,如果採用同步拷貝,必須等拷貝完成後才會返回結果,非同步拷貝在資料傳送到tcp連線就返回結果,不等待拷貝完成)。
在拷貝時,所有儲存在當前Session中的可序列化的物件都會被拷貝,而不僅僅是發生更新的部分。
4. t1崩潰
當t1崩潰時,t2會被告知t1已從叢集中退出,然後t2就會把t1從自己的membership列表中刪除,
發生在t2的Session更新不再往t1拷貝,同時負載均衡器會把後續的http請求全部轉發給t2。
在此過程中所有的Session資料不會丟失。
5. t2接收s1的請求
t2正常響應s1的請求,因為t2儲存著s1的所有資料。
6. t1重新啟動
按步驟1、2一樣的操作啟動,加入叢集,從t2拷貝所有Session資料,拷貝完成後開放自己的http和mod_jk/ajp埠接收請求。
7. t1接收請求,s1失效
t1繼續接收來自s1的請求,把s1設定為過期。
這裡的過期並非因為s1處於非活動狀態超過設定的時間,而是執行類似登出的操作而引起的Session失效。
這時t1並非傳送s1的所有資料而是一個類似s1 expired的訊息,t2收到訊息後也會把s1設為過期。
8. t2接收請求,建立Session s2
和步驟3一樣。
9. t1 s2過期
對於因超時引起的Session失效t1無需通知t2,因為t2同樣知道s2已經超時。
因此對於tomcat叢集有一點非常重要,所有節點的作業系統時間必須一致!
不然會出現某個節點Session已過期而在另一節點此Session仍處於活動狀態的現象。
因為tomcat的session同步功能需要用到組播,
windows預設情況下是開通組播服務的,
但是linux預設情況下並沒有開通,
可以通過指令開啟route add -net 224.0.0.0 netmask 240.0.0.0 dev eth0,
如果需要伺服器啟動時即開通組播需在/etc/sysconfig/static-routes檔案內加入
eht0 net 224.0.0.0 netmask 240.0.0.0。