fbpx

PHP / Server Setup

PHP OPcache (ex. Zend OPcache)

by , , revisited on


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

PHP is fast. But not when you use CMS with hundreds of small PHP files. WordPress, Magento, Joomla are all great examples of popular CMS solutions, but you can find their performance extremely slow. This is due to the fact, that PHP is interpreted language. Each time a website page is requested, all those files have to be parsed and executed.

You can increase the performance of the PHP engine by creating precompiled bytecode cache for all PHP files. This way you remove the need for PHP to load and parse scripts on each request.

Install Zend Opcache on CentOS/RHEL 7

These commands install PHP 8 with OPcache extension:

yum -y install epel-release
yum -y install http://rpms.famillecollet.com/enterprise/remi-release-7.rpm
yum -y install yum-utils
yum-config-manager --enable remi-php80
yum -y install php-cli php-fpm php-common php-opcache

Optimal settings

The truly most important settings for OPcache are the following.

opcache.validate_timestamps

This setting is crucial to be set to 0 on production. This way you disable constant checking of changes to PHP scripts.
This checking, if not disabled, is a huge performance bottleneck for the OPcache.

You can set this setting in a new file, e.g. /etc/php.d/zzzz.ini with custom settings:

opcache.enable=1
opcache.validate_timestamps=0

opcache.memory_consumption

The opcache.memory_consumption setting basically sets a cap on how many compiled scripts can be stored/cached in memory.

This setting can be set individually for a PHP-FPM pool, e.g. /etc/php-fpm.d/example.com.conf:

opcache.memory_consumption=128

The value is in megabytes. For frameworks with a huge number of PHP files, like Magento 2, we suggest 256.

File-based OPCache

Versions of PHP >= 7.0 are capable of storing OPcaches in the file system, in addition to memory. You need to explicitly enable this feature during compilation and in PHP configuration.

Remi builds of those PHP versions are compiled with support for file-based OPcache.

Configuring a separate PHP-FPM pool’s cache directory for file-based OPCache works and PHP-FPM will save the respective .bin cache files using appropriate users between PHP-FPM pools. For each pool running under a different user, you’d have:

php_admin_value[opcache.file_cache] = /home/example/.cache/opcache

You’d need to create /home/example/.cache/opcache beforehand and make sure it’s chown-ed with the appropriate user.

And, of course, you can enable PHP OPcache for CLI by passing appropriate options, e.g.:

/path/to/php -d opcache.file_cache_only=1 -d opcache.enable_cli=1 -d opcache.file_cache=/home/username/.cache/opcache /path/to/cron.php

The obvious benefits are:

  • PHP CLI programs (composer, n98-magerun2, etc.) can now benefit persistent file-based OPCache and run faster
  • Cron PHP scripts, being a subset of PHP 7 CLI programs, can run faster as well
  • You can safely restart PHP-FPM: OPcaches will be copied to memory from the file system. Now there’s less time that your website is hit against raw unparsed PHP scripts

Some more insights on the benefits of file-based OPcaches is available here:

Security concerns of OPcache cache poisoning can be found here.

In case of a file based OPcache, opcache_reset will not reset it. You need to reset it by simply deleting it on the file system. The proper way seems to be the most performant one, which is calling rm -rf /home/example/.cache/opcache/*, or:

// Check if file cache is enabled and delete it if enabled
if ( ini_get( 'opcache.file_cache' ) && is_writable( ini_get( 'opcache.file_cache' ) ) ) {
    $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( ini_get('opcache.file_cache'), RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST );
    foreach ( $files as $fileinfo ) {
        $todo = ( $fileinfo->isDir() ? 'rmdir' : 'unlink' );
        $todo( $fileinfo->getRealPath() );
    }
}

However, see below for a more efficient/consistent approach.

Caveats of file-based OPCaches

… are greatly outlined by iquito at the discussion thread:

You would need to delete all cache files and then call opcache_reset(). Race conditions can easily occur: between the rm and opcache_reset() new files could have already been created. Especially when you deploy an application some files will have changed and there is no “atomic” way to delete the file cache and the opcache in sync. PHP-FPM might also have issues if it tries to read a file that has just been deleted

Consistent clearing of file-based OPcache

Due to the caveats above, the ideal way to clear cache when file-based OPcaches are in place is by the following workflow:

  • Move the file-based cache location on the same file system for deletion (instead of rm its contents), e.g. mv /path/to/opcache /path/to/opcache.rm, thus avoiding race condition between web-based OPCache creation and file-based. Because we make file-based temporarily unusable
  • Clear web-based OPcache, via opcache_reset(), e.g. using cachetool. If using cachetool, you must invoke it with a different, existing directory, so it won’t choke on the currently missing cache directory e.g. php -d opcache.enable_cli=0 -d opcache.file_cache=/tmp $(which cachetool) opcache:reset. That is required because cachetool itself is written in PHP and requires a “working” PHP configuration.
  • Re-create file-based cache location, e.g. mkdir -p /path/to/opcache and remove the one pending deletion rm -rf /path/to/opcache.rm

It is crucial that the mv operation refers to the same file system in order for it to be fast.

For WordPress, consistent clearing of OPcache upon updates is implemented in the OPcache Reset plugin.

A significantly less safe alternative is rm (removing) the OPcache directory itself, directly, clearing via opcache_reset(), then re-creating the file-based cache location. Removing the OPcache directory will likewise make it unusable for the time being that we clear the web-based cache. However, directly removing the OPcache directory is subject to race conditions, because removing a directory can be slow, as opposed to simply renaming it.

Persisting and auto-clearing OPcache directory under ~/.cache

The ~/.cache/opcache location for your OPcache is great, however, the parent directory is often mounted to tmpfs (in RAM) for performance reasons. Thus there is a need to always make sure that the directory exists upon boot time. Mind that PHP-FPM doesn’t bother to automatically create it.

Of course, it is worth noting that it makes little sense to have file-based OPcache in memory, other than a “backup” for existing shared memory OPcache.

Create a file /etc/tmpfiles.d/php-opcache.conf with contents:

d /home/foo/.cache/opcache  700  foo foo 365d
d /home/bar/.cache/opcache  700  bar bar 365d

Where foo and bar are two system users which run PHP-FPM pools. We auto-clear the caches every year, as they might grow up heavily if PHP files are deleted and added constantly.

This will ensure that the ~/.cache/opcache is created upon boot time, even if ~/.cache is mounted on tmpfs, or if it was erroneously deleted.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: