Die folgende Beschreibung bezieht sich auf Tomcat 6.0.14 mit Java 5.
Eeine Anleitung zum Aufbauen eines Tomcat-Clusters mit Session-Replikation ist in der offiziellen Tomca-Dokumentation zu finden: http://tomcat.apache.org/tomcat-6.0-doc/cluster-howto.html
Ich beschreibe hier die Probleme auf die gestoßen bin beim Aufbau eines solchen Clusters.
Bei einem Cluster werden die HttpSessions auf alle Nodes (Teilnehmer/Server) im Cluster repliziert (kopiert). Somit ist es egal auf welche Node der Loadbalancer vor dem Cluster weiterleitet, da alle Sessions auf allen Nodes zur Verfuegung stehen. Das hat den Vorteil der hohen Verfuegbarkeit aber den Nachteil eines hohen Speicherverbrauchs.
Wenn sich ein Kunde auf eine Webapplikation einloggt die auf so einem Cluster laeuft, dann kriegt er nichts davon wenn der Server auf dem er gerade arbeitet abstuerzt. Der Loadbalancer leitet den naechsten Request vom Kunden auf einen anderen Server im Cluster um, wo er mit seiner alten Session weiter arbeiten kann. Bei einem einfachen Loadbalancing ohne Cluster und ohne Session-Replikation muesste sich der Kunde wieder neue anmelden an der WebApplikation und seine HttpSession waere fuer immer verloren.
Nach jedem Request wird geprueft ob sich eine Aenderung in der HttpSession ergeben hat. Wenn dies der Fall ist, wird das Delta auf alle anderen Nodes im Cluster repliziert. In der offiziellen Doku ist das folgende Stueck Konfigurationscode zu finden, das in die <TOMCAT_HOME>/conf/server.xml eingefuegt werden muss, um Tomcat im Cluster laufen zu lassen.
<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>
Nach dem Starten des Tomcats habe ich dann folgende Fehlermeldung bekommen:
SEVERE: Unable to process request in NioReceiverjava.net.SocketException: Invalid argument
Das Problem kann behoben werden indem man dem Transport einen timeout und einen KeepAliveCount zuweist.
<Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender" timeout="60000" keepAliveCount="120000" />
Mein Eintrag in der server.xml sieht so aus.
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="6" > <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="4002" autoBind="100" selectorTimeout="5000" maxThreads="12" /> <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter" > <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender" timeout="60000" keepAliveCount="120000" /> </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>
Wenn die Nodes alle auf der gleichen Hardware laufen dann muessen die Ports vom Receiver angepasst werden. Die server.xml des zweiten Tomcats auf der gleichen Hardware sieht bei mir so aus:
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="6" > <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="4001" autoBind="100" selectorTimeout="5000" maxThreads="12" /> <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter" > <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender" timeout="60000" keepAliveCount="120000" /> </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="true" /> <ClusterListener className="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener" /> <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener" /> </Cluster>
Der naechste Fehler auf dem ich gestossen bin ist:
java.net.SocketException: Invalid argument
Wenn der Tomcat mit der Option -Djava.net.preferIPv4Stack=true aufgerufen wird, dann tritt der Fehler nicht auf. Unter Linux kann mit folgendem Befehl die Umgebungsvariable “JAVA_OPTS” erweitert werden.
export JAVA_OPTS=$JAVA_OPTS;-Djava.net.preferIPv4Stack=true
bei starten des Tomcats werden alle Optionen aus der JAVA_OPTS gelesen.
Nun lief das Cluster endlich erfolgreich, also habe ich angefangen einige Applikation drauf zu deployen. Damit eine Applikation Clusterfaehig ist muessen alle Objekte die in der HttpSession abgelegt werden Serialisierbar sein. Ausserdem muss der Deploymentdescriptor (web.xml) folgenden Eintrag enthalten.
<distributable/>
Das deployen selber wollte ich ueber den Farmdeployer erledigen. Dabei soll die WAR-File nach “/tmp/war-listen” kopiert werden und der Farmdeployer verteil die Applikation auf alle Nodes im Cluster. Das hat leider nicht funktioniert! Nach einigem forschen im Internet bin ich in der tomcat-mailinglist auf einen Eintrag gestossen, aus dem ich erfahren habe das der farmdeployer noch nicht wirklich funktioniert und die Entwickler noch dran sind.
Also habe ich meine Applikationen per Hand deployt. Bei einer IceFaces Applikation konnte die Session mit folgender Fehlermeldung nicht repliziert werden:
java.io.NotSerializableException: com.icesoft.faces.webapp.http.servlet.ServletExternalContext
Bei einer Struts 2 + Spring 2.5 Anwendung ist die Replikation an folgender Fehlermeldung gescheitert.
java.io.NotSerializableException: org.springframework.jdbc.datasource.DriverManagerDataSource
Alles nicht so wirklich zufriedendstellend. Da gibt man sich muehe alle Seine Objekte Serialisierbar zu halten und kriegt man vom Framework nicht Serailisierbare Objekte in den Weg gelegt. Das bedarf einer intensiven Nachforschung.