High performance WordPress in 512MB

I’ve been experimenting with migrating from a 2 gig VPS server to a much smaller 512MB server for hosting this blog and a dozen or so other small sites. I have had great experiences with Linode over the years, but wanted to try Digital Ocean as they have the small inexpensive servers which seem perfect for hosting personal sites like this (those links are referral codes; if you use them I get a small credit on my hosting, thanks).

Initially, just setting up an Ubuntu server with standard LAMP configuration on Digital Ocean runs as expected and WordPress is simple to install. The only trick is, when you’re done with the default configuration, you have a server that will run out of memory when faced with anything but a few simultaneous users. And it’s slow. So let’s see if we can fix this.

How to get to 25 users per second

My goal was 25 users per second without a performance hit. And I wanted fast page loads. Starting with the above default configuration, here are the steps I used.

Apache2 comes with modphp installed by default which works perfectly well for serving php scripts like WordPress. The downside is even if a request is for an image or a .css file, Apache still has to keep PHP loaded for even serving those simple requests. On an average blog, maybe 1 out of 10 requests will require PHP so separating PHP from Apache2 is a good way to save some memory.

With a default config, if we hit the site with 25 users per second (using loader.io) it proves we have some work to do:

loader-io-before

That’s an average load time of almost 7 seconds, with about 20% of the users getting a timeout error.

PHP5-FPM to the rescue

Configuring Apache to use FPM to process PHP isn’t too hard. I found this walk thru very helpful.

These commands update your repos to use a more recent Apache server which makes it possible to connect over a unix socket instead of having to use a TCP socket (slightly faster I understand):

sudo add-apt-repository -y ppa:ondrej/apache2
sudo add-apt-repository -y ppa:ondrej/php5-5.6
sudo apt-get update

Install newer Apache and PHP-FPM:

sudo apt-get install -y apache2
sudo apt-get install -y php5-fpm php5-mcrypt \ 
                        php5-mysql php5-gd php5-curl
sudo service php5-fpm status

The relevant FPM config file is found in /etc/php5/fpm/pool.d/www.conf . You’ll want to check it out to ensure listen is set to use the unix socket.

I also was able to turn up the number of servers from default to handle larger loads:

listen = /var/run/php5-fpm.sock
pm = dynamic
pm.max_children = 8
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

Ensure the right mods are turned on and off in Apache:

sudo a2enmod proxy proxy_fcgi mpm_event
sudo a2dismod php5

I made a new configuration file for Apache to ensure it points php files to the unix socket where php5-fpm will be listening system wide so I didn’t have to do it for each blog separately. Use your favorite editor to create /etc/apache2/conf-enabled/fcgi.conf to read:

<FilesMatch \.php$>
  # 2.4.10+ can proxy to unix socket
  SetHandler "proxy:unix:/var/run/php5-fpm.sock|fcgi://localhost/"
  # Else we can just use a tcp socket:
  #SetHandler "proxy:fcgi://127.0.0.1:9000"
</FilesMatch>

With mpm_event turned on, here are the low memory configuration options I used for it. Event handling is pretty lightweight with Apache2 without PHP installed so I found I could run a lot of servers without using much RAM:

<IfModule mpm_event_module>
 StartServers 2
 MinSpareThreads 25
 MaxSpareThreads 75
 ThreadLimit 64
 ThreadsPerChild 25
 MaxRequestWorkers 150
 MaxConnectionsPerChild 0
</IfModule>

Restart apache and it should begin executing PHP files with php and your new event handler:

sudo service apache2 restart

With those changes in line, ps_mem.py reports apache2 server using only 65 MB of RAM even under load running for 24 hours.

Now, let’s tune MySQL

You can probably find a lot of people more knowledgable than me when it comes to tuning MySQL. In it’s default config, it consumes more memory than our small server can handle. Here are my settings which I derived by playing with this MySQL Memory Calculator and tuning it until it fit into the space I had available.

/etc/mysql/my.cnf:

#
# * Fine Tuning
#
key_buffer = 32M
tmp_table_size = 32M
max_allowed_packet = 16M
thread_stack = 192K
thread_cache_size = 2

innodb_buffer_pool_size = 8M
innodb_buffer_pool_size = 8M

# This replaces the startup script and checks MyISAM tables if needed
# the first time they are touched
myisam-recover = BACKUP
max_connections = 50

# * Query Cache Configuration
#
query_cache_limit = 2M
query_cache_size = 16M

I might be too conservative here as MySQL is using less than 50 MB of RAM now so I probably have room to turn up some caching as needed, but it still seems snappy for my usage.

The need for speed, Google Pagespeed

Google Pagespeed is great for lazy site developers like me. It optimizes css, javascript and even images. I’ve noticed it cutting load time for most sites in half or more. And, it’s really easy to install, just follow the instructions in Google’s PageSpeed docs here.

Wordfence for security + caching

To run any number of simultaneous users on this server, you’ll need a decent caching solution for WordPress. You may already have a favorite WordPress Caching plugin. I like the Wordfence plugin for caching and security as it does a great job at both.

Once installed, just go to the Wordfence -> Performance page and turn on the Wordfence Falcon engine.

wordfence-falcon-config

Memory Usage and Performance Under Load

With all that running, here is the relevant memory usage as reported by ps_mem.py after 24 hours under load. It’s using just over 50% of the memory of our little server:

 41.1 MiB + 109.0 KiB =  41.2 MiB mysqld
 43.8 MiB +  21.2 MiB =  65.0 MiB apache2 (3)
 91.3 MiB +  45.5 MiB = 136.8 MiB php5-fpm (4)
---------------------------------
                        260.5 MiB

With these optimizations complete, we maintain very solid performance even with 25 users per second hitting the home page:

loader-io-after

Each user is seeing an average of 0.25 second load time. No timeouts. And even during this load testing the site is responsive. The CPU is heavily loaded for the first second or two while it warms up the cache (if needed) and then drops down to about 30%. With 100 clients per second (again, simulated with the awesome loader.io) we’re reaching the edge of what this server can handle configured above but the site still loads in an average of 0.6 seconds. Pretty remarkable for $5/month.

If you try a similar configuration or have suggested tweaks, I welcome your comments here or on twitter.

 

This Post Has One Comment

  1. Awesome write up, Rob. Great tips for optimizing on minimal resources.

Comments are closed.

Close Menu