Pressing WordPress
This post is designed to inspire our beloved server administrator to get some sort of caching installed.
So I finally decided that enough was enough and I wasn’t going to wait until I spontaneously combusted with the motivation to build my own blogging/CMS system and finish my site. Last night I downloaded WordPress and began hacking my design into their template framework.
While testing it all out, I noticed that the responses seemed pretty slow. I wasn’t sure if it was the wireless network (I’d been wrangling with it a few hours earlier), so this evening I decided to do some benchmarking. I am something of a performance freak, after all. (So I lied about my intentions at the beginning of the post… who cares?) I’d already looked at some of the code (quite hideous, in my personal opinion), so I had a feeling things wouldn’t be pretty out of the box.
I was right: the default installation managed a measly 4 requests per second. First I installed APC, which, under Ubuntu, requires installing the PEAR and php5-dev packages, then running sudo pecl install apc
. The addition of byte-code caching pushed it up to 13 requests/second. Clearly, the code was suffering from runtime — not compilation — issues.
I didn’t have any real desire to delve too deep into the code, so I opted for the prebuilt WP-Cache plugin. And this one was worth the money: just by enabling the plugin I jumped to nearly 500 requests/second. Note that this is a 125 times better than I started with. (Out of curiosity, I also ran with caching on and APC off; about 200 requests/second.)
In short: if you’re running WordPress and you can/have self respect (*grin*), install APC and WP-Cache.
You’ve very subtle.
The problem is that the main page (/main) is code I wrote from scratch. (I just copied the template.) Using WP-Cache would help individual pages, but since /main isn’t even powered by WP (it just uses its database), I would get no benefit. (APC, OTOH, would help.)
I was going to go to bed since it’s 1:30 am, but now I’m going to try to benchmark the main page. I’m thinking it’d benefit enormously from having the query results cached.
Requests per second: 35.39 [#/sec] (mean)
Time per request: 28.254 [ms] (mean)
Time per request: 28.254 [ms] (mean, across all concurrent requests)
Transfer rate: 913.62 [Kbytes/sec] received
The above was for blogs.n1zyy.com/main/index.php, and I used 1,000 requests.
This one is for blogs.n1zyy.com/meta/ (one of the usual WP-powered blogs). The accuracy might be lower because I lowered it to 1,000 requests because it was so slow.
Requests per second: 3.62 [#/sec] (mean)
Time per request: 276.348 [ms] (mean)
Time per request: 276.348 [ms] (mean, across all concurrent requests)
Transfer rate: 45.99 [Kbytes/sec] received
It’s really irrelevant to anything, but for the heck of it, I benchmarked loading http://blogs.n1zyy.com/wp-content/themes/black-minimalism-10/images/wallpaper.jpg — the 22KB background image used on /main.
Requests per second: 1006.93 [#/sec] (mean)
Time per request: 0.993 [ms] (mean)
Time per request: 0.993 [ms] (mean, across all concurrent requests)
Transfer rate: 21625.33 [Kbytes/sec] received
Something is clearly just wrong, though. 4 pages a second? Of course, are results are very similar, which suggests that it’s not something we’re doing wrong, as much as proving what initially turned me off to WP: it makes no sense to dynamically generate a blog with every hit.
Do you think we would see improvements if we played with MySQL tweaking? It seems highly probable to me that the SQL queries are the bottleneck.
How about if I just enable query caching?
Done. I allowed a modest 15M total cache size, since I’m not working with mammoth tables.
I’m also running ab2 as root this time, which may make a difference. But…
blogs.n1zyy.com/main
Requests per second: 1449.30 [#/sec] (mean)
Time per request: 0.690 [ms] (mean)
Time per request: 0.690 [ms] (mean, across all concurrent requests)
Transfer rate: 724.65 [Kbytes/sec] received
blogs.n1zyy.com/meta
Requests per second: 3.64 [#/sec] (mean)
Time per request: 274.942 [ms] (mean)
Time per request: 274.942 [ms] (mean, across all concurrent requests)
Transfer rate: 46.23 [Kbytes/sec] received
So it seems like the individual pages aren’t benefiting from caching. (Buy boy is mine!)
I don’t get it, though… There’s not even 15MB of data in the database?
(Haha okay you’re going to come here in the morning and think I’m nuts.)
I’m real confused here:
mysql> SHOW STATUS LIKE ‘%cache%’;
+——————————–+——-+
| Variable_name | Value |
+——————————–+——-+
| Binlog_cache_disk_use | 0 |
| Binlog_cache_use | 0 |
| Qcache_free_blocks | 0 |
| Qcache_free_memory | 0 |
| Qcache_hits | 0 |
| Qcache_inserts | 0 |
| Qcache_lowmem_prunes | 0 |
| Qcache_not_cached | 0 |
| Qcache_queries_in_cache | 0 |
| Qcache_total_blocks | 0 |
| Ssl_callback_cache_hits | 0 |
| Ssl_session_cache_hits | 0 |
| Ssl_session_cache_misses | 0 |
| Ssl_session_cache_mode | NONE |
| Ssl_session_cache_overflows | 0 |
| Ssl_session_cache_size | 0 |
| Ssl_session_cache_timeouts | 0 |
| Ssl_used_session_cache_entries | 0 |
| Threads_cached | 0 |
+——————————–+——-+
Even as I’m getting 1400+ pages a second on /main, it’s showing that no caching is in use. This is clearly not true, since it’s gone from 35 pages a second to 1400 a second.
This is all I added to my.cnf:
# Enable query caching!
query-cache-type = 1
query-cache-size = 15M
(And, of course, I restarted MySQL.)
Browsing through the table, BTW, I’m unable to find anything that’s being changed on page load. I kind of figured that maybe something stupid was going on like updating a hit counter in the database that was causing the cache to get flushed. But I don’t think that’s it.
For my own amusement, I bumped query-cache-size up to 50MB, and set query-cache-limit (the size of any individual query) to 5MB. Still no beans.
But I’m noticing something bizarre… /main varies a bit, between 1400 and 1500 a second. Various external factors are going on, so it makes perfect sense that it would fluctuate a bit.
With that last sentence in mind, results for /meta are remarkably consistent: between 3.62 and 3.64 requests per second, and they’re all 275ms +/- 0.5ms or so.
You said 4ms. Out of curiosity, did you round? Maybe up from 3.64?
I actually logged the queries to a flat file after I’d done the benchmarks, and while they might fall flat once the dataset begins to grow, I had relatively _nothing_ in the database (now, with additions, it’s at 760K), so I doubt they’re contributing much at all.
The one query that was most alarming (grouped all posts by year and month for the archive list; requires a temporary table and filesort) was actually run _twice_ per page load (sidebar and link tags in the HTML header).
When I profiled the code with XDebug, it looked like a _lot_ of the time was spent in internationalization functions; stuff I don’t really care about (I apologize to everyone who doesn’t speak English).
Or here’s a good example: the query function (in wp-includes/wp-db.php) does a preg_match on the query to determine if should fetch the number of affected rows. It then does _another_ preg_match to see if it should fetch an insert ID. Just a horribly inefficient way to go about it.
Or another example: every time you load a page, it’s checking to see if wp-config.php exists, then including it. PHP caches stat calls for the file functions it exposes, but that doesn’t appear to be shared with the include/require functions. So you end up stat’ing the file (relatively expensive) twice.
I think I may go on a rampage and rip out a bunch of similar stuff that I don’t need and then see how it benchmarks.
(BTW, you might be able to use the single page something or other to build the front-page within the templating framework and take advantage of WP-Cache? Disclaimer: I don’t actually know how to do this.)
Try SHOW GLOBAL STATUS LIKE ‘%cache%;
Huh… I tried hitting /meta again, and paid a little more attention to the cache results. I hit it 50 times with ab2.
Qcache_hits grew by 1250. Divided by 50 hits, that’s 25 queries cached per hit.
Qcache_not_cached grew by 151. 3 a page and one extra?
So it seems like 25 queries get cached, and 3 don’t. But I’ve just allowed caching of queries up to 5MB, and I really doubt that it’s hitting that limit. So maybe something is changing in one of the tables with each page load?
But having 25/28 queries cached should speed things up. So I think I’m going to retract my hypothesis that MySQL is the major bottleneck.
But then… What is the bottleneck?
If I don’t doze off before I’m done, I’m going to send you an e-mail.
Are you still reading this? I sent you an e-mail but it bounced:
I knew I wasn’t just not getting spam… Garrr..