OXY NOTES

WordPressの最速キャッシュを探せ!APC、memcached、Transients APIを比較

APCとmemcached、WordPressのTransients APIを比較

少し前の投稿で紹介しましたが、「Step by Step Social Count Cache」というWordPressのプラグインを作成しました。「SNSのカウントをキャッシュする」という性質上、表示速度にこだわってみました。

その中でデータベースからデータを取得する場合と、APCmemcachedTransients APIから取得する場合で速度を比較したので、まとめてみました。


まず簡単にそれぞれのキャッシュについて解説

既にキャッシュの仕組みについて知っている方は読み飛ばしてください。

APCとは

APCとはAlternative PHP Cacheの略で、コンパイル済みの中間コードをキャッシュする機能と、ユーザーが指定したデータをキャッシュする機能(KVS)があります。

PHP5.5からは技術的な問題でコードキャッシュはZend OPcache。ユーザーキャッシュはAPCuへと分離されました。
今回使うのはAPCのユーザーキャッシュ機能です。メモリ上にキャッシュを展開するため、読み書きが非常に高速というメリットがあります。デメリットは揮発性で唯一のデータ保存場所としては使えず、メモリ量が少ないと古いものから順に消えてしまいます。

memcached

memcachedは名前の通りメモリ上にデータをキャッシュする機能です。ややこしいことに末尾に「d」のあるなしで、「memcached」と「memcache」の2種類があります。違いについて詳しくは専用の解説サイトに任せるとして、PHP5.2.0以上でlibmemcachedモジュールをインストールできるなら、パフォーマンス的に有利で多機能な「memcached」がおすすめです。

