PHPのOPcacheについて調べた
はじめに
ふと、PHPのOPcacheってどういう実装になってるんだろと思ったので調べてみることにしました。具体的には、いつ、何を、どこに、どんな形で、キャッシュし、いつキャッシュを使うのかについて調べました。
なお、今回の調査に用いたPHPのバージョンは7.4.10です。
0. そもそもどういう実行パスでコンパイルされるのか
キャッシュ以前にPHPがどうやってコンパイルされるの流れを調べる必要がありました。これがわからないことには、どこから手を付ければいいのかわからないですよね。
ソースコードを見てみたところ、PHPのファイルはどうやら以下の実行パスでコンパイルされることがわかりました。なお、SAPIはfpmである前提です。
OPcacheが関係してくるのは、persistent_compile_file()
っぽいので、今回はここを重点的に調べます。
1. いつキャッシュするのか
persistent_compile_file()
内を見てみると以下の部分に答えがあります。
/* If script was not found or invalidated by validate_timestamps */ [*1] if (!persistent_script) { uint32_t old_const_num = zend_hash_next_free_element(EG(zend_constants)); zend_op_array *op_array; /* Cache miss.. */ ZCSG(misses)++; /* No memory left. Behave like without the Accelerator */ if (ZSMMG(memory_exhausted) || ZCSG(restart_pending)) { SHM_PROTECT(); HANDLE_UNBLOCK_INTERRUPTIONS(); if (ZCG(accel_directives).file_cache) { return file_cache_compile_file(file_handle, type); } return accelerator_orig_compile_file(file_handle, type); } SHM_PROTECT(); HANDLE_UNBLOCK_INTERRUPTIONS(); persistent_script = opcache_compile_file(file_handle, type, key, &op_array); [*2] HANDLE_BLOCK_INTERRUPTIONS(); SHM_UNPROTECT(); /* Try and cache the script and assume that it is returned from_shared_memory. * If it isn't compile_and_cache_file() changes the flag to 0 */ from_shared_memory = 0; if (persistent_script) { persistent_script = cache_script_in_shared_memory(persistent_script, key, key ? key_length : 0, &from_shared_memory); [*3] }
- [*1]: もしキャッシュが見つからなかった/キャッシュがあってもファイルがすでに更新されていたら
- [*2]: ファイル内容をコンパイルし
- [*3]: 共有メモリに保存する
- コンパイル結果を共有メモリ上のハッシュテーブルに保存する。キーは
file_handle->filename
を引数に、accel_make_persistent_key
で生成している - https://github.com/php/php-src/blob/PHP-7.4.10/ext/opcache/ZendAccelerator.c#L1966
- コンパイル結果を共有メモリ上のハッシュテーブルに保存する。キーは
2. 何をキャッシュするのか
zend_persistent_script
構造体をキャッシュします。コンパイル済みのバイトコードだけではなく、キャッシュの一貫性検証に必要な情報など、メタデータ類も含まれています。
typedef struct _zend_persistent_script { zend_script script; zend_long compiler_halt_offset; /* position of __HALT_COMPILER or -1 */ int ping_auto_globals_mask; /* which autoglobals are used by the script */ accel_time_t timestamp; /* the script modification time */ zend_bool corrupted; zend_bool is_phar; zend_bool empty; uint32_t num_warnings; zend_recorded_warning **warnings; void *mem; /* shared memory area used by script structures */ size_t size; /* size of used shared memory */ void *arena_mem; /* part that should be copied into process */ size_t arena_size; /* All entries that shouldn't be counted in the ADLER32 * checksum must be declared in this struct */ struct zend_persistent_script_dynamic_members { time_t last_used; #ifdef ZEND_WIN32 LONGLONG hits; #else zend_ulong hits; #endif unsigned int memory_consumption; unsigned int checksum; time_t revalidate; } dynamic_members; } zend_persistent_script;
コンパイル済みのバイトコードはzend_script
型のscript
メンバの中にあって、これは以下のように定義されている。
typedef struct _zend_script { zend_string *filename; zend_op_array main_op_array; // バイトコード列 HashTable function_table; HashTable class_table; uint32_t first_early_binding_opline; /* the linked list of delayed declarations */ } zend_script;
3. どこにキャッシュするのか
- 基本的には、共有メモリにキャッシュする。ただし、OPcacheの設定によってはファイルにキャッシュすることもある?
4. どんな形で保存しているのか
- ハッシュテーブルのエントリ
- おそらく、Hashtable APIを使っている
5. いつキャッシュを使うのか
persistent_compile_file()
内の以下の部分に答えがあります。
if (file_handle->opened_path) { bucket = zend_accel_hash_find_entry(&ZCSG(hash), file_handle->opened_path); if (bucket) { persistent_script = (zend_persistent_script *)bucket->data; [*1] if (key && !persistent_script->corrupted) { HANDLE_BLOCK_INTERRUPTIONS(); SHM_UNPROTECT(); zend_shared_alloc_lock(); zend_accel_add_key(key, key_length, bucket); zend_shared_alloc_unlock(); SHM_PROTECT(); HANDLE_UNBLOCK_INTERRUPTIONS(); } } }
- [*1]: ハッシュのエントリから
zend_persistent_script
構造体を取り出している箇所がある。ここがハッシュテーブルのエントリからキャッシュを取り出してるところっぽい - これ以降、timestampやchecksumによるキャッシュの一貫性の検証がありますが、これらの検証を突破するとめでたくキャッシュが使われることになります
- 突破できなかった場合は
persistent_script = NULL;
が実行され、キャッシュがなかったことにされます
- 突破できなかった場合は
おわりに
OPcacheは共有メモリにバイトコードおよびその他のメタデータをキャッシュします。また、キャッシュはハッシュテーブルのエントリという形で保持されていることがわかりました。
ソースコードを読んでみたところ、共有メモリやハッシュテーブル周りがよくわからなかったので、別の機会にこれらのコードも調査してみたいと思います。