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

Автор: | 12/25/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:

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

Проверяем 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());
?>

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

Проверяем:

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)
}

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

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

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

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

OpCache tunning

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

OpCache memory

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

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

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
}

Тут видим использование выделенной памяти, которой по умолчанию 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:

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

Наши 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

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

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

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
}

Тут:

  • 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.

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

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

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

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

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.

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

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

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

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

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

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

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

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

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

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