Оптимизация PHP на Ubuntu: OPcache и PHP-FPM

Оптимизация PHP на сервере сводится в основном к двум вещам: OPcache, который держит скомпилированные скрипты в памяти, и менеджеру процессов PHP-FPM, который решает, сколько запросов выполняется одновременно. В пакетах Ubuntu оба включены, но с настройками для маленькой машины. Инструкция для PHP 8.3 на Ubuntu 24.04 (в 22.04 стоит PHP 8.1: в путях и именах служб пишите 8.1 вместо 8.3). Числа ниже — отправная точка, сверяйте их со своей нагрузкой. Свою версию PHP покажет команда: ls /etc/php/

Настройки OPcache для продакшена

OPcache включён по умолчанию, ему дают 128 МБ памяти и место под 10 000 скриптов. У фреймворка с зависимостями файлов легко бывает больше. Посчитайте PHP-файлы своего сайта и поставьте opcache.max_accelerated_files больше этого числа:

find /var/www/example.com -type f -name '*.php' | wc -l

Ubuntu читает все .ini из /etc/php/8.3/fpm/conf.d/ после php.ini, поэтому свой файл там переживёт обновления пакетов. Создайте его:

sudo nano /etc/php/8.3/fpm/conf.d/99-tuning.ini
/etc/php/8.3/fpm/conf.d/99-tuning.ini
; OPcache opcache.enable=1 opcache.memory_consumption=256 opcache.interned_strings_buffer=16 opcache.max_accelerated_files=20000 opcache.validate_timestamps=0

opcache.validate_timestamps=0 значит, что PHP больше не проверяет, изменились ли файлы, и это экономит обращение к диску на каждый запрос. Цена такая: после каждой выкладки PHP-FPM нужно перезапускать, иначе продолжит работать старый код. Если вы правите файлы на сервере руками или ваша выкладка не умеет перезапускать службы, оставьте opcache.validate_timestamps=1 и поставьте opcache.revalidate_freq=60: тогда PHP проверяет файлы раз в минуту.

sudo php-fpm8.3 -t sudo systemctl restart php8.3-fpm

Проверка должна ответить configuration file /etc/php/8.3/fpm/php-fpm.conf test is successful. Чтобы увидеть, насколько заполнен кэш, сделайте временную страницу с состоянием OPcache, откройте её в браузере и сразу удалите: она показывает пути ваших скриптов всем подряд.

echo '<?php print_r(opcache_get_status(false));' | sudo tee /var/www/example.com/opcache-check.php

Откройте https://example.com/opcache-check.php, посмотрите, что нужно, и удалите файл:

sudo rm /var/www/example.com/opcache-check.php

Если на странице [cache_full] => 1 или в [free_memory] осталось мало, мал opcache.memory_consumption. Если [num_cached_scripts] близко к opcache.max_accelerated_files, мал он. [opcache_hit_rate] после некоторого времени работы сайта должен быть около 100.

JIT в PHP 8.3

JIT компилирует часто выполняемый код в машинный. В PHP 8.3 он выключен, потому что opcache.jit_buffer_size по умолчанию 0. Он помогает коду, который сильно грузит процессор: вычислениям, обработке картинок. Обычный сайт, который в основном ждёт базу данных, выигрывает от него мало, так что замерьте до и после. Чтобы включить, добавьте две строки в тот же 99-tuning.ini и перезапустите PHP-FPM:

/etc/php/8.3/fpm/conf.d/99-tuning.ini
; JIT opcache.jit=tracing opcache.jit_buffer_size=64M

В состоянии OPcache в [jit] появится [enabled] => 1. Память JIT идёт сверх opcache.memory_consumption. Если загружен Xdebug, JIT не работает.

PHP-FPM: режимы pm и как посчитать pm.max_children

Каждый процесс PHP-FPM обслуживает один запрос за раз, так что pm.max_children — это сколько PHP-запросов идёт одновременно. В Ubuntu там 5, большинству сайтов мало. Но слишком много — ещё хуже: если процессы не влезают в память, сервер уходит в подкачку или начинает убивать процессы. Считайте от памяти. Сначала посмотрите, сколько памяти свободно и сколько в среднем занимает один процесс PHP под настоящей нагрузкой:

free -m ps --no-headers -o rss -C php-fpm8.3 | awk '{ sum += $1; n++ } END { print sum / n / 1024 " MB" }'

