nazolabo

フリーランスのWebエンジニアが近況や思ったことを発信しています。

APCキャッシュを安全に扱うSafeApcを作った

https://github.com/nazo/safeapc

なにこれ?

APC(APCu)のユーザーキャッシュ(アプリから指定するキャッシュ。ソースコードのキャッシュではない)を「そこそこ安全に」扱うための簡単なラッパーです。

どう使うの?

packagistに登録してあるので普通にcomposerから入れてください。

基本的には普通にapc_fetchとかするのがSafeApc::get等に変わっただけです。

あと、リクエストの最初に、以下を入れる必要があります。

// キャッシュで使用するリクエスト開始時間を指定
SafeApc::setCacheStartTime($_SERVER['REQUEST_TIME']);

// キャッシュのバージョン番号を指定(この例では外部ファイルから)
SafeApc::setCacheVersionKey(file_get_contents('apc_version'));

これだと何がいいの?

これにより、APCキャッシュの問題2つが改善されます。

ttlの時間は常にtime()を参照する問題

APCttlを指定した場合、そのttlの判定は、常に「APC関数を呼び出した時点でのtime()の戻り値」となります。

通常、Webアプリでは、処理中の内部時間がずれてしまうと内部時間の一貫性がなくなるため、リクエスト開始時刻をそのリクエスト中の時間とし、リクエスト中の処理が1秒以上経過しても、全ての更新時刻などはリクエスト開始時刻で扱うことが多いですが、APCttlはそこは当然無視されるので、ttlを厳密に扱おうとした場合に、微妙にずれる、ということが発生します。

例えば、以下のような感じです。

// このキャッシュは1秒だけ持たせよう
apc_store('key', 'value', 1);

// 適当な処理
sleep(3);

// 同一リクエスト中だしさっきのキャッシュ残ってるよね?→ない!
apc_fetch('key');

これを回避するために、ttlは86400で固定し、PHP側で生存時間を判定するようにしています。なんで86400固定なのかは後述。

全キャッシュクリアは高負荷だよ問題

APCの全キャッシュクリアは、同プロセスのAPC処理を全てブロックします。このため、確実に他の処理に悪影響が発生します。

また、PHP5.3以前&apache2 mod_phpでは、このブロックされている途中にmax_execution_time超え等で強制終了されると、ロックを開放せずに全プロセスを巻き込んで死ぬという挙動があります。(このへんの話

どちらにしても、全キャッシュクリアは危険なので、キャッシュクリアは2つの方法を組み合わせて行っています。というか、行ってください。

  • 無期限設定でも、必ず86400秒(1日)経過で自動で消える。(変更できます)
    • この時、86400秒で一斉に消えると高負荷になるので、ランダムで+0〜30秒増やしています。これによりキャッシュクリアのタイミングが、よりバラつくようになります。
    • デフォルトが86400秒なのには特に意味はありませんが、なんとなく1日くらい残ってくれれば大丈夫かなーという感じで指定しています。
  • キャッシュのバージョンを変えることによって、そもそも前のキャッシュキーにアクセスしないようにしている。
    • 内部的にはキャッシュのキーは「バージョン#本来のキー」という文字列になります。そのため、バージョンを変えると、古いキーには絶対にアクセスしません。
    • これはこれで、古いキーが1日残り続ける、という問題はあります。
    • 従って、「SafeApc::setCacheVersionKeyに与える引数を変える=キャッシュクリア」となります。
    • ついでに言うと、通常APCのキャッシュクリアは、Webサーバーのプロセス上で行わないといけないので、コマンドラインからの実行には一手間必要でしたが、この方法だとただバージョン番号を変えるだけなので、簡単にキャッシュクリアをコマンドライン上で行うことができます。

必要な設定

  • apc.user_ttlは何でもいいので必ず設定してください。0だと、メモリが溢れた場合に、apc_storettlを無視して全キャッシュクリアを行うという問題があります。
  • 削除を行わない方針のため、比較的メモリ使用量が増えやすいです。apc.shm_sizeは、「キャッシュの最大量 * 想定される1日のキャッシュクリア回数」くらいのサイズを指定しておきましょう。
  • 内部的にはapc_fetch等を呼んでいるので、APCuで使う場合はAPC互換モードでビルドしてください。(Enable full APC compatibility [yes] : yes)

まとめ

ここまで書いておいてアレですが、APCキャッシュは外部との通信がない分高速ですが、基本的にmemcachedかredisでいいと思います。APCだと複数サーバあるとその台数分キャッシュ生成処理が走るし。状況次第ですね。

某所で散々検証した内容なので、理論的には問題ないはずですが、このコード自体は全く使っていないしろくにテストしていないので、バグがあれば教えてください。