Entry

Making Sites Fly with Varnish

by: Kevin Cupp on: 5/16/2012

There comes a time in an ExpressionEngine site’s life when it needs to scale. The demand for its dynamically-generated pages becomes too much; you can throw more servers at the problem or tweak the ones you’ve got, but that has its own scaling issues.

Or, there’s Varnish Cache.

The idea of caching is familiar: why waste resources regenerating the same content from scratch on each page view? But Varnish takes it a bit further by stepping in before the request even makes it to ExpressionEngine or Apache (or your favorite HTTP server). Even better, it can intercept a request before it hits the disk!

To illustrate this impact, let’s send 1000 requests with 100 concurrent to EE’s default Agile Records theme, hosted on a quad-core Mac with 8GB of RAM, accessed via a local network over 802.11n:

No Caching
13.67 hits/sec

EE’s Template Caching
36.01 hits/sec

Varnish
276.55 hits/sec

With little or no caching, the CPU just cannot keep up. With Varnish, however, the only bottleneck becomes the network.

On that bombshell, if you’re wondering where to sign up, let me walk you though setting up Varnish from scratch and then configuring both Varnish and EE to set up the ideal caching for your site.

Wait, what’s going on, here?

Varnish is a proxy that sits between your HTTP server and the world. For example, when a request comes to your server for “example.com/index.php”, it first goes through Varnish who then checks to see if it has cached output for the requested URL. If it has cached the output of that specific request before, and it has not expired yet, Varnish will serve up the all ready, fully-generated output for that request from its cache stored in the RAM.

With that conceptual illustration, you can see the request doesn’t even make it to Apache, PHP or MySQL, which is great for our server’s health, no matter how optimized those processes may be. Since Varnish, its configuration, and cache can all live in the server’s RAM, this allows for lightning-fast responses and high hit rates.

Who is Varnish best for?

Sites who have minimal user interaction, therefore mostly static (mainly driven by channels, pages, etc.), are best for Varnish. Since the caching proxy’s job is to serve up the same pre-generated content to everyone, it won’t be able to show user-specific content (login-driven sites) and be as effective since that specific content will need to be regenerated on each page view. But that’s not to say it’s not possible or that it won’t help in those situations. Later, we’ll get into ways of serving dynamic content while still using Varnish to cache the static bits.

Installing Varnish

Now, it’s time to play. Varnish is one of those daemons that runs in the background on your server, much like Apache or MySQL. Your favorite flavor of Linux should provide a Varnish package which you can install with a one-liner, or you can compile from source. A Homebrew package is also available for OSX.

Like Apache, Varnish listens on a port. Since we want Varnish to listen to all HTTP requests coming to our web server, it needs to listen on port 80. This means Apache has to listen on another port; we’ll choose 8080. Or if you’re not ready to put Varnish in production, we can choose to have Varnish listen on 8080 and keep Apache on port 80 while we mess around.

You can tell Varnish which port to listen on when starting the daemon, as well as specify storage for the cache. These flags may also be set in your DAEMONOPTS configuration, depending on your OS-specific installation.

Configuring Varnish: The VCL

We need to tell Varnish when to cache, when not to cache, and how long to cache for. To do that, we write in the Varnish Configuration Language (VCL). Fun, geeky fact: Varnish translates this VCL code to C and compiles it into a small program kept in memory, for even more uber-fast request processing.

When you installed Varnish, a default VCL file was likely supplied at /etc/varnish/default.vcl if you’re on Linux. I recommend creating your own file instead of editing the default, because upgrades tend to overwrite that file, don’t learn the hard way.

Go ahead and paste this in your new VCL file:

backend default {
    .host = '127.0.0.1';  # IP address of your backend (Apache, nginx, etc.)
    .port = '8080';       # Port your backend is listening on
}

sub vcl_recv {

    # Set the URI of your system directory
    if (req.url ~ '^/system/' ||
        req.url ~ 'ACT=' ||
        req.request == 'POST')
    {
        return (pass);
    }

    unset req.http.Cookie;

    return(lookup);
}

sub vcl_fetch {

    # Our cache TTL
    set beresp.ttl = 1m;

    return(deliver);
}

That is the bare minimum we need to get EE caching, but let’s first understand what Varnish is being told to do. Notice the backend declaration at the top:

backend default {
    .host = '127.0.0.1';  # IP address of your backend (Apache, nginx, etc.)
    .port = '8080';       # Port your backend is listening on
}

Your backend is the source for which Varnish will pass requests to. If Varnish does not have a request cached, it forwards it to the backend to be generated. You can do some neat things with backend configuration, such as load balancing and health-checking, which we’ll touch on later. Since Apache is running on the same machine as our Varnish service, the host is a localhost IP address, and the port is 8080 since that’s what we’ve set Apache to listen on.

