CakePHPアプリケーションの基本的な設計指針 (2) - キャッシュまわり -

イントロダクション

CakePHPのキャッシュ機構は、

  • キャッシュストレージへのラッパー
  • コアに統合されているもの

の2種類があります。
前者はCache::read()などを使うもので、自由にキャッシュの操作が行えます。
後者のほうは、ビューキャッシュ、クエリキャッシュ、ディレクトリマップのキャッシュなどで、仕組みを理解した上で設計を思慮する必要があります。
特に問題となるのはビューキャッシュです。

ビューキャッシュの生成・破棄

ビューキャッシュは、エレメントキャッシュと、アクションキャッシュ(フルページキャッシュ)があります。
アクションキャッシュでは、生成されるファイル名の規則としてURIを用いています
これにより、ルーターの起動の前にキャッシュ処理に移行することを可能にしています。
例:(以下、パスはAPP/tmp/cache/views/以下を示します。)

  • / => home.php
  • /users => user.php
  • /users/view/1 => users_view_1.php

さて、usersテーブルに変更があったとき、/users/view/1などのキャッシュが残っていると、古い内容を表示したままになってしまいます。
そこでモデルからデータ操作をした際、これをCakePHP自動で破棄します。
消されるファイルのフォーマットは以下のようなものです。

*users.php
*users_*.php

同時に関連モデルについても消されます。
また、PostCommentのように複数の単語からなる名前は、また少々異なります。

*post_commnets.php
*postcommnets.php
*post_commnets_*.php
*postcommnets_*.php

これらの元となるのは、モデルのエイリアス名の複数形であることに注意しましょう。
例えば、

<?php Router::connect('/ranking', array('controller' => 'users', 'action' => 'ranking')) ?>

といったルートがあり、ビューキャッシュされている場合、これは破棄されないことになってしまいます。
同様に、/user/:nickname/なども該当します。

このようなビューキャッシュの破棄の挙動を変えるには、Model#_clearCache()をオーバーライドします。
よって、ルートをデフォルト以外のものに変える際は、そのビューキャッシュが消えるように設計することが望ましいでしょう。
例えば、次のような設計が考えられます。

  • モデルのエイリアス単数形を破棄対象に加える
  • アプリケーション特有のURL規則に則ったものを破棄対象にする
  • routes.phpでルーティングするURLをどこかに保持しておく
  • カスタムルートクラスを使ってルートの情報を保持しておく
  • モデルでなく(空のメソッドでオーバーライド)、コントローラコンポーネント)でキャッシュを破棄する
  • データに変更があった場合、ビューキャッシュは全て消す。データの変更がメンテナンス時にしかありえないときなど、場合によってはあり得る選択肢です。
  • 全て手動で消す(頑張ってください :P)
ユーザエージェントによるビューの振り分け

ビューキャッシュにおいて、同じURLのキャッシュは同等のものとみなされます。
よって、PC携帯で同じURLを見ているとすると、PC携帯のキャッシュが、逆に携帯PCのキャッシュが現れてしまうことがあり得ます。
このことから、少なくともビューキャッシュを使う限り、コントローラやアクションはユーザエージェントごとに別にしてしまったほうが良いでしょう。
コントローラが肥大化するという危惧をするかもしれませんが、コントローラの継承、モデルへのfindオプションの保持を行っていれば、それもあまり問題にはなりません。
ただし、設計時にコントローラの継承などはコンポーネントのマージなど十分気をつけなくてはなりません。これについては機会があったら示します。

SSLを交えたビューキャッシュ

SSLページと非SSLページで同等のURIの場合、勿論同じビューキャッシュが適用されます。
あるページでSSLでも非SSLでも表示させる必要がある場合、外部サービスのアセットがブラウザにより遮断されてしまう恐れがあります。(twitterのつぶやきボタンなど)
その場合は、//example.com/util.jsなどといった、HTTPとHTTPSの判断をブラウザに任せる記述を用いるのが適当です。
しかし、これはヘルパやルータなどでうまく反映されず、/path/to/webroot//example.com/util.js などといったパスに変換されてしまうことがあります。(2.0では対応されています)
その場合は、AppHelper#url()をオーバーライドするなどして対処しましょう。

<?php

class AppHelper extends Helper {

	public function url($url, $full = false) {
		if (is_string($url) && preg_match(sprintf('/^%s.+/', preg_quote('//', '/')), $url)) {
			return h($url);
		}
		return parent::url($url, $full);
	}

}

エレメントキャッシュ

固定的なウィジェットを表示するために、AppController#beforeRender()/beforeFilter()などで常にDBの読み込みが発生することは、致命的なパフォーマンス問題を引き起こします。
そういったものはrequestAction()を用いたエレメントをキャッシュすることが望ましいでしょう。
詳しくは
requestAction :: その他の便利なメソッド :: コントローラのメソッド :: コントローラ :: CakePHPによる開発 :: マニュアル :: 1.3コレクション
を見てください。

フレームワークから離れる

一般的に言えることですが、外部サービスのストレージを利用する場合など、遅いストレージのものは常にキャッシュすべきです。
しかし、単に画像をキャッシュする場合など、CakePHPのアクションを通してしまうと、余計な負荷がかかってしまう場合があります。
キャッシュ機構自体をhttpdのモジュールなどで対処することもスマートな解決策の一つですが、ここはPHPに任せてみましょう。

  • APP/webroot/tts.php
<?php

// 一例。ストリーミングなどを使ったほうが当然良いが、コードが複雑化するだけなので省略する
// 「チャンク変換考慮してないの?」 => 複雑化省略

if (empty($_GET['word'])) {
	header('HTTP/1.1 403 Forbidden');
	exit;
}

$word = $_GET['word'];
$word = rawurlencode($word);

$cacheKey = 'tts_word_' . $word;
$cached = apc_fetch($cacheKey, $success);

if ($success) {
	foreach ($cached['headers'] as $header) {
		header($header);
	}
	echo $cached['contents'];
} else {

	$contents = file_get_contents("http://translate.example.com/?word=$word");

	if (!$contents || empty($http_response_header) || !is_array($http_response_header)) {
		$forbidden = 'HTTP/1.1 403 Forbidden';
		header($forbidden);
		$contents = null;
		$headers = array($forbidden);
	} else {
		$headers = array();
		foreach ($http_response_header as $header) {
			header($header);
			$headers[] = $header;
		}

		echo $contents;

		ob_end_flush();
	}

	// caches a week
	apc_store($cacheKey, compact('contents', 'headers'), 60 * 60 * 24 * 7);

}
  • 呼び出し
<?php echo $this->Html->link($word, '/tts.php?word=' . rawurlencode($word)) ?>

これはもちろんサービスがプライベートアクセスを要求する場合は使いにくい手法です。
CakePHPのキャッシュは素晴らしいですが、それを使わないという手段があることも頭の片隅に入れておくと良いかもしれません。

最後に

ビューキャッシュの仕組みは複雑で扱いにくく、辟易するかもしれませんが、使い方によっては強力な武器になります。
ハマりどころが分かっていればそれなりの対処はできるはずです。
CakePHP2.0では改善した部分もあるので、是非窓からPCを投げ捨てないように付き合ってあげてください ;)

補足Togetter - 「CakePHPアプリケーションの基本的な設計指針 (2) - キャッシュまわり - へのツッコミなど」