PHP: кеширование PHP-скриптов – настройка и тюнинг OpCache

Автор: | 25/12/2019

OpCache увеличивает производительность PHP сохраняя уже скомпилированные скрипты PHP в общей памяти, таким образом уменьшая работу для PHP-FPM, которому приходится меньше выполнять загрузку, чтение и обработку PHP при поступлении новых запросов.

Workflow обработки новых запросов выглядит так:

Исходный код проекта – тут>>>.

Enable OpCache

Редактируем php.ini файл, в данном примере это будет /etc/php/7.3/fpm/php.ini.

Добавляем в блок [opcache]:

[opcache]
opcache.enable = 1

Проверяем синтаксис и перезагружаем конфиги PHP-FPM:

[simterm]

$ php-fpm7.3 -t && systemctl reload php7.3-fpm.service
[24-Dec-2019 13:08:08] NOTICE: configuration file /etc/php/7.3/fpm/php-fpm.conf test is successful

[/simterm]

Проверяем phpinfo():

Другой вариант – проверить через вызов opcache_get_status():

<?php
var_dump(opcache_get_status());
//var_dump(opcache_get_configuration());
?>

Или вывести сразу в JSON:

<?php
//var_dump(opcache_get_status());
//var_dump(opcache_get_configuration());
echo json_encode(opcache_get_status());
?>

Все допустимые функции – тут>>>.

Проверяем:

[simterm]

