フツーここまでやらない、キャッシュヒット率100%で運用するWordPress高速化

プリロード導入前後の平均表示時間の推移

キャッシュ系プラグインを入れて速くなった気がしていても、Google Analytics の「サイト速度」が思ったほど速くなってない、なんて言う経験はないですか?

今年の4月に発覚した WP Super CacheW3 Total Cache の、心底ヤバイ 脆弱性騒動 も乗り切り、懲りもせずタイトルの様なことを試しています 😉 。

特に共有サーバーで運営しているよいこの皆さんは、絶対にマネしちゃイケナイ、本サイトのヒミツを暴露しちゃいますネ 😉 。

キャッシュを導入しても速度が上がらない理由

そもそも WordPress のキャッシュ・プラグインは、下の図のように、ページが要求されてから最初の HTML ドキュメント生成までの時間を短縮するものです。

キャッシュの効果

かつ、速度的には 圧縮して応答するのが効果的 です。それ故、ページを丸ごと圧縮しておく「ページ・キャッシュ」が、応答時間と転送時間の両方を稼げる、最も効率の良いキャッシュ方式だと私は思います。

さて、以下は Wikipedia の英語版 のキャッシュについての解説です。

要求されたデータがキャッシュに含まれる場合(キャッシュヒット)、高速なキャッシュを単に読み出して提供すればいいが、それ以外の場合(キャッシュミス)、データを再計算するか、元の保存場所から取り出すことになり、大抵は低速となります。

キャッシュには必ず「有効時間」があります。それは、生成から一定の時間間隔であったり、何かのイベントが発生するタイミングであったりと、色々です。

例えばウチのように訪問者が少ないサイトでは、せっかく誰かのアクセスで作られたキャッシュも、次の誰かが同じページにアクセスするまでに有効時間が過ぎてしまい、キャッシュを作り直す追加の負荷を負わされたユーザーを単に増やしただけ、ということが頻繁に起こり得ます。

極端な場合、キャッシュ生成のオーバーヘッドがヒット率を上回り、例えば1日平均では、かえって遅くなってしまう事だってあるかもしれません。

それ故、永続性を持つ MO ファイルのキャッシュDB系のキャッシュ、またページを丸ごとキャッシュしないからこそ有効な Hyper Response との組み合わせが、平均的には効果的だったりするワケです。

が、少なくとも本サイトでは、瞬間的な性能はページ・キャッシュが一番でした。

つまりキャッシュの課題とは、あるアクセス量を前提に、需要と供給、そして鮮度のバランスを考慮した上で、「期待値=ヒット率」をどう上げるかだと思います。

この解決策の1つが、次に述べる「プリロード」です。

プリロードとその課題

ページ・キャッシュを生成できるプラグインは大抵、予めキャッシュを生成しておきヒット率を上げる、「プリロード」という機能をサポートしています。

また次の様なプリロード専用のプラグインもあります。これらは、1ページずつ HTTP リクエストを発行し、WordPress の外側からキャッシュを構築します。

  • AskApache Crazy Cache
    • ページをリクエストする WP Cron タスクが、1時間ごとにスケジューリングされます。WP Super CacheHyper-Cache 専用のプラグインと考えた方がよいかもしれません。2008年7月から更新されていません。
  • Generate Cache
    • ガベージコレクションによりキャッシュ量が設定値を下回ると発動し、ピンポンのように2つの php が互いを呼び出し合いながらページをリクエストします。無限ループの防止だと思いますが、Lacoocan ではこのような仕組みが動きません。
  • Warm Cache
    • サーバーの cron と組み合わせるのが前提で、プラグイン起動用の URL を wgetcurl コマンドで呼び出すと、sitemap.xml から1ページずつ リクエストしていきます。残念ながら Lacoocan では、全ページがリクエストされる前にタイムアウトで動作が停止してしまいます。

上記いずれかが動作するサーバーなら、ヒット率はそれなりに改善するでしょう。

しかしサーバーの負荷を押さえるためでしょう、キャッシュの有効時間(例えば3600秒)以内に全ページを更新できる機能を持ったプラグインはありません。

また有効時間がページごとにバラバラになり、ガベージコレクション(期限切れキャッシュの削除)と再構築との間にタイムラグが生じ、「ヒット率」が思ったほど上がらないのも頭の痛い課題です。

キャッシュ有効時間とガベージコレクションの同期

そこで本サイトでは、少量のページでキャッシュ有効時間とガベージコレクションとを同期させ、短時間でキャッシュの再構築を行う方式を導入しています。

次の図はその原理を示していて、例えば総ページ数が300ページのサイトで、キャッシュ有効時間を1時間、ガベージコレクション周期を20分に設定し、100ページずつのキャッシュ削除と再構築を cron で起動する様子を表しています。

タイミングチャート

