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:
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.sockpm = 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.
# # * 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
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.
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:
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.