Дальше так: pm.max_children = память, которую можно отдать PHP / средний размер процесса. Оставьте память системе, базе данных и всему, что ещё есть на сервере. Например, сервер на 4 ГБ с MySQL, который занимает 1,5 ГБ: 0,5 ГБ оставляем системе, PHP отдаём 2 ГБ, при процессах по 60 МБ выходит 2048 / 60 ≈ 34. Округлите вниз, до 30. Цифра из ps учитывает общую память OPcache в каждом процессе, так что настоящий запас чуть больше.

  • pm = dynamic (по умолчанию в Ubuntu) — держит несколько свободных процессов наготове и добавляет новые до pm.max_children. Подходит большинству серверов.
  • pm = static — всегда ровно pm.max_children процессов. Для нагруженного сервера, где работает только этот сайт и памяти хватает на все процессы.
  • pm = ondemand — запускает процессы только на запросы и гасит их через pm.process_idle_timeout (по умолчанию 10 секунд). Для маленького сервера или множества пулов с небольшим трафиком.
sudo nano /etc/php/8.3/fpm/pool.d/www.conf
/etc/php/8.3/fpm/pool.d/www.conf
pm = dynamic pm.max_children = 30 pm.start_servers = 6 pm.min_spare_servers = 4 pm.max_spare_servers = 8 pm.max_requests = 500

pm.start_servers обычно ставят посередине между двумя значениями spare. pm.max_requests = 500 перезапускает каждый процесс после 500 запросов, и утечки памяти в библиотеках не растут бесконечно. По умолчанию там 0, то есть процессы не перезапускаются никогда.

sudo php-fpm8.3 -t sudo systemctl restart php8.3-fpm sudo grep max_children /var/log/php8.3-fpm.log

Строка WARNING: [pool www] server reached pm.max_children setting (30), consider raising it значит, что запросам пришлось ждать свободный процесс. Поднимайте лимит, только если позволяет память, а иначе сначала ищите медленные запросы.

Как найти медленные запросы PHP через slowlog

PHP-FPM умеет записывать стек вызовов каждого запроса, который работает дольше заданного времени: видно скрипт, функцию и строку, где он застрял. По умолчанию это выключено. Добавьте две строки в тот же файл пула:

/etc/php/8.3/fpm/pool.d/www.conf
request_slowlog_timeout = 5s slowlog = /var/log/php8.3-fpm.slow.log
sudo php-fpm8.3 -t sudo systemctl restart php8.3-fpm sudo tail -n 50 /var/log/php8.3-fpm.slow.log

Каждая запись начинается со времени, пула и script_filename, дальше идёт стек вызовов, самый глубокий вызов сверху. Начните с 5 секунд и снижайте порог, когда самые тяжёлые запросы исправлены.

Realpath cache

Готовые пути подключаемых файлов PHP держит в realpath cache, чтобы не спрашивать диск каждый раз. В нынешних версиях PHP ему дают 4 МБ, этого хватает большинству проектов, а записи живут 120 секунд. При большом числе файлов и opcache.validate_timestamps=0 срок подольше сэкономит ещё немного обращений к диску:

/etc/php/8.3/fpm/conf.d/99-tuning.ini
; Realpath cache realpath_cache_size=4096K realpath_cache_ttl=600

После правки перезапустите PHP-FPM. Если задан open_basedir, кэш не работает вовсе. Чтобы увидеть, насколько он заполнен, сделайте так же, как с проверкой OPcache, временную страницу с <?php echo realpath_cache_size();: она покажет занятые байты. Потом удалите её.

Если вы работаете и на Mac

MacWashing, раздел «Сборка мусора»: найдены мусор Xcode, пользовательский кэш, установщики и журналы, к очистке 22,83 ГБ

Сборка мусора в MacWashing находит кэш всех знакомых ей программ, в том числе спрятанный внутри их данных, а ещё журналы, мусор Xcode и остатки удалённых программ. Она показывает, сколько места занимает каждая находка, и очищает всё за один раз. Всё спорное остаётся без галочки, пока вы не решите.

Бесплатная пробная версия · macOS 14 и новее

Вопросы и ответы

Что даёт больше всего при оптимизации PHP?

OPcache, в который влезают все ваши скрипты, и pm.max_children, который помещается в память. Дальше ищите медленные запросы через slowlog: чаще всего время уходит на запросы к базе, а не на сам PHP.

Мои правки не видны на сайте. Почему?

При opcache.validate_timestamps=0 PHP выполняет код из кэша, пока PHP-FPM не перезапустят. Перезапускайте его после каждой выкладки или добавьте команду в скрипт выкладки: sudo systemctl restart php8.3-fpm

Сколько pm.max_children на сервере с 2 ГБ?

Если PHP можно отдать около 1,2 ГБ, а процесс занимает 50 МБ, то около 24. Размер процесса измерьте на своём сайте: тяжёлый фреймворк может занимать 100 МБ и больше на процесс.