The Opcache extension has been part of the core for ten years and adds support for byte-code caching of PHP scripts. For a dynamic language such as PHP, a byte-code cache can increase the performance significantly, because it guarantees a script is compiled only once.
The default settings of the Opcache extension already boost PHP performance by a large amount, but you can tinker with the configuration settings to get even more out of it.
A word of warning: This post repeatedly mentions monitoring stats with opcache_get_status(false)
. Because Opcache uses a unique pool of shared memory for every SAPI, you cannot access the webservers statistics from the console. The call has to be made through Apache or PHP-FPM.
Avoid Destructive Caching Behavior When Memory is Too Small
Opcache uses 128 MB of RAM to save the compiled PHP scripts by default and up to 16229 php scripts. This sounds more than enough to store your PHP application scripts, but there are caveats:
- If your application uses code-generation or a PHP file based cache such as Symfony, Doctrine Annotations or FLOW3, then there might be a large amount of scripts that are not part of your source control.
- If you do validate timestamps and your code changes in production, then the old cache entries are marked as “waste”. This waste adds to the memory consumption of Opcache.
- If you use the “new checkout for every release” deployment strategy, then Opcache might cache the same script multiple times in different filesystem locations. This can crowd out the available memory fast.
When Opcache is “full”, under some circumstances it will wipe all cache entries and start over from an empty cache. When this happens and your server gets large amounts of traffic, this can cause a thundering herd problem or cache slam: lots of requests simultaneously generating the same cache entries.
You absolutely want to avoid Opcache to restart with an empty cache.
The algorithm that detects whether Opcache is “full” depends on an interaction between three different INI settings which is not intuitive and currently not documented. It helped to dig through the C code to understand it.
The three relevant ini variables are defined as follows:
opcache.memory_consumption
defaults to 128 (MB) for caching all compiled scripts.opcache.max_accelerated_files
defaults to 10.000 cacheable files, but internally this is increased to a higher prime number for undocumented reasons. The maximum is 1.000.000.opcache.max_wasted_percentage
is the percentage of wasted space in Opcache that is necessary to trigger a restart (default 5).
Opcache works on a first-come, first-serve basis for the cache and does not use an eviction strategy such as Least Recently Used (LRU).
Whenever maximum memory consumption or maximum accelerated files is reached Opcache attempts to restart the cache. However if the wasted memory inside Opcache does not exceed the max_wasted_percentage, Opcache will not restart and every uncached script will be re-compiled every request as if there was no Opcache extension available.
This means you absolutely want to avoid Opcache to be full.
To find the right configuration you should monitor the output of opcache_get_status(false)
which you can use to check for the memory, waste and number of used cache keys:
- If
cache_full
is true and a restart is neither pending nor in progress, that probably means that the waste is not high enough to exceed the max waste percentage. You can check by comparingcurrent_wasted_percentage
with the INI variableopcache.max_wasted_percentage
. In this case also the cache hit rateopcache_hit_rate
will drop below >=99%.Solution: Increase theopcache.memory_consumption
setting. - If
cache_full
is true andnum_cached_keys
equalsmax_cached_keys
then you have too many files. When there is not enough waste, no restart will be triggered. As a result there are scripts that don’t get cached, even though there might be memory available. Solution: Increase theopcache.max_accelerated_files
setting. - If your cache is never full, but you are still seeing a lot of restarts, that can happen when you have too much waste or configured the max waste percentage too low. Solution: Increase the
opcache.max_waste_percentage
setting.
To look for inefficient restart behavior you can evaluate the oom_restarts
(related to the opcache.memory_consumption
setting) and hash_restarts
(related to opcache.max_accelerated_files
). Make sure to check when the last restart happened with last_restart_time
statistic.
To find a good value for opcache.max_accelerated_files
you can use this Bash one-liner to get the number of PHP files in your project:
$ find project/ -iname *.php|wc -l 9607
Make sure to account for code-generated and cache files or multiple applications running inside the same FPM worker pool.
Increase Interned Strings Buffer
When parsing PHP files that contain hardcoded strings, Opcache converts them to interned strings in memory, which means that a string “foo” in the code used at different locations and executed in different requests at the same time all point to a single place in memory. This reduces the memory overhead of PHP massively. However the interned strings buffer is only 8 MB large by default. For applications using frameworks it makes sense to increase this significantly to 32MB or 64MB:
opcache.interned_strings_buffer=64
Avoid Unnecessary Filesystem Calls By Disabling Timestamp Validation
With the default settings, whenever a PHP file is executed (for example through include
or require
) Opcache checks the last time it was modified on disk. Then it compares this time with the last time it cached the compilation of that script. When the file was modified after being cached, the compile cache for the script will be regenerated.
This validation is not necessary in production, when you know the files never change. To disable timestamp validation add the following line to your php.ini:
opcache.validate_timestamps=0
With this configuration you have to make sure that during a deployment, all the caches get invalidated. There are several ways to ensure this happens:
- Restart PHP FPM or Apache – The sledgehammer method of invalidating files in Opcache has the downside that it might lead to aborted requests and a very small amount of time where requests get lost.
- Calling
opcache_reset()
, which is tricky because it has to be called in a script executed by Apache or PHP-FPM to affect the webservers Opcache. You can add a special endpoint to your application and secure it using a secret hash. - Changing the document root and reloading the Nginx webserver configuration. Nginx completes all the currently running requests and starts new workers in parallel that serve requests with the new configuration.
- For Apache, Rasmus has a blog post detailing their approach at Etsy.
Careful with the last two approaches because using them results in making the available Opcache memory “full” very fast. See the previous section for more information on how memory configuration works.
For both cases you might need a maintenance script that calls opcache_invalidate($file, true)
on all scripts in old deployments or opcache_reset()
directly. Otherwise old deployments crowd out the cache for new ones and you might end up with a full cache and an unaccelerated application.
Putting Everything Together
If you combine all the results you get the following php.ini configuration for Opcache in production:
opcache.memory_consumption=256 # MB, adjust to your needs
opcache.max_accelerated_files=10000 # Adjust to your needs opcache.max_wasted_percentage=10 # Adjust to your needs opcache.validate_timestamps=0 opcache.interned_strings_buffer=64
We tested this on the Tideways demo application server, which was running with a “full” Opcache because of the many different PHP applications running inside PHP-FPM there. The Symfony Sylius demo application showed the following considerable improvements, because now all scripts fit into Opcache. Register for the Tideways demo to directly view this event yourself.
We also tested the Opcache configuration on the Profiler itself. In this case the cache was not already full, but timestamp validation was enabled.
With 180.000 requests compared before and after the release we can be very certain that the improvement of roughly 2ms can be attributed to the disabled timestamp validation.
Conclusion
Even though Opcache already provides a signifcant boost in PHP performance, you can still improve on it. This blog post expands on the small information available in the php documentation on opcache_get_status and the opcache configuration.