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:
- Dodge the thundering herd with file-based Opcache in PHP7
- Zend OPCache – opcache.enable_cli 1 or 0? What does it do?
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. usingcachetool
. If usingcachetool
, 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 becausecachetool
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 deletionrm -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.