Apache mod_proxy balancing with PHP sticky sessions


Note : This page may contain outdated information and/or broken links; some of the formatting may be mangled due to the many different code-bases this site has been through in over 20 years; my opinions may have changed etc. etc.

I’ve been investigating the new improved mod_proxy in Apache 2.2.x for use in our new production environment, and in particular the built-in load balancing support. It was always possible to build a load-balanced proxy server with Apache before, using some mod_rewrite voodoo, but having a whole set of directives that do all the hard work for you is a great feature. There is however, a catch. It won’t work out of the box with PHP sessions, or many other applications. I’ve since worked out a way around this which enables you to continue using all the great features mod_proxy_balancer offers and still bind requests to an originating server. All you need is a little mod_rewrite magic : Read on for more details…

If you’re going to use mod_proxy to build a reverse proxy (caching or otherwise) in front of your backend application servers, you’re probably going to want to use sticky sessions so that a user of your site always gets "bound" to the server that the session originated on. Otherwise, you’ll get people randomly getting logged out and all manner of other nastiness. According to the docs, you can use the "stickysession" key in your configuration :

The value is usually set to something like JSESSIONID or PHPSESSIONID, and it depends on the backend application server that support sessions.

Sounds great, but it doesn’t actually work that way with PHP. If you set your configuration to something like this :

ProxyPass / balancer://cluster/ lbmethod=byrequests \
stickysession=PHPSESSID failover=Off

You’ll find that regardless of whether a session exists or not, you’ll still end up randomly pinging between the backend servers, which is definitely NOT what you want to happen. So why is this happening ? Well, it turns out that the Apache documentation is actually a little misleading. If you look at the relevant code in modules/proxy/mod_proxy_balancer.c, lines 195 to 210, you’ll see it’s actually looking for a session ID, then a period (.) character and a route. Tomcat can be configured to do this, but PHP can’t; PHP sessions can only be alphanumeric and don’t specify an originating server or "route". So, as a workaround, you can create your own cookie and use that instead when binding requests to a backend server. It does however mean that any request gets bound to a server,not just when sessions start but this shouldn’t be too much of an issue, particularly if you are caching static content as well. An easy way to do this on recent versions of Apache is to use mod_rewrite to set the cookie for you (this feature was only added in later versions of 2.0.x and up - I recommend running 2.2.x to be certain all this will work). Say you have 2 backend servers, www1.example.com and www2.example.com. You’d add the following to your backend vhost configuation :

RewriteEngine On
RewriteRule .* - [CO=BALANCEID:balancer.www1:.example.com]

And then do the same for www2, but obviously changing the cookie value to reflect this. You then need to tell your frontend proxy that it should look for this cookie, and which server each "route" refers to :

ProxyPass / balancer://cluster/ lbmethod=byrequests stickysession=BALANCEID
ProxyPassReverse / balancer://cluster/
<Proxy balancer://cluster>
BalancerMember http://www1.example.com route=www1
BalancerMember http://www2.example.com route=www2

And then give it a kick. What you then should see (if you switch LogLevel to debug) is the following the first time a request comes in :

proxy: BALANCER: Found value (null) for stickysession BALANCEID
proxy: Entering byrequests for BALANCER (balancer://cluster)

So it doesn’t have a preferred route, and it switches to the default load balancing algorithm (byrequests) to get a random server. Next time, the cookie will have been set, and you’ll see :

proxy: BALANCER: Found value balancer.www1 for stickysession BALANCEID
proxy: BALANCER: Found route www1
proxy: BALANCER (balancer://cluster) worker (http://www1.example.com)
rewritten to http://www1.example.com/

And you should then be good to go. Each new request that comes in will be directed to a backend server according to your load-balancing method, and any subsequent requests from that user (assuming they have cookies enabled) will then go back to the same backend server. When they close their browser and the cookie expires, the "binding" is reset and they’ll get a new random server next time they connect.