Skip to main content

Optimize Wordpress for tiny free tier VMs

The Wordpress logo
In which I show how to optimize Wordpress on Apache to handle a reasonable continuous load on a tiny free tier virtual machines, with only standard Ubuntu packages. [6 minutes]

I recently managed to do all this myself, and am writing it up as I haven't found a blog post that covers everything required to get Wordpress, Apache, mod_rewrite, mod_fastcgi, php-fpm and php-apc to play nicely together. There's plenty out there for nginx though.

Brandon's Blog on FastCGI with PHP Opcode Cache captures basic principle and explains the benefits. Opcode caching however provides most of the performance, so most sites will do fine with mod_php (no need for mod_fastcgi + php-fpm).

On free tier VMs from cloud providers, a stock Wordpress/mod_php install runs out of memory and CPU resources at just one request per second! One user opening all the archive posts or a vulnerability scanner or crawler can DOS your site for everyone else.

Basic Apache Settings

If you are using mod_php on a micro, first:
  1. Set MaxClients from 150 down to 50 (Apache 2.2), StartServers in later versions.
  2. Set MaxConnectionsPerChild to 1000
  3. Set KeepAliveTimeout to 5
  4. Set MaxKeepAliveRequests to 30 (about two-thirds of the MaxClients value)
Lowering MaxClients respects micro's limited 593MB of system memory. Each Apache+mod_php process chews about 10MB. FastCGI with FPM can bring the Apache process size down to 2.5MB, but at that point the bottleneck is having only one CPU core for all servers to share.

Software used

Required
Optional
PHP FPM works with mod_fastcgi OR mod_fcgi_proxy but does NOT work with mod_fcgid, because the latter does its own process management exclusively. FastCGI with FPM helps by sharing the opcode cache between processes... but this turns out to be a tiny improvement, so consider it optional.

Basic Optimizations

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.

Advanced micro-optimization

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) because it conserves memory better.

If you really want FastCGI+FPM, 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 127.0.0.1:9000
        AddHandler php-fpm .php
        Action php-fpm /php5.fcgi
</IfModule>
</IfModule>

Make 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.

P.S. If you're using Elastic Load Balancer, check out my post on getting Apache to log the correct client IP.

Comments

  1. Good call setting max_children to 6 and max_requests to 500 - helped me out a bunch. Thanks!

    ReplyDelete
  2. If you want to squeeze a little more performance from this setup, configure php-fpm to listen on a unix socket rather than a tcp socket, this avoids the TCP handshake delays.

    ReplyDelete

Post a Comment

Popular posts from this blog

The keys to doing long-form Narrative Improv

Here are some key ingredients for full-length improvised plays known as Narrative Improv. Providing tips on story structure, normalcy, the protagonist, consequences and clarity. [4 minutes]

How to remove smoke smell from waterproof garments

This is how I got my waterproof jacket to smell fresh without losing the waterproofing, after it was infused with campfire smoke on the Cape Point overnight trail. [2 minutes]