ポイントは、ごく短時間の構築を複数回に分けることです。これにより単位時間当たりのサーバー負荷を下げつつ、有効時間を迎えたキャッシュの一斉削除と再構築を、短時間かつ連続して行います。

厳密には先のタイムラグはゼロにはなりませんが、数十秒程度には押さえることが可能で、ほぼ「キャッシュヒット率100%」と言ってよい状態になります。

準備するモノ達と設定

前章のシナリオ通りに運用するための要件と準備するものは、以下の4つです。

1回のキャッシュ構築を短時間で終わらせるプリロード

複数のリクエストを並列実行できる cURL multi を用いた preload-by-cron を作りました。本体の preload.php を、ガベージコレクションの周期毎に起動するよう、サーバーの cron に設定します(例えば Lacoocan では CGIの定期実行)。

wget -q "http://example.com/path/to/your/preload.php?key=your-secret-key"
php -q /path/to/your/preload.php --key "your-secret-key"

オプションには負荷やタイミングを調整するパラメータを指定できますが、主要な項目は preload.php を直接書き換えて設定するというダサイ仕様です。

以下、要件と共に主要な設定値について、順に説明します。

ガベージコレクションの起動を完全に制御する

3章のシナリオに沿って、ガベージコレクションとキャッシュ再構築を連続して行うには、WP-Cron によるタスクの実行を完全に制御できなければなりません。

これには WP-Cron Control を使うか、wp-config.php に以下を追加し、訪問者のアクセスによるタスクの実行を禁止します。

define('DISABLE_WP_CRON', true);

前者の方が「合い言葉」を設定でき、オススメです。詳しくは「WordPressプラグインの重い処理をバックグランドで非同期に実行する方法」を参照して下さい。

preload.php には、以下を設定します。

$garbage_collector = 'http://example.com/wp-cron.php?doing_wp_cron';

構築するページをsitemap.xmlから拾う

定番の Google XML Sitemaps か類似のプラグインを使います。

Google XML Sitemap

preload.php には複数のファイルを指定できるので、上記選択によって構築するファイルを使い分けるのもいいかもしれません(私はそうしています)。

$sitemap_urls = array(
	'http://example.com/sitemap.xml',
);

キャッシュ有効時間とガベージコレクション周期を独立に設定する

WP Super CacheW3 Total Cache は、ガベージコレクションで削除されるまでは、期限切れの古いキャッシュでもそのまま使われる仕様なので、先のタイムラグを小さくするのに好都合です。

同じく人気のある Quick Cache も、ガベージコレクション周期が1時間に固定されてはいますが、古いキャッシュはアクセスされた時点で再構築される仕様なので、問題なく使えます。ただしプラグイン自体では gzip 圧縮転送をサポートしておらず、次第点といったところでしょう。

さて、キャッシュ有効時間とガベージコレクション周期の設計値を、それぞれ3600秒、1200秒とします。3章のシナリオを成立させるには、「ガベージコレクション + キャッシュ再構築」の処理時間を、各々の設計値から引いて設定します。

下の図(WP Super Cache の設定画面)では、ガベージコレクションを10秒、キャッシュ再構築を30秒と見込み、合計40秒を引いた値を設定しています。

WP Super Cacheの設定画面

この辺りがイマイチ美しくないのですが、今のところ良い方法が見つかりません。

一方 preload.php には、上記の設計値やページ取得用のパラメータを設定します。

define( 'CACHE_DURATION',     3600 ); // キャッシュ有効時間 [秒]
define( 'GARBAGE_COLLECTION', 1200 ); // ガベージコレクション周期 [秒]
define( 'CONCURRENT_FETCHES',    5 ); // 並列コネクション数
define( 'TIMEOUT_OF_FETCHE',    10 ); // 各ページ毎のタイムアウト [秒]
define( 'INTERVAL_OF_FETCHES', 500 ); // 並列フェッチ間の待ち時間 [秒]

運用してみて

共有サーバーが許容する同時接続可能なコネクション数は、一般に20〜30だそうです(出典忘れた 🙁 )。本サイトでも curl_multi の並列コネクション数が25を超える辺りから接続失敗が発生します。

現在は、並列コネクション数を2、待ち時間を250msecに設定し、10分ごとに cron を起動、合計250ページあまりを1時間で更新しています。

これだと、キャッシュ再構築中の秒当たりのリクエスト数が2程度と、非常に低い負荷で運用することが出来ます。まぁ、それでもCPU資源をムダに消費しているワケですが…。

同じ仕組みの プラグイン版 も作ってみましたが、「フツーここまでしないよナ」と思いとどまり、開発のモチベーションが萎えているところです。

まぁ明らかに「平均表示時間」は下がっているので、しばらくはコレで運用してみます 🙂 。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

海外からの投稿は受け付けていません