While trying to speed up my wordpress installation I noticed that there seemed to be a lot of plugins that generate a static home page – they all used complex methods to store the files, checking the last modified times and implementing complex checking methods to see if the content has changed.
It struck me as strange that no one had suggested using APC cache – I don’t know if this is because its not available on all commercial hosting packages (such as dreamhost) or if its just due to lack of knowledge about the apc_store and apc_fetch commands. APC doesn’t just cache PHP opcode, it can cache user defined PHP variables as well.
I’ve come up with a small script that is capable of caching my PHP pages generated by wordpress. It uses the APC cache to store and manage the page data and it guarantees that the page will never be more than a specified number of seconds out of date.
In order to use this plugin you only have to make 2 changes go index.php in the wordpress root. Firstly you place this code at the top of your index.php
<?
if (empty($_POST)) {$cache_key = md5($_SERVER['REQUEST_URI'] .serialize($_GET));
$cache_data = apc_fetch($cache_key, $cache_result);if ($cache_result == true) {
echo($cache_data);
die;
}ob_start();
}
?>
And then place this code at the base of index.php
<?
if (empty($_POST)) {
$cache_data = ob_get_clean();
apc_store($cache_key, $cache_data, 300);
echo($cache_data);
}
?>
The code works by creating a MD5 hash of the URL and the GET request – this is important as it means that pages that take variables as arguments, such as search, will create their own cached pages. We don’t use the cache if there are POST variables present as we can probably assume that the output of the page is very dynamic. If you don’t want to cache pages that use a GET request as well you could use something like this
if ((empty($_POST)) && (empty($_GET)) {
It would be unwise to cache separate pages for each set of POST data as POST data is generally much larger than data sent via GET.
We use the MD5 of the URI and GET parameters to act as a key for our storage, if we find the key in memory and it hasn’t expired, we can use the data retrieved from the key to display the page – this bypasses all the MySQL and other PHP that was needed to create the original page.
Wordpress Static vs Dynamic benchmarks
If we now look at some benchmarks of before and after we can see the difference. I’ve use ab (that comes with apache) to perform the tests
Without the caching
| Requests per second: | 8.52 [#/sec] (mean) |
| Time per request: | 1173.343 [ms] (mean) |
| Time per request: | 117.334 [ms] (mean, across all concurrent requests) |
| Transfer rate: | 212.85 [Kbytes/sec] received |
| Connection Times (ms) | |||||
|---|---|---|---|---|---|
| min | mean[+/-sd] | median | max | ||
| Connect: | 0 | 0 | 0.1 | 0 | 1 |
| Processing: | 579 | 1161 | 104.4 | 1158 | 1464 |
| Waiting: | 234 | 465 | 82.6 | 449 | 756 |
| Total: | 579 | 1161 | 104.4 | 1158 | 1464 |
| Percentage of the requests served within a certain time (ms) | |
|---|---|
| 50% | 1158 |
| 66% | 1186 |
| 75% | 1225 |
| 80% | 1238 |
| 90% | 1289 |
| 95% | 1315 |
| 98% | 1407 |
| 99% | 1413 |
| 100% | 1464 (longest request) |
With caching (tests done on non primed cache)
| Requests per second: | 136.07 [#/sec] (mean) |
| Time per request: | 73.490 [ms] (mean) |
| Time per request: | 7.349 [ms] (mean, across all concurrent requests) |
| Transfer rate: | 3391.63 [Kbytes/sec] received |
| Connection Times (ms) | |||||
|---|---|---|---|---|---|
| min | mean[+/-sd] | median | max | ||
| Connect: | 0 | 0 | 0.6 | 0 | 4 |
| Processing: | 10 | 72 | 32.2 | 77 | 202 |
| Waiting: | 5 | 32 | 25.0 | 26 | 196 |
| Total: | 11 | 72 | 32.2 | 77 | 202 |
| Percentage of the requests served within a certain time (ms) | |
|---|---|
| 50% | 77 |
| 66% | 89 |
| 75% | 93 |
| 80% | 98 |
| 90% | 109 |
| 95% | 120 |
| 98% | 136 |
| 99% | 142 |
| 100% | 202 (longest request) |
You can see that without the caching that the server can serve 10 requests per second and after the caching it can serve 136 requests per second. That’s a 13 times increase in speed. Not bad for a server with only 256mb of RAM and a single core CPU. I’m pretty sure the server could handle even more load but at this point the upstream connection became saturated.
There are however some pitfalls with caching. Currently when users are logged in we cache their page, we need to add a condition to prevent this caching. Its not a problem on my server however as I am the only personal allowed to blog. Other pitfalls are the freshness of data, we can set this to whatever we want (from 1 second to infinity) but its never going to be as “fresh” as a page that isn’t cached.
Caching isn’t for everyone but if you do want your site to run faster, can afford to have some slight inconsistencies in your data and don’t mind waiting a few minutes for a comment to appear then you can achieve some really good results.
Let hope comments still work with this caching…
these are pretty good numbers, but isn’t the whole point of supercache and static file generating is so you avoid the trip to PHP in the first place? handling a ton of requests via PHP/FastCGI vs. having lighttpd/Apache return the static file with one stat() call to check if it exists, it seems like the static file method is going to be tons faster.
Yeah, i agree totally, if you used a static file the results would be much faster (quite how much faster I’m not sure) and it would prevent the call to APC/PHP. In my case APC will do as it appears that my upstream connection gets saturated before the calls to PHP become an issue.
I’ll give the static file method a try (probably sometime this week) and see how the results compare
Caching pages with in memory caches can improve performance but you will exhaust the reserved memory and page flushes will occur, actually reducing in performance in a realtime scenario.
But for heavily customized homepages, with loads of visitors as witten http://www.php-trivandrum.org/general-articles/speeding-up-wordpress-home-page/, the partial data caching in ram disk without any in memory cache would be at par. Here the home page is only cached.
I am working on a plugin for word press which will help out doing this more decently.
My method is somewhat of a bodge – I only have 150 pages on my site which means that everything will easily fit within 6mb of memory preventing page flushes. You are quite right – if you know you can’t fit everything in memory, dont use my crude method. Pre-generating the page and storing it on disk would be a much better option.
Look forward to seeing your wordpress cache plugin – hopefully it’ll be a little more sophisticated than my method
Fatal error: Call to undefined function apc_fetch() in /home/xxxxxxx.com/index.php on line 5
unfortunately with the WP- 2.9.1 on dreamhost it is not working well
any idea what to change??
thanks
Its probably that dreamhost doesnt support APC (the APC cache is an extension of PHP), do a phpinfo(); and see if you can see a heading for APC – if its not there then its not being support by Dreamhost.
Very good idea. I had exactly the same and wanted to do the same thing. Just wanted to tell you that : md5($_SERVER['REQUEST_URI'] .serialize($_GET)) will change at the same frequency as md5($_SERVER['REQUEST_URI']) because the REQUEST_URI actually contains the GET arguments.
I think you can implement it more properly, give a longer timeout and clean everything as soon as a comment is posted or a new post is added.
[...] I had the brilliant idea to put everything in an APC cache, someone had the same [...]