Fine-Tune Your Opcache Configuration to Avoid Caching Surprises

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 comparing current_wasted_percentage with the INI variable opcache.max_wasted_percentage. In this case also the cache hit rate opcache_hit_rate will drop below >=99%.Solution: Increase the opcache.memory_consumption setting.
  • If cache_full is true and num_cached_keys equals max_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 the opcache.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.

Comparing PHP Performance before and after Opcache Configuration OptimizationsComparing PHP Performance before and after Opcache Configuration Optimizations

We also tested the Opcache configuration on the Profiler itself. In this case the cache was not already full, but timestamp validation was enabled.

Comparing PHP Performance before and after Opcache Configuration OptimizationsComparing PHP Performance before and after Opcache Configuration Optimizations

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.

Benjamin Benjamin 23.07.2015