違いについて解説されているサイト
ケーワン・エンタープライズのエンジニアメモ(`・ω・´)ゞビシッ!! 「memcacheとmemcachedの違い」

その特徴はなんといっても分散メモリキャッシュシステムです。簡単にいえば複数のサーバのメモリを利用できるという機能です。Facebookは高速化のために独自のPHPを実装するほどのこだわりをもっていますが、memcachedサーバも数千台運用しています。
Facebookの例からわかるように、サービスの規模に合わせてスケールできるというのが最大のメリットです。

デメリットは、キャッシュのアクセスにネットワークで接続するため遅延が発生します。また分散するため、キャッシュの削除や更新などの管理が難しいという点です。
その特性から小さなファイルよりも取得や生成に時間のかかる大型のファイルをキャッシュするのに向いています。

Transients API

Transientsという名前が示す通り、一時的なデータを保存するWordPress独自のAPIです。WordPressのoptionsテーブルデータ有効期限をセットで保存するという仕組みです。

Transients APIのCodex解説ページ

有効期限があるため、一時的にデータを保存するといった用途に向いています。管理が楽な反面、データの読み書きはデータベースに直接アクセスするため低速です。キャッシュ系のプラグインを導入してオブジェクトキャッシュをAPCmemcachedに割り当てることで速度を出すことができます。
プラグインは割り当てるキャッシュを選択することのできる「W3 Total Cache」がお勧めです。

当サイトでも解説したことがあるので詳しく知りたい方は以下の投稿をご覧ください。
W3 Total Cacheの設定を通して学ぶ、WordPressを高速化するキャッシュの仕組み

以上、手軽に導入できる3つのキャッシュ機能を使って、速度の比較を行いました。


各キャッシュを利用して速度を比較する

単純にgetやsetを行ったデータは他にいくらでもあるので、実際にWordPressでデータベースからデータを取得する速度を比較します。

普通にデータベースにアクセスする場合

まずは比較用にキャッシュを使わないでデータベースにアクセスする方法を計測します。
今回はプラグインで作成したテーブルから複数のカラムの値を取得します。1回だと早すぎて計測できないので100回取得します。環境はXAMPPです。

$time_start = microtime(true);

global $wpdb;
$table_name = $wpdb->prefix . "socal_count_cache";
$postid = 100;
$query = "SELECT day,twitter_count,facebook_count,google_count,hatena_count,pocket_count,feedly_count FROM {$table_name} WHERE postid = {$postid}";

for ($i = 0 ; $i < 100 ; $i++) {
	$result = $wpdb->get_row( $query );
	var_dump($result);
}

$time_end = microtime(true);
$time = $time_end - $time_start;
var_dump($time . "秒掛かりました。");

どこでもいいですが、今回はfunction.phpに書いて実行しました。

上記のコードを10回実行した平均が36msでした。これが基準になります。
…というか、これ凄く早いですね。MySQLのキャッシュに乗っているからだと思いますが、クエリ1回あたり1ms以下ですね。WordPressの底力を見た気がします。

APCでキャッシュする場合

続いてAPCを利用して計測します。

実際に使用するコードに近づけるということで、キャッシュが無効な場合の処理も追加しています。
APCモジュールがインストール済みかどうかはfunction_exists( ‘apc_store’ )で調べられますが、PHPの設定で無効になっている場合があるのでini_get( ‘apc.enabled’ )で合わせて調べています。

$time_start = microtime(true);

global $wpdb;
$table_name = $wpdb->prefix . "socal_count_cache";
$postid = 100;
$query = "SELECT day,twitter_count,facebook_count,google_count,hatena_count,pocket_count,feedly_count FROM {$table_name} WHERE postid = {$postid}";

for ($i = 0 ; $i < 100 ; $i++) {
	if ( function_exists( 'apc_store' ) && ini_get( 'apc.enabled' ) ) { // apcモジュール読み込まれており、更に有効かどうか調べる
		$sbs_apc_key = "sbs_db_cache_" . md5( __FILE__ ) . $postid;
		if ( apc_fetch( $sbs_apc_key ) ) { // キャッシュがある場合
			$result = apc_fetch( $sbs_apc_key );
			var_dump( $result );
		} else { // キャッシュがない場合(データベースのキャッシュを取得)
			$result = $wpdb->get_row( $query );
			apc_store( $sbs_apc_key, $result, 10);
			var_dump( $result );
		}
	} else {
		$result = $wpdb->get_row( $query );
		var_dump( $result );
	}
}
$time_end = microtime(true);
$time = $time_end - $time_start;
var_dump($time . "秒掛かりました。");

10回の平均が3msでした。クエリ1回あたり0.03msという早すぎて正確に測れてるんだかわからないほどの速度です。

memcachedでキャッシュする場合

memcachedも同じように実行します。

$time_start = microtime(true);

global $wpdb;
$table_name = $wpdb->prefix . "socal_count_cache";
$postid = 100;
$query = "SELECT day,twitter_count,facebook_count,google_count,hatena_count,pocket_count,feedly_count FROM {$table_name} WHERE postid = {$postid}";

$memcache = new Memcache;
$memcache->connect('localhost', 11211) or die ("接続できませんでした");

for ($i = 0 ; $i < 100 ; $i++) {
	$result = $memcache->get('key');
	if ( ! $result ) {
		$result = $wpdb->get_row( $query );
		$memcache->set('key', $result, 0, 30);
		var_dump( $result );
	} else {
		var_dump( $result );
	}
}
$time_end = microtime(true);
$time = $time_end - $time_start;
var_dump($time . "秒掛かりました。");

10回の平均が29msでした。

実際はプラグインが呼び出される毎にインスタンスを作成するので、毎回インスタンスを作成する方法でも計測します。

$time_start = microtime(true);

global $wpdb;
$table_name = $wpdb->prefix . "socal_count_cache";
$postid = 100;
$query = "SELECT day,twitter_count,facebook_count,google_count,hatena_count,pocket_count,feedly_count FROM {$table_name} WHERE postid = {$postid}";

for ($i = 0 ; $i < 100 ; $i++) {
	$memcache = new Memcache;
	$memcache->connect('localhost', 11211) or die ("接続できませんでした");
	$result = $memcache->get('key');
	if ( ! $result ) {
		$result = $wpdb->get_row( $query );
		$memcache->set('key', $result, 0, 30);
		var_dump( $result );
	} else {
		var_dump( $result );
	}
}

$time_end = microtime(true);
$time = $time_end - $time_start;
var_dump($time . "秒掛かりました。");

10回の平均が356msでした。
オペランドキャッシュ等を用いれば違った結果になるかもしれませんが、差分が327msなので単純計算でインスタンスの作成に毎回3.1msかかっていることになります。
また、インスタンスの作成を外部に置いて純粋にsetとgetを100回試したところ12msでした。

Transients APIでキャッシュする場合

まずはオブジェクトキャッシュとしてキャッシュしないで、毎回optionsテーブルに保存する場合。
注意点があり、Keyの文字数は45文字以下にしないと動作しません。そして失敗しても何のエラーも表示されないので注意が必要です。

$time_start = microtime(true);

global $wpdb;
$table_name = $wpdb->prefix . "socal_count_cache";
$postid = 100;
$query = "SELECT day,twitter_count,facebook_count,google_count,hatena_count,pocket_count,feedly_count FROM {$table_name} WHERE postid = {$postid}";

for ($i = 0 ; $i < 100 ; $i++) {
	$sbs_transient_key = "sbs_db_cache_" . $postid;

	if ( false === ( $result = get_transient( $sbs_transient_key ) ) ) {
		$result = $wpdb->get_row( $query );
		set_transient( $sbs_transient_key, $result, 10 );
		var_dump( $result );
	} else {
		var_dump( $result );
	}
}
$time_end = microtime(true);
$time = $time_end - $time_start;
var_dump($time . "秒掛かりました。");

10回の平均が52msでした。大健闘と言いたいところですが、データベースからデータを取得するよりも低速です。これはTransients APIが初回にoptionsテーブルにデータを保存するためです。

キャッシュ時間を伸ばして、全てキャッシュで返すようにすると、9msという速度で実行が可能です。逆にキャッシュが切れている場合、1回データを読み書きするだけでも42msかかってしまいます。

このことからわかるように、頻繁に書き換えを行う場合は普通にデータベースから取得するよりも低速になってしまいます。

そのデメリットを補うのが既存のキャッシュとの組み合わせです。今回はキャッシュを管理するプラグインW3 Total Cacheを導入してオブジェクトキャッシュをAPCmemcachedに保存する方法を両方試してみました。
すると、キャッシュが無効な場合は50msと変化がありませんが、キャッシュが有効時にはどちらも3msにまで短縮できました。
これはAPCに直接保存するのと遜色ないスピードで、memcachedに関しては直接記述するよりも高速になっています。
(memcachedのインスタンスはW3 Total Cache側で生成しているので、全体の速度としては上の結果と同じか遅いはずです)


計測のまとめ

以上の結果をまとめると以下のグラフのようになります。

APCについて

単純な速度比較だとAPCが最も高速という結果でした。高速化を目指しており、メモリに余裕のある場合は積極的に利用すべき。という結果です。

memcachedについて

memcachedは読み書きの速度に関してはAPCに遜色ないスピードでしたが、インスタンスを作成するのに時間がかかるため、キャッシュの対象を作成するのに15ms以上かかるような重い処理でないと、速度面でのメリットはありません

memcachedの特徴でもあるメモリを分散している場合、ラウンドトリップ遅延も加算され、早くても100ms程度かかります。
既にキャッシュ系のプラグインでmemcachedを利用しているならまだしも、あえてプラグインにmemcachedを利用する仕組みを実装するメリットは無さそうです。

使うとしたらレンダリングに時間のかかる複雑なHTMLを書き出すといった用途か、負荷軽減のための対策といった用途です。

Transients APIについて

Transients APIは「キャッシュをデータベースに書き込む場合は低速である」という特徴を把握すれば有用です。
例えばバックグラウンドで予めデータをキャッシュしたり、書き換えの頻度が低いキャッシュに向いています。
オブジェクトキャッシュとしてAPCmemcachedを利用している場合は高速化という点でも申し分ありません。プラグインの導入が欠かせませんが、キャッシュの仕組みをユーザーに任せるという点では実装が楽です。


上の計測とは関係なく「数千件のpostsテーブルからステータスが公開で、パスワードが設定されていないものを、IDで降順に取得」というクエリを100回取得してみましたが、150ms程度でした。

遅い遅いと言われているWordPressですが、プラグインを大量に入れたり、投稿を1画面に100件表示といった、無茶な実装をしなければキャッシュを利用せずとも十分速度は出るようです。