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.

14 Comments so far

  1. n1zyy on July 29th, 2007

    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.

  2. n1zyy on July 29th, 2007

    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

  3. n1zyy on July 29th, 2007

    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

  4. n1zyy on July 29th, 2007

    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

  5. n1zyy on July 29th, 2007

    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.

  6. n1zyy on July 29th, 2007

    How about if I just enable query caching?

  7. n1zyy on July 29th, 2007

    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?

  8. n1zyy on July 29th, 2007

    (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.

  9. n1zyy on July 29th, 2007

    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?

  10. andrew on July 29th, 2007

    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.)

  11. andrew on July 29th, 2007

    Try SHOW GLOBAL STATUS LIKE ‘%cache%;

  12. n1zyy on July 29th, 2007

    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.

  13. n1zyy on July 30th, 2007

    Are you still reading this? I sent you an e-mail but it bounced:

    Delivery to the following recipient has been delayed:

    [removed so you don’t get spammed]

    Message will be retried for 2 more day(s)

    Technical details of temporary failure:
    TEMP_FAILURE: SMTP Error (state 16): 451 qq temporary problem (#4.3.0)

  14. andrew on July 30th, 2007

    I knew I wasn’t just not getting spam… Garrr..

Leave a Reply