Maximum Wordpress Performance on an EC2 Micro Instance

This is how to get Wordpress handling a reasonable continuous load on an Amazon EC2 t1.micro instance using only standard Ubuntu packages. I'm writing down the recipe before I forget how I managed it, and because I could not find a post that specifically details how to get all of Wordpress, Apache, mod_rewrite, mod_fastcgi, php-fpm and php-apc to play nicely together. Subsets, but not all of them. There's plenty out there for nginx though. Eventually I pieced an Apache solution together from many sources, and the synthesis follows. One post in particular from Brandon's Blog on FastCGI with PHP Opcode Cache does capture the basic principle and explains the benefits.  Note: most of the benefit is from opcode caching, mod_php will be fine for most sites.

Why an EC2 micro instance? Micro instances are only $0.025 per hour. However, a stock Wordpress/mod_php install quickly runs out of memory and CPU resources at just one request per second. One user ctrl-clicking on posts in the archive for a minute, or a vulnerability scanner, or rude crawler, can DOS your site for everyone else. Without these optimisations, your remaining option is to rent a small instance at 3.8x the price of a micro. I realise the irony of going to all this effort myself and yet my own blog is hosted on Blogger.

Aside: If you are using mod_php on a micro, set MaxClients from 150 down to 50 to respect the micro's limited 593MB of system memory, because each Apache+mod_php process chews about 10MB. Even with FastCGI bringing Apache processes down to 2.5MB, keep it low because there's only one CPU core for all 50 to share. On a micro I also recommend setting MaxRequestsPerChild to 1000, KeepAliveTimeout to 5, and MaxKeepAliveRequests to 30 (about two-thirds of MaxClients).

List of ingredients:

PHP FPM works with mod_fastcgi OR mod_fcgi_proxy but NOT mod_fcgid, because the latter does its own process management exclusively, so does not support proxying to an external process manager like PHP-FPM, and FPM is necessary to get the benefit of opcode cache sharing.

Basic Setup

First install Wordpress from scratch under Apache + mod_php to a DocumentRoot of /srv/wordpress. On Ubuntu, run sudo apt-get install libapache2-mod-php5 php5-mysql php5-gd to get dependencies.

Configure Permalinks using mod_rewrite, since they're pretty and getting mod_rewrite to play nice with FastCGI is part of the trick.

WP Super Cache

Install WP Super Cache from the plugin manager, using the default "PHP" caching. WP Super Cache is a huge win for minimal effort, serving cached HTML to anonymous users without regenerating the page.

Opcode and Object Caching

Install the APC opcode cache with sudo apt-get install php-apc. Run php -r 'phpinfo();'|grep apc to check that it's enabled. APC causes PHP sub-processes to inherit a warm opcode cache, greatly conserving CPU resources.

Extract /usr/share/doc/php-apc/apc.php.gz to /srv/wordpress/apc.php and visit /apc.php to see how the cache is doing. If fragmentation is substantial then shm_size is too low. Restrict access to apc.php.

apc.shm_size = 64M in /etc/php5/conf.d/apc.ini, because the default 32MB cache is too small for Wordpress (once you open Admin). Also set PHP memory_limit to 64M. Also set apc.slam_defense = 0 to prevent piles of "Potential cache slam averted for key" in Apache's error.log. The the slam_defense setting is deprecated by the default apc.write_lock anyway.

Install the trunk version (supports WP 3.1+) of APC Object Cache Backend simply by placing object-cache.php in the wp-content directory. Simply dropping the file in enables Wordpress to cache complex PHP objects between requests and is also a huge win for little effort.

Set up mod_fastcgi with FPM

FPM takes PHP out of Apache, cutting startup memory for new Apache workers from 10MB to 2.5MB per process.

WARNING: Brandon's benchmarks found reqs/second performance is not improved by switching from mod_php to FastCGI, most of the performance gain came from opcode caching.  I now only recommend using FastCGI+FPM in case of (a) many concurrent requests and (b) serving a lot of static files (not just PHP content).  In those cases the memory saving should be substantial.

Uncomment the "multiverse" lines in /etc/apt/sources.list and run apt-get update to make libapache2-mod-fastcgi available. Run sudo apt-get install libapache2-mod-fastcgi php5-cgi php5-fpm, which also enables mod_fastcgi and starts the "php5-fpm" service. For a micro instance, set FPM max_children to 6, max_requests to 500. Immortal PHP processes been known to go crazy after a while - after a couple of days I found one hogging 100% CPU - so don't let them live forever.

Add the following lines inside your WordPress VirtualHost, adapting "/srv/wordpress" to your DocumentRoot. It pretends that there exists a PHP5 executable called "/php5.fcgi". mod_fastcgi intercepts calls to php5.fcgi and passes them to FPM instead. Also enable the "actions" module to support the "Action" line.

<IfModule !mod_php5.c>
<IfModule mod_fastcgi.c>
        Alias /php5.fcgi /srv/wordpress/php5.fcgi
        FastCgiExternalServer /srv/wordpress/php5.fcgi -host
        AddHandler php-fpm .php
        Action php-fpm /php5.fcgi

Making mod_fastcgi work with mod_rewrite

The final trick is avoiding infinite recursion between mod_rewrite and mod_fastcgi, which shows up as lots of these in error.log: the Request exceeded the limit of 10 internal redirects due to probable configuration error. Use 'LimitInternalRecursion' to increase the limit if necessary.

The solution is to add a RewriteCond %{REQUEST_URI} !^/php5.fcgi just before the final RewriteRule . index.php [L] to prevent /php5.fcgi (the handler) from being re-written to /index.php, which then needs a handler (/php5.fcgi), which is then rewritten to /index.php, ad infinitum. The complete rule block looks as follows:

RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteRule ^files/(.+) wp-includes/ms-files.php?file=$1 [L]
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteCond %{REQUEST_URI} !^/php5.fcgi
RewriteRule . index.php [L]

Now you are ready to a2enmod fastcgi && a2dismod php5 && service apache2 restart.

Try a test query. If anything goes wrong, you can immediately revert to mod_php5 by running a2enmod php5 && a2dismod fastcgi && service apache2 restart. Post a comment and I'll see if I can help.

Popular posts from this blog

A comparison of file synchronisation software

Growing large crystals of Copper (II) Sulphate