Hosting WordPress on Raspberry Pi Part 4 – Optimise WordPress through cache

We had WordPress installed on the Raspberry Pi, we are going to look into how to implement various cache solutions to optimise WordPress without depending on plugins.

In the previous parts of the articles, we’ve covered how to setup the server and get the WordPress up and running with no considerations were made on how well the server would handle traffic. There are many cache plugins available, but we’d want to implement as much the cache at the system level as possible without depending on plugins.

PHP OPcode

PHP is an interpreted language that the code written in PHP is parsed compiled into Opcode on every page request. This is quite inefficient as imaging that the WordPress web pages served rarely changes between page requests, therefore it’s unnecessary to re-compile it. As of PHP 5.5, there’s now a built-in opcode cache called OPcache. The OPcache stores the compiled PHP codes and serves it from memory, which vastly improves performance. OPcache is a core component built into php-fpm, so it does not require separate installation.

check-opcache-using-phpinfo
Check OPcache using phpinfo()

The default value in php.ini are good enough except that we’d want to disable the opcache from caching the /var/www/html/admin directory where we keep our administration utilities, to do so we need to create a blacklist file first:

cd /etc/php5/fpm
sudo nano opcache-blacklist.conf

Add the directory /var/www/html/admin/* to the file

create-opcache-blacklist-conf-file
Create OPcache-blacklist file configuration file

If there is more than one directory that we need to add to the blacklist, simply add the second line(for example, if we’d want to disable opcache for /var/www/html/wp-admin/*). Press Control-X to save and exit the editor.

Now we need to inform php.ini about the blacklist file.

sudo nano php.ini

Press Control-W to search for opcache.blacklist_filename, uncomment (remove the ; in front of the line, and add the directory name so that it looks like this:

php-ini-opcache-blacklist_filename-setting
php.ini opcache-blacklist_filename setting

Remember to test the php.ini changes and then restart the php5-fpm:

sudo php5-fpm -t
sudo service php5-fpm restart

OPcache status monitor

In order to monitor the status of OPcache, we will install a simple single-file php tool written by the original php language create Rasmus Lerdort called OPcache Status. To install the file:

cd /var/www/html/admin
sudo wget https://github.com/rlerdorf/opcache-status/blob/master/opcache.php

To use the tool, simply run the browser and point the url to http:192.168.0.101/admin/opcache.php.

opcache-status-display
OPcache status display

Query Monitor Plugin

Now the PHP is configured to cache PHP code, it’s time to take a look at database access, which can be a bottleneck to a WordPress website as every post and page are actually stored in database and retrieved by PHP to generate the page or post.

To find out the data query performance, we used a plugin called Query Minitor. Query Monitor is a plugin that provides a lots of information about a WordPress page. Among all the information it generated, it provides a detailed breakdown of how many queries happened on a page request, the execution time of each query, which functions spent most time on SQL queries, whether those queries came from plugins, theme or the WordPress core. For slow queries that is slower than 0.05s, Query Monitor displayed it as red text.

NOTE: Query Monitor is not required for running the WordPress site, we should considered to either deactivate it or even delete the plugin once we completed our configuration and testing at end of this article.

Query Cache

Connecting to MySQL on every page request is the biggest bottleneck within WordPress. One of the best ways to speed up WordPress web site is to enable query caching in the database. This is ideal for an application like WordPress blog that mostly does reads against the database.

MariaDB (and MySQL) actually has query caching support available and is likely enabled by default. This can be verified by running this SQL command from MySQL console, which will tell if query caching is enabled.

MariaDB [(none)]> show variables like 'query%';
+------------------------------+----------+
| Variable_name                | Value    |
+------------------------------+----------+
| query_alloc_block_size       | 8192     |
| query_cache_limit            | 1048576  |
| query_cache_min_res_unit     | 4096     |
| query_cache_size             | 16777216 |
| query_cache_strip_comments   | OFF      |
| query_cache_type             | ON       |
| query_cache_wlock_invalidate | OFF      |
| query_prealloc_size          | 8192     |
+------------------------------+----------+

The query_cache_type shows that query cache is ON or enabled. The default cache memory query_cache_size is set at 16m which is more than sufficient for our WordPress site at this moment. We can then use the SHOW STATUS command to take a look at how it performs under the hood.

MariaDB [(none)]> SHOW STATUS LIKE 'Qc%';
+-------------------------+----------+
| Variable_name           | Value    |
+-------------------------+----------+
| Qcache_free_blocks      | 53       |
| Qcache_free_memory      | 16630976 |
| Qcache_hits             | 4789     |
| Qcache_inserts          | 1059     |
| Qcache_lowmem_prunes    | 0        |
| Qcache_not_cached       | 375      |
| Qcache_queries_in_cache | 84       |
| Qcache_total_blocks     | 233      |
+-------------------------+----------+

This proved that there are plenty of free memory left. This is good enough for now, but in the future when there is a need to change the settings, it can be done by modifying the /etc/mysql/my.cnf configuration file for MariaDB.

Object Cache

Query caching can significantly improve the speed of web application, especially if the application does mostly reads. If the application updates tables frequently, then the query cache will be constantly purged. For example, if the website is a community website or forum where users constantly post comments or postings, or if you are running certain plugins such as BuddyPress, or bbPress. In those cases, Object cache would have to be implemented other than query cache. An object cache stores potentially computationally expensive data such as database query results and serves them from memory.

Since the results from Query Monitor indicates that there is no additional needs for object cache, and since we are running a self-hosting server instead of a shared hosting (Query caching won’t work if you are running on shared hosting), there is no need to implemented object cache for now. We therefore won’t cover the setup of object cache solution such as Redis server and Redis Object Cache plugin here.

Page Cache

Although we have configured opcode and query cache to improve our little WordPress site’s performance, for WordPress sites, content is rarely updated. Meaning each page or post is more or less like a static HTML page. WordPress generated the static HTML version of the request page by querying the database and build the desired page dynamically on every single request. Furthermore Raspberry Pi has a relatively slow SD Card as its main storage media.

With proper configuration, Nginx can automatically cache a static HTML version of a page using the FastCGI module. Then any subsequent calls to the requested page will receive the cached HTML version without ever hitting PHP or WordPress. A guide to caching with Nginx provide details on how to setup the cache.

Open the virtual server configuration file at:

sudo nano /etc/nginx/sites-available/default

Add the following lines before the server block.

fastcgi_cache_path /var/run/nginx-cache levels=1:2 keys_zone=WORDPRESS:10m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header http_500;

The fastcgi_cache_path is the path where the cache will be stored, we will use /var/run as its mounted as tmpfs (that is, in RAM). To verify it and the size of tmpfs, run command:

df -h /var/run

You will see output like below:

Filesystem      Size  Used Avail Use% Mounted on
tmpfs           463M   18M  446M   4% /run

It looks there are sufficient space for the cache.

levels sets up a two‑level directory hierarchy under /var/run/nginx-cache/. If the levels parameter is not included, Nginx puts all files in the same directory which can slow down file access when having a large number of files.

keys_zone sets up a shared memory zone for storing the cache keys. A 10‑MB zone configured in the example can store data for about 80,000 keys.

inactive specifies how long an item can remain in the cache without being accessed. In this example, a file that has not been requested for 60 minutes is automatically deleted from the cache by the cache manager process, regardless of whether or not it has expired.

A powerful feature of Nginx content caching is the ability to deliver stale content from its cache when it can’t get fresh content from the origin servers. proxy_cache_use_stale enable Nginx to delivers the available stale file to the client when receiving an error, timeout, invalid headers or 500 errors from the origin server.

fastcgi_cache-settings
fastcgi_cache settings

Next we need to instruct Nginx not to cache certain pages. The following will ensure wp-admin screens and user login pages are not cached, plus a few others. Add the following lines before the location directive within the server block.

	set $skip_cache 0;

	# POST requests and urls with a query string should always go to PHP
	if ($request_method = POST) {
		set $skip_cache 1;
	}   
	if ($query_string != "") {
		set $skip_cache 1;
	}   

	# Don't cache uris containing the following segments
	if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") {
		set $skip_cache 1;
	}   

	# Don't use the cache for logged in users or recent commenters
	if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
		set $skip_cache 1;
	}
skip-cache-for-certain-pages
Skip cache for certain pages

Within the PHP location block add the following directives near the end of the block.

    fastcgi_cache_bypass $skip_cache;
    fastcgi_no_cache $skip_cache;
    fastcgi_cache WORDPRESS;
    fastcgi_cache_valid 60m;
fastcgi_cache-settings-for-php
fastcgi_cache settings for handling php files

The follow step is not compulsory, but would be nice to add an extra http header response so that we can easily determine whether a request is being served from the cache. Open the nginx.conf file.

sudo nano /etc/nginx/nginx.conf

Add the following below the Gzip settings.

##
# Cache Settings
##

add_header Fastcgi-Cache $upstream_cache_status;

Save the configuration, test the configuration settings before reloading Nginx.

sudo nginx -t
sudo service nginx reload

The extra fastcgi_cache response header can be view from the Developer Console as shown below.
Now when you visit the site and view the headers, you should see an extra parameter.

fastcgi_cache-response-header
fastcgi_cache response header

The possible return values are:
HIT – Page cached
MISS – Page not cached yet
BYPASS – Page cached but not served (e.g. admin screens or when logged in)

Nginx Helper plugin

Nginx has built-in support for fastcgi_cache but it doesn’t have mechanism to purge cached content. So we use a WordPress plugin Nginx Helper to purge the cache when a post/page is created or updated. Once Nginx Helper plugin is installed, navigate to the Settings > Nginx Helper page and configure the purge options.

nginx-helper-settings
Nginx Helper settings

Under the Purge Method, choose “Delete local server cache files” since the version of Nginx we installed does not support “fastcgi_cache_purge”. In order for cache purging to work from the dashboard, we must add the following constant to the wp-config.php file.

define('RT_WP_NGINX_HELPER_CACHE_PATH', '/var/run/nginx-cache');

Now we have a well-configurated WordPress server ready to take on real traffic. We will need a domain name for our web site, which will be covered on the next part of the series.

Leave a Reply

Your email address will not be published. Required fields are marked *