おぎろぐはてブロ

なんだかんだエンジニアになって10年以上

PHP5.1から実装されたrealpath cacheについて(1) - 概要と、デフォルトのキャッシュサイズが小さめというはなし

PHP5.1から、パフォーマンス対策として、realpath cacheというものが導入されています。

PHPがファイルを開く際(実行ファイル本体から、include/require、fopenなど基本的には全てのファイルアクセスが対象)に、open_basedirの検証や、include_pathの探索のために、realpath(3)を呼び出し、その中でstat(2)を大量に呼び出しており、ファイルをたくさん扱う場合には、結構ボトルネックとなっていました。(realpathとは正規化された絶対パス名を返すもので、パス中のシンボリックリンクをすべて展開する関数です)
ここらへんの実際の動きについては前に書いてます。

Yahoo!では、このキャッシュがなかったころ(PHP4の時代)、realpathを呼び出さないようにパッチをあてており、Derickがそんなパッチを作ったのをあててみたら、30%スピードアップという話。

とはいえ、セキュリティ上制約を与える必要があったり、realpathを見ないことによる弊害もあるわけで、この結果をキャッシュするようにしたのがrealpath cacheです。

キャッシュ設定

php.iniで、キャッシュサイズと、生存時間(TTL)を設定することができます。デフォルト値は、16K (byte)と120秒です。

パフォーマンスチューニング

名前 デフォルト 変更の可否 変更履歴
realpath_cache_size "16K" PHP_INI_SYSTEM PHP 5.1.0 以降で使用可能。
realpath_cache_ttl "120" PHP_INI_SYSTEM PHP 5.1.0 以降で使用可能。

以下に設定ディレクティブに関する簡単な説明を示します。

realpath_cache_size integer
PHP が使用する realpath キャッシュの大きさを設定します。 PHP で大量にファイルをオープンする際に、 この値を大きくすることによってファイル操作のパフォーマンスを 向上させます。
realpath_cache_ttl integer
与えられたファイルやディレクトリについての realpath 情報キャッシュの有効期限を (秒単位で) 設定します。 ファイルを変更することがほとんどない場合は、この値を大きくすることを検討してください。
PHP: Description of core php.ini directives - Manual

realpath_cache_sizeの値は、小さいかも

デフォルト値は、16K = 16,000byteとなっていますが、これで適切なのかどうか。コードを眺めてみます。
キャッシュ周りの処理は、TSRM/tsrm_virtual_cwd.c にあります。
TSRM/tsrm_virtual_cwd.c (PHP5.2.6) line:436

static inline void realpath_cache_add(const char *path, int path_len, const char *realpath, int realpath_len, time_t t TSRMLS_DC) /* {{{ */
{
  long size = sizeof(realpath_cache_bucket) + path_len + 1 + realpath_len + 1;
  if (CWDG(realpath_cache_size) + size <= CWDG(realpath_cache_size_limit)) {
    realpath_cache_bucket *bucket = malloc(size);
    unsigned long n;
  
    bucket->key = realpath_cache_key(path, path_len);
    bucket->path = (char*)bucket + sizeof(realpath_cache_bucket);
    memcpy(bucket->path, path, path_len+1);
    bucket->path_len = path_len;
    bucket->realpath = bucket->path + (path_len + 1);
    memcpy(bucket->realpath, realpath, realpath_len+1);
    bucket->realpath_len = realpath_len;
    bucket->expires = t + CWDG(realpath_cache_ttl);
    n = bucket->key % (sizeof(CWDG(realpath_cache)) / sizeof(CWDG(realpath_cache)[0]));
    bucket->next = CWDG(realpath_cache)[n];
    CWDG(realpath_cache)[n] = bucket;
    CWDG(realpath_cache_size) += size;
  }
}

要は、

long size = sizeof(realpath_cache_bucket) + path_len + 1 + realpath_len + 1;

が1ファイルのエントリのサイズで、realpath_cache_bucket

typedef struct _realpath_cache_bucket {
	unsigned long                  key;
	char                          *path;
	int                            path_len;
	char                          *realpath;
	int                            realpath_len;
	time_t                         expires;
	struct _realpath_cache_bucket *next;	
} realpath_cache_bucket;

ということで、ファイルをたくさん読むようなプログラムだと、往々にしてパスも深く、ファイル名も長ったらしくなるので、構造体と終端文字で30byteと仮においたとして、だいたい1ファイルあたり100byteくらいでなかろうかと。となると、16K (160ファイルくらい)では足りない気がします。64Kくらい?
また、サイズがオーバーすると、新しいパスはキャッシュされないこともコードから読み取れるので、なるべく余裕を持ったサイズ設定を心がけるべきです。
このキャッシュは、APCなどと違って、各プロセス (ZTSならスレッド)がそれぞれ別に持って、別にキャッシュし、プロセス(or スレッド)が終了まで保持されます。DB系にある、pconnectと同じしくみですね。プロセス(or スレッド)の数の分だけメモリを食うことになりますが、まぁ、100 x 64KB でも640KBなのでそんなに気にしなくても。
APCなどOpcode Cacheを利用している場合は、include/requireに関してはまた違う動きをするので、もしかしたらこっちは呼ばれないかもですが、調べていません。

まとめ

  • PHP5.1からrealpath cacheという仕組みが導入された
  • デフォルト値は16Kと小さめなので、ファイルを多く扱う場合は、キャッシュサイズを大きく設定するほうがよい
    • 64Kくらい?

つづく

深追いしすぎていろいろカオスになってきたので、とりあえずここまででアップ。実はこのネタ自体が別のネタの前ふりだったりするんのですが。。
ということで、続きます。

次はこちら