During the time when we were building the server environment for a new version of one of our community pages, we tested many different server applications and architectures. One thing which brought us a big speed and efficiency improvement was the combination of Nginx, Memcached and PHP-FPM which we are using and which I’m going to line out here.

First I’ll need to introduce the three main components a little.

  • Nginx is an extremely efficient high performance webserver. Actually its not only a webserver, but I call it that since its other functionalities are crap (subjective opinion). For serving HTTP it provides many things like very advanced URL rewriting and content filtering that includes subrequests to HTTP, FastCGI, Memcache and other backends.
    nginx-logo
  • Memcached is a popular and well known small, fast and simple key-value cache. Memcached
  • FPM is a patch for PHP and stands for FastCGI Process Manager. As the name says it manages processes to serve FastCGI. We first tried to use the spawn-fcgi process manager that comes with the Lighttpd package, but FPM proved to be more reliable and it has some nice features which spawn-fcgi doesn’t provide. PHP-FPM

In our setup we use Nginx as webserver in front of the environment and first entry point for user requests. For each request to non-static files it creates a FastCGI request and forwards it back to the right PHP Cluster.

Caching makes more sense on static content, so we make it static

On our site we serve a community in which each user has a profile that can be seen by other users. Those profiles are probably the most accessed part of the page, so it makes sense to think twice about how to cache them. Big parts of the profiles are more or less static, the users usually don’t change their profile data every day. So first we had to extract all the more dynamic parts, like the guestbook or the users recent forum posts from the profile by loading them via ajax. Now that we load those things on a separate request, we can start treating the profiles as almost static pages.

First steps with the new environment

In the first few months when we started serving the new version to users as a beta, all the requests to profiles arrived on the PHP cluster as FastCGI requests. To serve a request, PHP loaded the whole framework, checked if the profile was already on Memcache, and if not it generated the profile and stored it on the Memcache cluster. If the same profile got requested again, the FastCGI request went back to the PHP again and the PHP loaded the whole framework again only to check the Memcache.

blog nginx-memcache pic1

Improvement

If a profile got stored on the Memcache by PHP already, it doesn’t make much sense to load the whole, in our case really heavy, PHP framework again just to decide that it doesn’t need to regenerate the profile but read it from the cache instead. Luckily Nginx provides a really nice functionality to do subrequests to Memcache directly.

Probably you can already more or less imagine what we did. And now in detail:

Everytime when a request arrives at the Nginx, it first checks if this is a request for a static file or another subsystem like for example the forum. If not, the Nginx itself checks on the Memcache cluster if a key exists which it generates out of the URI. If it gets a hit, it totally bypasses the PHP by serving the request directly from Memcache.

To implement this, of course we needed to make sure that nothing else might accidentally store a key into the Memcache that might get hit by the Nginx and make it serve nonsense. Maybe it sounds stupid to mention this because its obvious, but honestly it actually happened.

blog nginx-memcache pic2

Enough theory

Thats where the magic happens:

1  location /{
2     if ($request_method != GET)
3     {
4       rewrite . @fallback last;
5     }
6     default_type    text/html;
7     add_header      "Content" "text/html; charset=utf8";
8     charset         utf-8;
9     set             $memcached_key nginx_prefix$uri;
10    memcached_pass  profilememcache;
11    error_page      500 404 405 = @fallback;
12 }
13 location @fallback {
15    /* pass to FastCGI */
16 }

That snippet is a part of our Nginx configuration. On top of that snippet all static requests or requests to other subsystems got catched aleady. So we can be sure that requests which reach the location / are dynamic and belong to the main system which is our PHP framework.

The location @fallback is where the requests have to go if they want to reach the PHP.

On line 2 all requests which are not of method GET get catched, of course we don’t want to have application logic on the Nginx, so they get rewritten to be handled by the @fallback location and sent to PHP.

Lines 6-8 are defining the encoding and content type. Since we don’t store any HTTP headers on the Memcache, but only the HTML output itself, the Nginx needs to know about those things on its own.

On line 9 the variable $memcached_key gets set. This variable name is defined by the Nginx Memcache module, and the variables value will be the key which gets retrieved from the Memcache backend.

Finally, on line 10 Nginx does the request to the Memcache backend. If you now think “How does it know how to reach this profilememcache?”. Profilememcache is the name of an upstream which I defined somewhere above in the config and it includes the IP and port of the entry point of the Memcache cluster.

Another really nifty things happens on Line 11. The Parameter error_page defines what the Nginx has to do in case of a certain HTTP error. If the request on line 10 doesn’t succeed and gets a miss back from Memcache, the Nginx will raise a 404 error. Thanks to line 11, the 404 error will let the Nginx fall back to the location @fallback where the request gets sent to FastCGI, and now you know where the second location got its name from.

Does that work?

Yes, it works like hell. We saved our PHP framework cluster from handling hundreds of thousands of requests to which the Memcache already knew the answer. This significantly lowered the machine load, which made the cluster suck less power and saved the pandas.