In the previous article in this series, we looked at using Apache with mod_pagespeed to perform on-the-fly enhancements to decrease page load times. Getting an optimised page is only half the battle however; we need to ensure that our backend is doing as little work as possible in order to be highly scalable. In this article, we look at how we can achieve this while improving performance – all with nginx.

What is nginx?

For those unfamiliar with nginx, it describes itself as “a free, open-source, high-performance HTTP server and reverse proxy”, first released publicly in 2004, and becoming one of the most successful webservers in the past few years – their wiki has an impressive list of sites powered by it. The biggest advantage it has over the tried-and-tested Apache webserver is that it uses an event-driven, asynchronous architecture – so it doesn’t rely on threads to handle requests. What does this mean in real terms? Well, apart from predictable increases in memory usage under load, the key point for us at the moment is that it can serve static content fast. Really fast.

Many PHP installations use Apache, which is a well known and robust solution for serving PHP, but it can be a little heavy for very high volumes of traffic. Most tutorials on configuring Apache for PHP rely on mod_php, a tried and trusted implementation. But there are other options: nginx uses PHP Fast Process Manager (PHP-FPM), an alternative fastcgi engine which has numerous advantages in both logging and adaptive process spawning. Effectively, PHP-FPM gives the developer far more control and performance over PHP than mod_php does, while nginx means this speed increase applies not just to dynamic responses but to static assets too.

Sailing up the river

It’s common practice – certainly within nginx – to refer to your servers further back as “upstream”, but when thinking of your application, I like to expand the metaphor.

Picture your application as a river. Along the banks, there are docks, and each dock serves a different purpose. Manpower – your dock workers, or application resources – are finite, and each dock can only load and unload so many boats per hour.

Imagine a stream of boats coming up from the sea; the majority just want water, some need fresh food, and the very worst need repairs. Moving further upstream than necessary wastes fuel, and upsets the crew as the river gets narrower and more congested.

If you could design your docks, you would have the largest dock at the mouth of the river, provisioning the majority of the ships with water so they didn’t have to sail upstream. You would have your food (the next most required resource, and taking longer to provide) further upstream, with less workers because you had already turned around a lot of traffic by handling the most simple requests. Only the most needy ships, badly in need of repair, would venture deep into the interior to be fixed far upstream, where your most valuable workers would sit awaiting damaged vessels – the lengthiest operation.

Using this metaphor, we can see there is value in dealing with as many requests at the edge as possible; if you extend these principles to apply to your web application, then nginx can help a great deal.

Install and Configure Nginx

Our users, sailing through our application, are an odd bunch of naval folk. If we think of the average dynamic Web page, we need only one dynamic bit of content: the HTML itself. The rest – the CSS files, the images, the JavaScript, fonts and so on – all of these are static.
Sure, we may pull in the odd bit of data via AJAX, but in the main, our site is dealing with dozens of requests for static content for each request that requires your application to run. Why put the onus of handling those simple requests on the same server that’s dealing
with dynamic content? Why pollute our caches (or use up bandwidth allowances) fetching a file that can be fetched easily from the file system?

First we will set up an instance of nginx to help us provide a lightweight front end. First, let’s get hold of a version of nginx that supports all the things we want to make it responsible for. The default distros in CentOS and Ubuntu are fine for normal usage (and you will see benefit), but nginx doesn’t support the pluggable module system of Apache; you will need to compile nginx from source with more modules enabled if you need more than is supplied by default.

To begin with we will use the default versions available in either distro, so let’s get them installed. I’m using a Vagrant box with Ubuntu 13.04 for this, so my commands look something like this:

Now let’s test nginx is installed correctly:

OK, nginx is installed, but what modules was it compiled with? Well, if we substitute the lower case v for upper case -V:

As you can see, there’s quite a few default modules in the default package. Starting nginx is trivial; we can either use nginx directly, or start it as a service:

Don’t worry if you already have a webserver such as Apache running and you receive warnings about not being able to bind to the socket – this is because nginx is trying to bind to port 80, which the other webserver is already bound to. You can either stop Apache or change the port nginx is listening on. We’ll explain how to change the port in the nginx configuration shortly.

Testing that we have everything up and running is as simple as making an HTTP request with cURL:

which outputs the default nginx index.html:

Welcome to nginx!

If you see this page, the nginx web server is successfully installed and working. Further configuration is required.

For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.

Thank you for using nginx.

This is fine, but really we want to test in a browser on the host machine. A quick fix to make testing more natural is to set up port forwarding. Assuming you are using VirtualBox and Vagrant together on a Mac, we can set up port forwarding with relative ease. My test VM is callecd “ubuntu-nginx”, and from my Mac terminal I just type:

You should now be able to see the nginx default page by visiting http://localhost:8080 in your preferred browser. A further (optional) step for Mac users if you prefer not to specify the port on each request is to temporarily change your Mac firewall to port forward requests to port 80 to port 8080. Here’s the command to do that:

Now you should be able to see the test page using http://localhost without the port.

Benchmarking Nginx With Siege

At this point we’ve gone through quite a few steps just to get a minimal install, and some may be wondering: what advantage does all this work give us? To answer that question, we will now do some benchmarking. For this, we are going to turn to a very useful tool indeed: Siege.

Siege describes itself as “an http load tester and benchmarking utility”. It’s a very useful tool, and, provided you have a Mac with Homebrew, installation is trivial:

