Static Tomcat cluster, Apache load balancer and replicating sessions
The normal way to set up a Tomcat 6 cluster is to use multicast messages to manage cluster membership, with Tribes replication of session data (or deltas) to all known members. When multicasting is not supported in your network environment, static membership is an alternative you can try. It’s not as flexible, in that you cannot dynamically size a running cluster, but it has all the essential features such as session synchronization, balancing, stickiness and failover.
Assuming you are reasonably familiar with Apache httpd and Apache Tomcat, here’s the OS-neutral recipe.
You will need:
- Apache httpd 2.2
- Apache Tomcat 6.0.xx (and we will assume it’s on the latest Java)
- Connectors 1.2.xx (latest version of mod_jk)
- A distributable Web application.
For this little tutorial, let’s assume that the Apache load balancer will be on 10.0.0.123 (port 80), and the two Tomcats will be on 10.0.0.111:80 and 10.0.0.222:80 respectively. Both Tomcats will also be using AJP on port 8009. Finally, we assume your DNS (or hosts file) will have cluster.example.com mapped to the load balancer on 10.0.0.123. If you want SSL, you’ll need a secure port (e.g. 8433) assigned to another connector, though you’ll need to terminate the SSL at the load balancer, then set secure=”true” and SSLEnabled=”false” in the corresponding <Connector> element. We are also reserving port 4444 for replication traffic. Obviously you can change all these values to suit your own requirements, to fit your network, avoid IP/port clashes etc.
Step 1 : set up Apache 2.2 as a load balancer
Download the latest version of mod_jk.so and place it into the modules directory in Apache. Edit the conf/httpd.conf file so that the following lines are placed immediately after the existing list of LoadModule entries:
LoadModule jk_module modules/mod_jk.so JkWorkersFile conf/workers.properties JkLogFile logs/mod_jk.log JkLogLevel info JkLogStampFormat "[%a %b %d %H:%M:%S %Y] " JkOptions +ForwardKeySize +ForwardURICompat -ForwardDirectories JkRequestLogFormat "%w %V %T" JkMount / balancer JkMount /* balancer
Make sure you are not using virtual hosts by commenting out the Include that loads the vhosts file:
#Include conf/extra/httpd-vhosts.conf
Edit the “Listen” entry so that it matches the loadbalancer address:
Listen: 10.0.0.123:80
Create a file called conf/workers.properties with the following:
worker.list=balancer worker.node1.port=8009 worker.node1.host=10.0.0.111 worker.node1.type=ajp13 worker.node1.lbfactor=1 worker.node2.port=8009 worker.node2.host=10.0.0.222 worker.node2.type=ajp13 worker.node2.lbfactor=1 worker.balancer.type=lb worker.balancer.balance_workers=node1,node2 worker.balancer.method=B
Step 2 : set up the first Tomcat node
Most of the magic happens in server.xml, which should look something like this:
<?xml version='1.0' encoding='utf-8'?> <Server port="8006" shutdown="s3cretzhutdown"> Â . . . Â <Service name="Catalina"> Â Â Â . . . Â Â Â <Connector address="10.0.0.111" port="80" protocol="HTTP/1.1" redirectPort="8433" connectionTimeout="10000" enableLookups="false" maxPostSize="20000" executor="tcThreadPool" /> Â Â Â <Connector address="10.0.0.111" port="8009" protocol="AJP/1.3" redirectPort="8433"/> Â Â Â <Engine name="Catalina" defaultHost="cluster" jvmRoute="node1"> Â Â Â Â Â <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="6" channelStartOptions="3"> Â Â Â Â Â Â Â <Manager className="org.apache.catalina.ha.session.DeltaManager" Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â expireSessionsOnShutdown="false" notifyListenersOnReplication="true" /> Â Â Â Â Â Â Â <Channel className="org.apache.catalina.tribes.group.GroupChannel"> Â Â Â Â Â Â Â Â Â <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver" Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â autoBind="0" selectorTimeout="5000" maxThreads="6" Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â address="10.0.0.111" port="4444" Â Â Â Â Â Â Â Â Â /> Â Â Â Â Â Â Â Â Â <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.TcpPingInterceptor" /> Â Â Â Â Â Â Â Â Â <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.StaticMembershipInterceptor"> Â Â Â Â Â Â Â Â Â Â Â <Member className="org.apache.catalina.tribes.membership.StaticMember" securePort="-1" Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â host="10.0.0.222" port="4444" Â Â Â Â Â Â Â Â Â Â Â /> Â Â Â Â Â Â Â Â Â Â Â . . . Â Â Â Â Â Â Â Â Â </Interceptor> Â Â Â Â Â Â Â </Channel> Â Â Â Â Â Â Â <Valve className="org.apache.catalina.ha.tcp.ReplicationValve" filter=".*.gif;.*.jpg;.*.png;.*.css" /> Â Â Â Â Â Â Â <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve" /> Â Â Â Â Â Â Â <ClusterListener className="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener" /> Â Â Â Â Â Â Â <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener" /> Â Â Â Â Â </Cluster> Â Â Â Â Â <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase" /> Â Â Â Â Â <Host name="cluster" appBase="webapps" unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false"> Â Â Â Â Â Â Â <Alias>cluster.example.com</Alias> Â Â Â Â Â Â Â . . . Â Â Â Â Â </Host> Â Â Â </Engine> Â </Service> </Server>
The channelStartOptions value of 3 will disable multicasting. We also don’t use the <Membership> element in this configuration, which is only for multicast environments. Instead, we explicitly list the other members of the cluster as <Member> elements within the StaticMembershipInteceptor. In this tutorial, from the perspective of the .111 node, there is only one other member (.222), so we have only one <Member> element. Add more if you have a bigger cluster. Don’t add the current node, since it makes no sense for a node to be sending replication data to itself.
The <Member> elements do not have a uniqueId attribute set. In the current version of Tomcat, attempting to set a value will cause a warning. The value seems to default to 16 zeros. This does not appear to cause any problem, because presumably the session manager is determining uniqueness based on socket identity. However, a Tomcat node that has just rebooted will be indistinguishable from it’s pre-reboot incarnation, despite the fact that its memory has been flushed, so relying only on the socket to recognize cluster members might not be 100% reliable. Nevertheless, in the absence of any clear documentation of this feature, we’re just going to assume this is normal.
Repeat this formula for the other Tomcat nodes in your cluster.
Step 3 : Configure a distributed Web application with session replication
In the WEB-INF/web.xml file, make sure the application is marked as distributable:
<destributable/>
Also make sure you have a META-INF/context.xml configured to tell Tomcat to replicate sessions:
<Context distributable="true"></Context>
All data placed into a session must be serializable. Keep in mind that Tomcat only knows about session data being changed when you use the session.setAttribute() method, so any indirect manipulation of complex session objects may go unnoticed unless you force the “dirty” side-effect of a redundant operation such as session.setAttribute(“x”,session.getAttribute(“x”)).
Step 4 : Test
Run Apache first. Then run the two Tomcats. You should see something like the following in the Tomcat logs:
**-***-**** **:**:** org.apache.catalina.ha.tcp.SimpleTcpCluster start INFO: Cluster is about to start **-***-**** **:**:**Â org.apache.catalina.tribes.transport.ReceiverBase bind INFO: Receiver Server Socket bound to:/10.0.0.111:4444 **-***-**** **:**:**Â org.apache.catalina.ha.tcp.SimpleTcpCluster memberAdded INFO: Replication member added:org.apache.catalina.tribes.membership.MemberImpl\ Â [tcp://10.0.0.222:4444,10.0.0.222,4444, alive=0,id={0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 },\ Â payload={}, command={}, domain={}, ] **-***-**** **:**:**Â org.apache.catalina.startup.HostConfig deployDescriptor INFO: Deploying configuration descriptor session.xml **-***-**** **:**:**Â org.apache.catalina.ha.session.DeltaManager start INFO: Register manager /session to cluster element Engine with name Catalina **-***-**** **:**:**Â org.apache.catalina.ha.session.DeltaManager start INFO: Starting clustering manager at /myapp **-***-**** **:**:**Â org.apache.catalina.tribes.io.BufferPool getBufferPool INFO: Created a buffer pool with max size:104857600 bytes of type:org.apache.catalina.tribes.io.BufferPool15Impl **-***-**** **:**:**Â org.apache.catalina.ha.session.DeltaManager getAllClusterSessions WARNING: Manager [localhost#/myapp], requesting session state from\ Â org.apache.catalina.tribes.membership.MemberImpl[tcp://10.0.0.222:4444,10.0.0.222,4444, \ Â alive=0,id={0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 }, payload={}, command={}, domain={}, ]. \ Â This operation will timeout if no session state has been received within 60 seconds. **-***-**** **:**:**Â org.apache.catalina.ha.session.DeltaManager waitForSendAllSessions
If you have cluster.example.com in your DNS/hosts, you can test the cluster by browsing to cluster.example.com:80/myapp and interacting with your distributable application. If you kill one of the Tomcats, your session will not be affected. If you examine the cookies during such tests you will see the “.node1” or “.node2” at the end of the JSESSIONID cookies, which gives you a clue as to which of the Tomcats is handling your requests.
Categorised as: Coding