PHP OPcache を正しく理解して最適化したい
はじめに
PHP は、リクエストごとにソースコードをそのまま実行しているわけではありません。
実際には、パースや構文解析を経て生成された Opcode(Zend VM 用の中間命令)を実行しています。
OPcache は、この Opcode を共有メモリ上に保持し、再利用することで、
毎回発生するパースやコンパイル処理を省略し、PHP の実行性能を大きく向上させる仕組みです。
そのため、
- OPcache が「何を」キャッシュしているのか
- どの粒度(ファイル単位なのか、関数単位なのか)で保持されるのか
といった PHP 内部の実行フローを理解していないと、
OPcache の設定値やステータスの意味を正しく解釈することができません。
PHP-FPM 環境における OPcache のプロセスモデル
php-fpm 環境では、OPcache の共有メモリは master プロセス起動時に確保され、複数の worker プロセス間で共有されます。
master
├─ worker
├─ worker
└─ worker
この構造のため、opcache.memory_consumption などのメモリ関連設定は プロセス起動時にのみ反映されます。稼働中に動的に増やすことはできず、設定変更後には php-fpm の再起動が必要です。
CLI と FPM では OPcache は完全に別物
CLI と PHP-FPM では、使用される php.ini も OPcache も 完全に分離されています。
そのため、CLI で確認した OPcache の状態や設定が、Web リクエスト側に反映されないケースは珍しくありません。
OS
└─ PHP
├─ SAPI: cli
│ └─ OPcache(CLI専用)
└─ SAPI: fpm-fcgi
└─ OPcache(Web用)
インフラ構成と OPcache メモリの関係
OPcache はディスクではなく RAM 上の共有メモリを使用するため、EC2 インスタンスや ECS タスクに割り当てられたメモリ量が、そのまま利用可能な上限となります。ECS や Fargate のようなコンテナ環境では、タスクごとに OPcache が分離されるため、スケールアウト時にはキャッシュが各タスクに分散されます。この特性を考慮せずに OPcache のメモリサイズを増やすと、アプリケーションプロセスが使用できるメモリを圧迫し、結果として OOM Kill を引き起こすリスクがあります。
OPcache 最適化の3つの観点
OPcache の状態は、「memory・files・strings」という3つの観点から判断できます。これらの状態はいずれも opcache_get_status() を使って確認可能であり、数値をもとにボトルネックを特定し、必要な設定だけを調整していくことが重要です。
memory の確認
$status = opcache_get_status(false)['memory_usage'];
Array
(
[used_memory] => 123456789
[free_memory] => 987654321
[wasted_memory] => 1234567
[current_wasted_percentage] => 1.23
)
| key | 意味 |
|---|---|
| used_memory | Opcode 等で使用中 |
| free_memory | 未使用 |
| wasted_memory | 断片化 |
| current_wasted_percentage | wasted率 |
wasted_memory(断片化)とは何か
wasted_memory は、一度使われたものの、再利用できなくなったメモリ領域を指します。
以下のような流れで断片化が発生します。
- ある PHP ファイルの opcode がメモリ上に配置される
- その PHP ファイルが更新され、再コンパイルされる
- 元の領域が解放される
- 新しい opcode が、元の領域より大きいサイズだった場合、その場所に収まらない
- 結果として「空いているが使えない穴」が残る
この 使えない穴 が wasted_memory です。
一方、free_memory は まだ一度も使われていない連続したメモリ領域を指します。
「空いている」という点では同じでも、再利用できるかどうかが両者の違いです。
files(キャッシュファイル数) の確認
$status = opcache_get_status(false)['opcache_statistics'];
Array
(
[num_cached_scripts] => 15842
[max_cached_scripts] => 20000
[hits] => 12345678
[misses] => 12345
)
| key | 意味 |
|---|---|
| num_cached_scripts | 実キャッシュ数 |
| max_cached_scripts | 上限 |
| hits / misses | 効いているか |
num_cached_scripts(実キャッシュ数)
num_cached_scripts は、現在 OPcache に格納されている PHP ファイルの数を表します。 ここで言う「PHP ファイル」には、以下のようなものがすべて含まれます。
-
require/includeによって読み込まれたファイル - オートローダ経由でロードされたクラスファイル
- フレームワーク内部で使用されている PHP ファイル
num_cached_scripts が増えるタイミング
num_cached_scripts が増加するのは、主に、以下のようなタイミングです。
単なるアクセス数の増加ではありません。
- 初回アクセス時(まだ OPcache が空の状態)
- 新しい PHP ファイルが 初めてロードされたとき
たとえば、別のエンドポイントや管理画面に初めてアクセスした際、
それまで使用されていなかったクラスやファイルが require されると、
その瞬間に num_cached_scripts が増加します。
strings(interned strings) の確認
$status = opcache_get_status(false)['interned_strings_usage'];
Array
(
[buffer_size] => 33554432
[used_memory] => 28765432
[free_memory] => 4789000
[number_of_strings] => 123456
)
| key | 意味 |
|---|---|
| buffer_size | 設定値 |
| used_memory | 使用中 |
| free_memory | 空き |
| number_of_strings | 登録数 |
interned strings とは
interned strings は、同じ文字列を1度だけメモリに保存し、参照を使い回す仕組みです。
対象となるのは、「クラス名」「メソッド名」「定数名」「名前空間」などです。
フレームワークを使うと、これらの文字列は大量に発生します。
buffer_size
interned strings 用に確保された専用メモリの総量
number_of_strings
登録されている ユニークな文字列の数
number_of_strings が増えると used_memory が増え、free_memory が減少していきます。free_memory が極端に少ない場合は、opcache.interned_strings_buffer の増加を検討します。
Discussion