Now let’s get some simple benchmarks for our nginx instance. Assuming we are using Vagrant with port forwarding and that traffic to port 8080 is routed to port 80 on the instance, we can send some traffic with this command:

This tells Siege to throw 25 concurrent users 100 times each at the URL specified – a total of 2,500 requests. Siege may return some output like this:

As you can see, we’re completing these transactions in just under a minute. Not bad at all! Get much above these numbers though, and you will start to see nginx flag. We can do better.

To get more out of nginx, we need to give it more workers; a good rule of thumb is generally one worker per core available. I’ve given my VM more cores to use, so now I’ll tell nginx to use them by editing /etc/nginx/nginx.conf:

Note that in later versions of nginx this isn’t strictly necessary as nginx works out the optimum number of worker processes for you. Now restart nginx:

The -s (for signal) tells nginx to accept one of a selection of signals: stop, quit, reload, and reopen – equivalent to SIGTER, SIGQUIT, SIGUSR1 and SIGHUP respectively. Next I’ll run siege again, but with much, much higher load, to give you an idea of what you can expect from nginx:

Yes, that’s 100 concurrent users – a staggering number of simultaneous connections for one server, even if this was a production site rather than a sandbox. And my output:

As you can see, although there are some failed connections, nginx is performing admirably, considering this is a Vagrant box allocated a mere 512MB of memory and four cores. If you have trouble reaching these numbers, try tweaking the nginx.conf file http {} section with some of these configuration options and see if it makes a difference. Be warned however; these very much fall under the “your mileage may vary” list of tweaks.

Adding PHP to Nginx

Nginx is now set up, but serving the 384 bytes of the default index page isn’t what we want to do. It’s time to configure nginx to do something more useful.

Similar to apache’s configuration file, nginx can either describe server hosts directly in the main configuration file or include a directory. Depending on your distribution, it may have the default host in the main nginx.conf file or include them from a separate directory. If your distro has the default server {} block embedded, let’s start by commenting it out or removing the entire server {} block within the http {} section of our nginx.conf file, and replacing it with:

and then ensure that the directories we will be using exist and we have no hosts:

Next we should flush out any symlinked hosts that are already enabled:

Now let’s set up a very small fresh server host:

And add in the smallest amount we need to get it working:

And then finally symlink it into position:

Now we can restart restart nginx:

Finally, we can test that we get the page served:

This output shows us that nginx is setup, but there’s no PHP interpreter associated with it: if you try to retrieve the front page in a browser, it will try and download rather than serve the page. This is because PHP isn’t setting the MIME type, and therefore nginx will use the default which is application/octet-stream.

To get the PHP rendering as expected from nginx, we will set up the PHP Fast Process Manager (PHP-FPM). However, Ubuntu’s version of PHP is a little old for us; PHP 5.3 is end of life and we want to keep up to date. Assuming your Vagrant instance is Ubuntu, we can take advantage of the PPAs maintained by Ondřej Surý:

After installation, let’s start FPM:

Now, let’s set up nginx to use FPM for PHP. First, we’ll use my preferred way of describing external dependencies within nginx: the upstream directive.

Here we tell nginx that it should use the FPM Unix socket whenever we refer to the upstream "_fastcgi". Note that this lives outside the server{} block as it is a http{} scope directive. Now let’s modify our main configuration and reference the upstream within the server{} block:

Now let’s reload nginx again:

When we visit the page again in our browser: Success! And, as an added bonus, let’s look at the banner of our page:

php-banner

We’re running PHP 5.5.8 with all the performance gains associated with it.

This example showed a fairly basic fastcgi setup. Let’s walk through the changes and some of the concepts being used here. The location directive is used to conditionally match against requested URLs. The location / matches all URLs, so this rule will be applied to all requests first, immediately after any server level directives. Within it we use the try_files directive to tell nginx to attempt to serve the file if it exists in the root, and to fall back to @fastcgi if it cannot find it. @fastcgi is what is called a named location, which means that it is an internal locale that only nginx can access. Within this location we put our main fastcgi configuration, passing the script filename that the user attempted to access. This is useful for clean URL mapping or when 404s are dealt with by the PHP application.

However, index.php does exist in this case, so the initial check to test if the file is readable passes and the @fastcgi{} block is not used if a user visits http://localhost/index.php. To deal with this, a separate (near-identical) block captures any URI ending in .php:

Note how we can nest a location within a location: in this case, to provide some security, we return the 403 Unauthorized header for PHP files that don’t exist. Taking the example from the nginx wiki, if a request is made for /forum/avatar/1232.jpg/file.php which does not exist, but if /forum/avatar/1232.jpg does, the PHP interpreter will process /forum/avatar/1232.jpg instead. If this contains embedded PHP code, this code would be executed. This nested location mitigates against the danger from user-generated uploads attempting this.

Nginx In The Real World

While we’ve looked at a basic example, this is far from a production-ready fastcgi implementation. Let’s look at updating this and using this with something more relevant such as the Symfony 2 skeleton application:

As we are port forwarding to our VM, we should also comment out the section within /var/www/symfony2-skeleton/example/web/app_dev.php that checks the host header:

And now onto setting up fastcgi in a better way. For inspiration I’m using the fabulous Nginx configuration for Drupal created by Perusio and altering it for more generic usage:

Once we reload the browser, we should see the Symfony skeleton application. There’s an awful lot of things we can do to improve performance using nginx and we’ve barely scratched the surface. In the next article in this series, we’ll be looking at how to setup nginx for HTTPS traffic – the right way.