How to increase the PHP App start performance by up to 60% with Opcode Preloading
In the early days, PHP parsed and compiled any file used to serve a request. The parsed/compiled result, which is called opcode, was not reused for further requests.
With PHP 7.4, support for preloading was added, a feature that improves the start performance up to 60% by preloading the most commonly used files from a framework.
In a nutshell, this is how it works:
- For preloading files, a custom PHP script is needed
- The preloading script is executed once after PHP-FPM start/restart
- All preloaded files are stored and available in memory for all requests
- Changes made to preloaded files won't have any effect until PHP-FPM restart
Performance
Using the preloading script from Symfony, I got about 50% speed-up on the first request and up to 60% on a Magento 2 installation.
However, real-world gains depend on the ratio between the bootstrap overhead and the runtime of the code.
Small scripts or microservices with a very lightweight bootstrap will probably not benefit that much.
CLP Opcache Preloader
Some frameworks like Symfony already generate preload scripts that can be used.
Other PHP Apps and frameworks like Laravel, Drupal, Magento, or WordPress don't yet provide a generated preload script that can be used.
We have developed and open-sourced the CLP Opcache Preloader to address this issue, which can be easily modified for your PHP App.
At the end of the preload script, you can add and ignore files:
<?php
$clpPreloader = new ClpPreloader();
$clpPreloader->setDebug(false);
$clpPreloader->paths(realpath(__DIR__ . '/src'));
$clpPreloader->paths(realpath(__DIR__ . '/vendor'));
$clpPreloader->ignore(realpath(__DIR__ . '/vendor/twig/twig'));
$clpPreloader->preload();
Configuration
To enable opcache preloading, you must tell PHP where this file is stored in its php.ini configuration file.
- Open the
php.ini
of your PHP Version:
sudo nano /etc/php/7.4/fpm/php.ini
- Search for
opcache.preload_user
andopcache.preload
and adjust the values:
opcache.preload_user=clp
opcache.preload=/home/cloudpanel/htdocs/www.domain.com/preload.php
If you are using a custom PHP-FPM Pool, change the opcache.preload_user.
If you use the default configuration, use www-data as opcache.preload_user.
- Restart the PHP-FPM service to apply the changes:
sudo systemctl restart php7.4-fpm
Does it work?
After the configuration, we want to know if the opcache preloading is working as expected; for that we can check the output of the php function opcache_get_status().
- Create a test script like
t.php
and put it in your document root:
nano /home/cloudpanel/htdocs/www.domain.com/public/t.php
- Add the following lines of php code:
<?php
echo '<pre>';
print_r(opcache_get_status());
echo '</pre>';
- Restart the PHP-FPM service to clear and preload the opcode cache:
sudo systemctl restart php7.4-fpm
- Open the test script in your browser to see the opcache statistics.
The value of num_cached_scripts is worth checking. The higher this number, the more scripts have been preloaded.
Array
(
[opcache_enabled] => 1
[cache_full] =>
[restart_pending] =>
[restart_in_progress] =>
[memory_usage] => Array
(
[used_memory] => 28023040
[free_memory] => 777282624
[wasted_memory] => 704
[current_wasted_percentage] => 8.7420145670573E-5
)
[interned_strings_usage] => Array
(
[buffer_size] => 6291008
[used_memory] => 4359696
[free_memory] => 1931312
[number_of_strings] => 56348
)
[opcache_statistics] => Array
(
[num_cached_scripts] => 1000
[num_cached_keys] => 1000
[max_cached_keys] => 130987
[hits] => 128
[start_time] => 1611310634
[last_restart_time] => 0
[oom_restarts] => 0
[hash_restarts] => 0
[manual_restarts] => 0
[misses] => 25
[blacklist_misses] => 0
[blacklist_miss_ratio] => 0
[opcache_hit_rate] => 83.660130718954
)
[preload_statistics] => Array
(
[memory_consumption] => 12108632
[functions] => Array
(
[0] => Symfony\Component\Routing\{closure}
[1] => Symfony\Component\Routing\{closure}
[2] => Symfony\Bundle\WebProfilerBundle\Csp\{closure}
[3] => Symfony\Component\VarDumper\Dumper\ContextProvider\{closure}
[4] => Symfony\Component\VarDumper\Cloner\{closure}
[5] => Symfony\Component\Validator\{closure}
[6] => Symfony\Component\Translation\{closure}
[7] => Symfony\Component\String\{closure}
[8] => Symfony\Component\String\{closure}
[9] => Symfony\Component\String\{closure}
[10] => Symfony\Component\String\{closure}
[11] => Symfony\Component\String\{closure}
[12] => Symfony\Component\String\{closure}
[13] => Symfony\Component\String\{closure}
[14] => Symfony\Component\String\{closure}
[15] => Symfony\Component\String\{closure}
[16] => Symfony\Component\String\{closure}
[17] => Symfony\Component\String\{closure}
[18] => Symfony\Component\String\Slugger\{closure}
[19] => Symfony\Component\HttpKernel\EventListener\{closure}
[20] => Symfony\Component\HttpFoundation\Session\Storage\{closure}
[21] => Sensio\Bundle\FrameworkExtraBundle\EventListener\{closure}
[22] => Sensio\Bundle\FrameworkExtraBundle\Security\{closure}
[23] => Sensio\Bundle\FrameworkExtraBundle\Security\{closure}
[24] => Sensio\Bundle\FrameworkExtraBundle\EventListener\{closure}
[25] => Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\{closure}
[26] => Sensio\Bundle\FrameworkExtraBundle\EventListener\{closure}
[27] => Symfony\Bundle\SecurityBundle\Security\{closure}
..........
Limitations
Unfortunately, it's only possible to define one preload script in the php.ini, which can cause conflicts if you are running multiple PHP Apps on the same server with the same PHP Version.
I thought it would be possible to define a opcache.preload script on the PHP-FPM Pool level, but it isn't possible yet.
For multiple PHP Apps, we could just (theoretically) create a new PHP-FPM Pool with its own user and opcache.preload script defined.
Conclusion
The new feature, opcache preloading, which has been introduced with PHP 7.4, can speed up the start performance by up to 60%. It's a great way to improve the startup performance, especially interesting if you are running an Auto Scaling setup where you scale the web servers or containers on demand.
However, it's a new feature and needs intensive testing before using it in production. I recommend enabling opcache preloading during the development and testing phase.