In the following article, I’ll demonstrate how we can use Apache HTTPD to load balance across two Apache Tomcat instances. Whilst in this example the Apache HTTPD load balancer is a single point of failure, we could implement (although outside the scope of this article) a failover HTTPD instance clustered using one of the many available clustering stacks (RHCS, or something more lightweight like keepalived). Then, we’d have a highly-available load balancer.
There are a few ways we can use HTTPD to load balance, via the use of loadable modules. mod_proxy_http is the simplest, and can be used to load balance any service that “speaks” plain HTTP. mod_proxy_ajp is a simple AJP balancer module, with a slight performance increase of mod_proxy_http. However, mod_proxy_ajp is purported to be rather buggy when compared with other similar modules. Hence, we’ll use mod_jk. This is a very active module developed alongside Tomcat, and in many years of working with it I’ve never experienced a major bug or a configuration requirement it couldn’t handle.
I’ll use two servers for this, both running CentOS 6.3 x86_64:
|
1 2 |
172.16.18.169 dolan www1.tokiwinter.com 172.16.18.172 gooby www2.tokiwinter.com |
I have already built httpd-2.4.3 from source, installed to /usr/local/httpd, configuration at /etc/httpd and logs at /var/log/httpd. apache-tomcat-7.0.35 has been installed to /usr/local/tomcat7; configuration and logs have not been placed elsewhere. You should refer to your setup when I mention either editing HTTPD and Tomcat configuration files or perusing appropriate logs. Only you will know where these things live on your system. Therefore, substitute any paths where required during the course of this article.
Building mod_jk
mod_jk doesn’t come bundled with the HTTPD distribution, so you’ll need to download it from the link given in the Introduction to this article and build it from source.
I downloaded tomcat-connectors-1.2.37-src.tar.gz to /usr/local/src and extracted as follows:
|
1 2 3 |
# cd /usr/local/src # tar xzf tomcat-connectors-1.2.37-src.tar.gz # cd tomcat-connectors-1.2.37 |
Next, run configure - ensuring that the correct path to apxs is specified for your configuration.
|
1 |
# ./configure --with-apxs=/usr/local/httpd/bin/apxs |
If no errors are reported, continue with compilation and installation of the module:
|
1 2 |
# make # make install |
Check that the module has indeed been installed:
|
1 |
# ls -l /usr/local/httpd/modules/mod_jk.so |
Configuration
First, configure HTTPD. I prefer to keep all of my various pieces of major configuration (mod_jk, SSL, VirtualHosts, etc.) in different files (or even directories for VirtualHosts) - so at the bottom of /etc/httpd/httpd.conf (path changed as per your installation) add:
|
1 |
Include /etc/httpd/conf.d/mod_jk.conf |
Create and edit /etc/httpd/conf.d/mod_jk.conf as follows:
|
1 2 3 4 5 6 7 8 9 |
LoadModule jk_module modules/mod_jk.so JkWorkersFile /etc/httpd/conf/workers.properties JkLogFile /var/log/httpd/mod_jk.log JkLogLevel debug JkLogStampFormat "[%a %b %d %H:%M:%S %Y]" JkOptions +ForwardKeySize +ForwardURICompat -ForwardDirectories JkRequestLogFormat "%w %V %T" JkMount /* loadbalancer JkShmFile /var/log/httpd/mod_jk.shm |
A few points to note here. Most parameters (log formats and so on) are sensible defaults taken from the mod_jk documentation. Ensure that the path to JkWorkersFile is correct for your site, as well as JkLogFile. Once you’ve verified that everything is working correctly you should definitely change JkLogLevel to be something other than debug, else you’ll create very voluminous logs. The real work happens with JkMount - here we route all requests to loadbalancer - which is the load balancer we will soon define in workers.properties. Using JkMount, you can configure HTTPD to only route specific requests through to the backend application servers, whilst leaving static content to be served by HTTPD, for example. JkShmFile is the path to the shared memory file used by mod_jk - it is important that you remember to define this.
Next, create and edit /etc/httpd/conf.d/workers.properties as follows:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
worker.list=loadbalancer worker.node1.port=8009 worker.node1.host=www1.tokiwinter.com worker.node1.type=ajp13 worker.node2.jvm_route=node1 worker.node2.port=8009 worker.node2.host=www2.tokiwinter.com worker.node2.type=ajp13 worker.node2.jvm_route=node2 worker.loadbalancer.type=lb worker.loadbalancer.balance_workers=node1,node2 worker.loadbalancer.sticky_session=TRUE |
First, we define our worker.list as loadbalancer - this will make sense towards the end of the file. Next, two workers are defined in the general format worker.<workername>.<parameter>. Hence, our two workers are named node1 and node2. If you use a non-standard AJP port, make sure that you set the correct values here in worker.<workername>.port. worker.<workername>.host should contain the hostname that this worker will service. worker.<workername>.type should be set to the type of worker you wish to run - in this case we select ajp13 - AJP 1.3. Lastly in the worker definitions, and superfluously, I set the value of worker.<workername>.jvm_route. By default, the jvm_route is the same as the worker name - but this explicit declaration matches up nicely with our Tomcat configuration, and is this author’s preference. You don’t need to follow it.
Finally, worker.loadbalancer.type=lb says that the loadbalancer worker is a load balancer (lb) and worker.loadbalancer.balance_workers is set to node1 and node2 - the names of our two defined workers. The worker.list property at the top of the file, set to loadbalancer, will then check worker.loadbalancer.balance_workers to get the list of workers - in our case, node1 and node2. worker.loadbalancer.sticky_session is very important. Set to 0 or FALSE, requests will be round-robin load-balanced across the two backend nodes. Set to 1 or TRUE, the requests will use session-based persistence, provided that the Tomcat instances are correctly configured.
There are many other parameters that can be set in both the HTTPD mod_jk.conf file, as well as workers.properties, including load balancer weighting/preference, load balancing algorithm, failed worker check times (default 60 seconds before a worker is rechecked, and reconnected if possible), cache sizes, and so on - you should consult the appropriate documentation for a thorough list and explanation.
Once HTTPD is configured, update your Tomcat server.xml files, replacing
|
1 |
<Engine name="Catalina" defaultHost="localhost"> |
with
|
1 |
<Engine name="Catalina" defaultHost="localhost" jvmRoute="<worker>"> |
Ensure that the jvmRoute property matches the worker names defined in your workers.properties file (also, in our case, the worker.<workername>.jvm_route options). If this isn’t done, the load balancer will not work as expected.
Restart and Testing
Now, restart your Tomcat instances, followed by your HTTPD instances. Access your web application via a browser, and check the Tomcat access logs. You will see the requests hitting a single node as expected. Shut that node down, and you’ll see messages such as the following in the mod_jk log file:
|
1 2 3 4 5 |
[Sat Feb 09 05:20:37 2013][6766:140248760825664] [info] ajp_connect_to_endpoint::jk_ajp_common.c (995): Failed opening socket to (172.16.18.172:8009) (errno=113) [Sat Feb 09 05:20:37 2013][6766:140248760825664] [error] ajp_send_request::jk_ajp_common.c (1630): (node2) connecting to backend failed. Tomcat is probably not started or is listening on the wrong port (errno=113) [Sat Feb 09 05:20:37 2013][6766:140248760825664] [info] ajp_service::jk_ajp_common.c (2623): (node2) sending request to tomcat failed (recoverable), because of error during request sending (attempt=2) [Sat Feb 09 05:20:37 2013][6766:140248760825664] [error] ajp_service::jk_ajp_common.c (2643): (node2) connecting to tomcat failed. [Sat Feb 09 05:20:37 2013][6766:140248760825664] [info] service::jk_lb_worker.c (1478): service failed, worker node2 is in error state |
You should then see failover occur to the second node. This is an active/active setup - so under normal operation each user session will be allocated to a worker depending upon your configuration in workers.properties. Our configuration is simple so the requests will be sent to each server in turn (presuming you send multiple sessions to the load balancer), each session “sticking” to the server it hits first - if you set up weighting/loading this will be different.
Conclusion
This has been a slightly terse article on the configuration of a simple Apache HTTPD load balancer in front of two Tomcat instances, including session persistence.
Apache HTTPD is a highly configurable piece of software, as are its modules. Review the HTTPD and mod_jk documentation should you wish to understand the examples given in more depth, or have a desire to delve deeper.