Next, notice the vcl_recv subroutine. This is called at the beginning of each request, before the backend is called. We need to make sure our control panel isn’t cached, otherwise we couldn’t interact with it and change content. To do this, we check req.url to see if it starts with /system/, our default system folder name. If that’s true, we return pass which tells Varnish to pull directly from the backend and not its cache. We also are choosing not to cache ACT or POST requests, since those likely need to hit the backend:

if (req.url ~ '^/system/' ||
    req.url ~ 'ACT=' ||
    req.request == 'POST')
{
    return (pass);
}

This is also a good place to specify sites you don’t want served out of Varnish at all. If your server runs multiple sites, but only need Varnish for some of them, exclude the site by checking for req.http.host ~ ‘example.com’.

Next, we need to make sure user-specific content isn’t cached, so we unset cookies:

# Try a lowercase 'cookie' if this gives you config errors
unset req.http.Cookie;

For example, if you’re logged into EE, and have a message on the front end of your site that says, “Hello, Admin!”, that will be cached in Varnish and then appear for all users who come to the site. So we anonymize the visit by unsetting those cookies when retrieving from the backend.

Finally, we’ll look at the vcl_fetch subroutine, which is called after a request has been retrieved from the backend. Here, we’ll just use this function to set the time-to-live (TTL) of our cache:

sub vcl_fetch {

    # Our cache TTL
    set beresp.ttl = 1m;

    return(deliver);
}

1m means one minute. This means that any single page won’t have to be regenerated more than once per minute, which should greatly ease the load on the server while making sure the content on your site is no longer than a minute old.

Now we’re ready to start Varnish. Once you find out where your varnish daemon is, start it like so:

varnishd -f /etc/varnish/main.vcl -s malloc,200M

-f specifies the path to your VCL file, and -s sets the cache storage type. I choose to store the cache in the RAM for extra speed by specifying malloc, but you can also choose to store the cache on the disk.

If all goes well, you should be able to go to your existing site like usual. To make sure caching is working, refresh a few times, then check the request headers in your browser and confirm your site’s Age is incrementing:

 

The Age shows how old the object is you are viewing, and if you keep refreshing, you’ll see the Age go back down to zero once the document becomes older than a minute. That means caching is working correctly, and you should notice a nice speed boost as well!

Now that we’ve got our site caching, let’s learn how to tune Varnish to work even better with ExpressionEngine.

Manual Purging

On some of my sites, I like to go the extreme route and set a TTL of 24h to always give the site that extra speed boost provided by Varnish. The problem with that is it may take an entire day for new changes to show up on the site, and you can imagine what clients think of that.

To fix this, we need to purge the cache when content is updated. I wrote an add-on called Purge to do just this task. It works by taking advantage of EE’s entry_submission_end and delete_entries_end hooks to know when content is updated, then it sends a special header to Varnish which we then check in the VCL, and then purge the cache if the header exists, like so:

if (req.request == 'EE_PURGE') {
    ban('req.http.host ~ example.com &&; req.url ~ ^/.*$');
    error 200 'Purged';
}

The Purge add-on could use some improvement, such as purging on comment submission (if you’re not pulling those in dynamically, as we’ll discuss below) and better integration with the Multiple Site Manager, so pull requests are welcome.

Showing Dynamic/User-Specific Content

Earlier I mentioned a block on our site that greets the user if they’re logged in, but we intentionally broke it to make sure the wrong name wouldn’t appear for other users.

 

We want to get this working again to greet the user and hide the “Log in” and “Register” links. To do that, we take advantage of Varnish’s Edge Side Includes feature. In a nutshell, ESI allows you to serve bits of content from from the backend without having to serve the whole document from the backend. That’s what we’ll do for our greeting box.

First, we need to make the greeting box its own template so that Varnish has something to request without loading the rest of the page. Now we’ll embed the template on our page, but we won’t use EE’s embed tag, we’ll use Varnish’s ESI tag:

<esi:include src='/index.php/global_embeds/member_box'/>

We’re not quite finished yet. We need to tell Varnish to process ESI tags, and NOT to cache that template request. We do this by adding req.url ~ ‘member_box’ to the check in vcl_recv, and by adding set beresp.do_esi = true; to vcl_fetch.

Now we’ll restart Varnish, and go to our homepage. If we’re logged into EE, we should see this on the homepage:

 