$ curl -s https://stage.example.com/opcache-status.php | head -n 20
array(8) {
  ["opcache_enabled"]=>
  bool(true)
  ["cache_full"]=>
  bool(false)
  ["restart_pending"]=>
  bool(false)
  ["restart_in_progress"]=>
  bool(false)
  ["memory_usage"]=>
  array(4) {
    ["used_memory"]=>
    int(40959816)
    ["free_memory"]=>
    int(93256808)
    ["wasted_memory"]=>
    int(1104)
    ["current_wasted_percentage"]=>
    float(0.00082254409790039)
  }

[/simterm]

array(8) { ["opcache_enabled"]=> bool(true) – окей, активен.

Заодно видим тут память – сейчас обсудим.

Ещё один вариант проверить настройки OpCache –  с помощью CLI:

[simterm]

$ php --ri 'Zend OPcache' 

Zend OPcache 

Opcode Caching => Disabled 
Optimization => Disabled 
SHM Cache => Enabled 
File Cache => Disabled 

Directive => Local Value => Master Value 
opcache.enable => Off => Off 
opcache.use_cwd => On => On 
opcache.validate_timestamps => On => On 
opcache.validate_permission => Off => Off 
opcache.validate_root => Off => Off 
opcache.dups_fix => Off => Off 
opcache.revalidate_path => Off => Off 
opcache.log_verbosity_level => 1 => 1 
opcache.memory_consumption => 128 => 128 
opcache.interned_strings_buffer => 8 => 8 
opcache.max_accelerated_files => 10000 => 10000 
opcache.max_wasted_percentage => 5 => 5 
opcache.consistency_checks => 0 => 0 
opcache.force_restart_timeout => 180 => 180 
opcache.revalidate_freq => 2 => 2 
opcache.file_update_protection => 2 => 2 
opcache.preferred_memory_model => no value => no value 
opcache.blacklist_filename => no value => no value 
opcache.max_file_size => 0 => 0 
opcache.protect_memory => 0 => 0 
opcache.save_comments => 1 => 1 
opcache.optimization_level => 0x7FFEBFFF => 0x7FFEBFFF 
opcache.opt_debug_level => 0 => 0 
opcache.enable_file_override => Off => Off 
opcache.enable_cli => Off => Off 
opcache.error_log => no value => no value 
opcache.restrict_api => no value => no value 
opcache.lockfile_path => /tmp => /tmp 
opcache.file_cache => no value => no value 
opcache.file_cache_only => 0 => 0 
opcache.file_cache_consistency_checks => 1 => 1 
opcache.huge_code_pages => Off => Off

[/simterm]

OpCache tunning

Все доступные параметры – тут>>>, документация – тут>>>.

OpCache memory

Начнём с памяти.

Возвращаемся к нашему статусу:

[simterm]

root@bttrm-stage-app-2:/home/admin# curl -s https://stage.example.com/opcache-status.php | jq '.memory_usage'
{
"used_memory": 101210560,
"free_memory": 24263968,
"wasted_memory": 8743200,
"current_wasted_percentage": 6.514191627502441
}

[/simterm]

Тут видим использование выделенной памяти, которой по умолчанию 128 мегабайт, см. opcache.memory_consumption.

  • used_memory – integer; bytes of memory used
  • free_memory – integer; bytes of memory available for cache
  • wasted_memory – integer; bytes of memory used by invalid or outdated code – OUDATED code
  • current_wasted_percentage – float; The percentage of wasted opcode cache memory out of total memory available

Wasted memory тут – например, у вас есть код на сервере, который уже закеширован OpCache.

Потом вы задеплоили новый код, OpCache выполнил проверку timestamps, увидел, что скрипт изменился – значит память, выделенная под кеш предыдущей версии скрипта, становится wasted – “зря потраченной“, т.к. кеш уже не валиден.

Значение wasted memory сбрасывается, когда OpCache перезапущен, или когда % wasted паамяти достигает лимита max_wasted_percentage.

Посчитаем: 101210560 занято кешем сейчас + 24263968 свободной + 8743200 wasted:

[simterm]

root@bttrm-stage-app-2:/home/admin# echo "(101210560+24263968+8743200)/1024/1024" | bc
128

[/simterm]

Наши 128 метров из дефолтных значений, всё сходится.

Избегайте OpCache reset

Проблема заключается в том, что если OpCache забьёт всю доcтупную память (лимит в memory_consumption)  и/или сработает лимит max_wasted_percentage и/или max_accelerated_files – то OpCache сбросит весь кеш, и начнёт заполнять его заново.

При этом, даже при условии, что memory_consumption и max_accelerated_files будут полностью заняты, но в max_wasted_percentage лимит ещё не достигнут – OpCache не выполнит reset кеша, и все PHP-скрипты, которые ещё не добавлены в кеш, будут обрабатываться в обычном порядке, т.е. будут каждый раз комплироваться в PHP-FPM.

Кроме того, могут возникнуть проблемы с блокировкиами записи в кеш, т.к. множество запросов к скриптам одновременно начнут зановсить в кеш новые записи об этих скриптах.

Потому – мониторим opcache_get_status(), и проверяем следующие значения:

  • Если cache_full, т.е. занята вся память под кеш из memory_consumption, но при этом нет “restart_pending” или “restart_in_progress” в opcache_get_status() – возможно, у вас слишком низкий уровень max_wasted_percentage.
    Сравните значения max_wasted_percentage в конфиге и current_wasted_percentage в opcache_get_status()
    Решение: увеличиваем значение memory_consumption
  • Если cache_full == true, а значение num_cached_keys == max_cached_keys – у вас слишком много файлов в кеше.
    При этом если среди них очень мало wasted данных – то резет кеша не будет вызыван, место под новые скрипты в кеше не появится, и новые файлы будут обрабатываться мимо кеширования вообще.
    Решение: увеличить max_accelerated_files
  • Если cache_full не появляется никогда, но OpCache постоянно рестартится – то у вас либо слишком много файлов в wasted состоянии в кеше, либо max_waste_percentage слишком низкий.
    Решение: увеличить max_waste_percentage

Проверка рестартов

Самый удобный способ проверить рестарты – через тот же статус:

[simterm]

root@bttrm-stage-app-2:/home/admin# curl -s https://stage.example.com/opcache-status.php | jq '.opcache_statistics'
{
"num_cached_scripts": 4814,
"num_cached_keys": 6386,
"max_cached_keys": 16229,
"hits": 22609079,
"start_time": 1577185705,
"last_restart_time": 0,
"oom_restarts": 0,
"hash_restarts": 0,
"manual_restarts": 0,
"misses": 5555,
"blacklist_misses": 0,
"blacklist_miss_ratio": 0,
"opcache_hit_rate": 99.97543625954769
}

[/simterm]

Тут:

  • oom_restarts: рестарты по достижению лимита памяти – memory_consumption
  • hash_restarts: рестарты по достижению лимитов max_accelerated_files
  • start_time: время запуска в UNIX epoch
  • last_restart_time: время последнего перезапуска в UNIX epoch

Прочие настройки

opcache.use_cwd

Полезно включить, что бы OpCache будет использовать полный путь к файлу скрипта при добавлении его в кеш – это поможет избежать коллизий, когда один и тот же скрипт загружается как единый объект кеша, хотя файлы принадлежат разным проектам на сервере.

Например – у нас целая пачка приложений на Yii, во всех используются аналогичные файлы, т.к. фреймворк один и тот же.

opcache.blacklist-filename

Сюда можно внести например файлы настроек, если они часто изменяются, и любые другие динамически создаваемые-обновляемые файлы.

opcache.revalidate_freq

Как часто в секундах проверять обновились ли файлы. Если задать в 0 – то проверка будет выполняться при каждом запросе к файлу (но см. validate_timestamps).

opcache.validate_timestamps

Проверяем модицикацию файлов файлов с периодом, заданным в revalidate_freq.

Если validate_timestamps задан в ноль, то проверка не будет выполняться вообще – имеет смысл на Production-окружении, т.к. там код будет обновляться редко (но в таком случае требуется либо перепзапуск OpCache при каждом деплое/апдейте файлов, либо выполнять явный вызов reset()).

opcache.max_accelerated_files

Уже упоминалась, но ещё раз – максимальное кол-во файлов, который OpCache будет хранить. Для лучшей производительности используйте просыте числа – 223, 463, 983, 1979, 3907, 7963, 16229, 32531, 65407, 130987.

В идеале – должно быть больше, чем общее кол-во файлов всех проектов:

[simterm]

root@bttrm-stage-app-2:/home/admin# find /data/projects/ -name *.php -type f | wc -l
206320

[/simterm]

Тогда как лимит по дефолоту задан в 10000, а закешировано сейчас:

[simterm]

root@bttrm-stage-app-2:/home/admin# curl -s https://stage.example.com/opcache-status.php | jq '.opcache_statistics.num_cached_scripts'
2521

[/simterm]

opcache.interned_strings_buffer

PHP использует Interned Strings для хранения данных типа string: для одинаковых строк создаётся одна переменная, которая хранит значение, а во всех остальных случаях – используется указатель (pointer) на эту переменную.

Вместо того, что бы создавать такую переменную для каждого процесса PHP-FPM – OpCache создаёт одну перменную для всех процессов, которые используют одну и ту же строку.

Значение задаётся в МБ, при этом оно будет отнято от значения из opcache.memory_consumption.

opcache.file_cache

Добавляет кеширование в файл. Помогает при перезагрузке сервера или если вся shared память занята (см. /proc/sys/kernel/shmmax).

opcache.enable_cli

Включает поддержку кеширования для PHP CLI. Имеет смысл когда, например, часть скриптов запукается из crontab.

Config summary

Соберём всё вместе:

opcache.memory_consumption=164            // 164 MB памяти под кеш
opcache.max_wasted_percentage=5           // при 5% wasted данных - будет выполнен reset всего кеша
opcache.max_accelerated_files=7963        // при достижении максимального значения закешированных файлов - будет выполнен reset
opcache.use_cwd=1                         // использовать полный путь для создания униканого имени ключа
opcache.revalidate_freq=0                 // проверять не изменился ли файл при каждом запросе, не играет роли при validate_timestamps=0
opcache.validate_timestamps=0             // никогда не проверять изменился ли файл на диске, пока не будет выполнен reset() или перезагрузка сервера
opcache.interned_strings_buffer=32        // 32 MB под strings-кеш
opcache.file_cache=/data/opcache          // файл(ы) кеша храним в /data/opcache
opcache.enable_cli=0                      // включить OpCache для PHP CLI

OpCache GUI – веб-интерфейс

Для более наглядного представления и работы с OpCache имеется много веб-интерфейсов, например opcache-gui.

Загружаем файл:

[simterm]

$ wget https://raw.githubusercontent.com/amnuts/opcache-gui/master/index.php

[/simterm]

Копируем его в каталог проекта, что бы получить доступ через NGINX:

[simterm]

$ mv index.php /data/projects/projectname/frontend/web/opcache-gui.php

[/simterm]

Открываем в браузере:

Тестирование и графики

И графики потребления CPU (% Load Average сверху) и RAM (нижний график) на двух серверах API-фронтенда нашего бекенда.

Видим, что Load Average прыгал до 2000% без использования кеша, и всего 300-400% – с активным кешированием.

Аналогично память – 25-50% без кеширования, и 21-35% – с кешированием.

В целом – на этом всё.