Great! But a problem with this is it really lowered our hitrate. We still have the benefits of caching the rest of the page, but hitting the backend on each request creates a bottleneck. Since we don’t need to have our greeting box hit the backend for anonymous users since it always appears the same for guests, let’s continue to cache everything for anonymous users, and only have our greeting box hit the backend for logged-in users.

To do that, modify your check for req.url ~ ‘member_box’ to read as (req.url ~ ‘member_box’ &&; req.http.Cookie ~ ‘exp_sessionid’). Varnish lets us check for individual cookies so we can decide how to best handle the request. With this change, requests for our member_box template will only hit the backend if a sessionid cookie is set. You can also alter the if-statement to allow ALL requests to hit the backend if that cookie is set, that way if you’re logged into your site to make changes, you can see the changes live without having to purge the cache or wait for it to expire.

There’s certainly more you can do to handle logged-in users, and Varnish has more documentation about that.

Get IP Logging Working Again

Since running Varnish, you may notice in your server and EE logs that everyone’s IP address appears as 127.0.0.1 or the IP address of your Varnish server. That’s because, technically, Varnish is the one making the request to the backend. Luckily we can tell Varnish to forward along the user’s actual IP address to us.

In your vcl_recv subroutine, add these lines:

remove req.http.X-Forwarded-For;
set req.http.X-Forwarded-For = client.ip;

Now open your EE install’s config.php and add your Varnish server’s IP address to the proxy_ips setting:

$config['proxy_ips'] = '127.0.0.1';

This should get ExpressionEngine seeing the correct IP address. Apache, however, requires further tweaking. In your httpd.conf, modify your LogFormat to read %{X-Forwarded-for} in the IP address portion of the log line. Other HTTP servers should provide a similar way of showing our special header in log files.

By the Grace of Varnish

We’ve all lost precious uptime when our HTTP service decides to crash. Varnish has some nifty tools in place to cover your tail in such an event, and it’s called backend polling. The concept is simple: Varnish will poll your backend at an interval you specify, and if it detects the backend is unreachable, it will continue to serve out of the cache for a specified period of time, called grace time.

Setting up polling is easy, we do it by adding a probe section to our backend declaration:

backend default {
    .host = '127.0.0.1';
    .port = '8080';
    .probe = { 
        .url = '/';
        .timeout = 34ms; 
        .interval = 1s; 
        .window = 10;
        .threshold = 8;
    }
}

These are the default settings from Varnish’s docs, but you may want to tweak them further for your server. This basically says, “Go to http://127.0.0.1:8080/ every second, and if it takes less than 34ms to respond for at least 8 of the last 10 polls, the backend is considered healthy.”

If the backend fails the test, objects are served out of the cache in accordance to their grace time setting. To set this, we need to set the grace time both for the request and for the fetched object. To set grace time for the request, add this line to vcl_recv:

set req.grace = 1h;

And to set grace time on the object, add this line to vcl_fetch:

set beresp.grace = 1h;

This allows our backend to be down for a whole hour before we get it fixed without website visitors ever noticing.

Go Forth

I hope by now that I’ve given you an arsenal of tips and tricks necessary to set up the ideal caching for your ExpressionEngine site, and that traffic spikes are no longer a cause of anxiety. If you need Varnish to do something I didn’t cover here, it’s likely outlined in the Varnish docs, along with more detailed descriptions of everything I mentioned here. This was just the tip of the Varnish iceberg.

For reference, you’ll find the final VCL file with all the modifications we made below.

backend default {
    .host = '127.0.0.1';  # IP address of your backend (Apache, nginx, etc.)
    .port = '8080';       # Port your backend is listening on
    .probe = { 
        .url = '/';
        .timeout = 34ms; 
        .interval = 1s; 
        .window = 10;
        .threshold = 8;
    }
}

sub vcl_recv {

    # Forward client's IP to backend
    remove req.http.X-Forwarded-For;
    set req.http.X-Forwarded-For = client.ip;

    # Set the URI of your system directory
    if (req.url ~ '^/system/' ||
        req.url ~ 'ACT=' ||
        req.request == 'POST' ||
        (req.url ~ 'member_box' && req.http.Cookie ~ 'exp_sessionid'))
    {
        return (pass);
    }

    unset req.http.Cookie;

    set req.grace = 1h;

    return(lookup);
}

sub vcl_fetch {

    # Enable ESI includes
    set beresp.do_esi = true;

    # Our cache TTL
    set beresp.ttl = 1m;

    set beresp.grace = 1h;

    return(deliver);
}

.(JavaScript must be enabled to view this email address) or share your feedback on this entry with @ellislab on Twitter.

.(JavaScript must be enabled to view this email address)

ExpressionEngine News!

#eecms, #events, #releases