
Привет.
Сегодня хочу поговорить о том, как ускорить приложение через конфигурирование PHP-FPM.
Сейчас самый популярный (из тех с которыми я сталкивался) стек на котором поднимается PHP приложение это веб сервер nginx и процесс-менеджер php-fpm.
Я хочу поднять простое приложение с Laravel проектом, которое устанавливается со всеми параметрами по умолчанию. Попробуем это приложение нагрузить пользователями с помощью простого Javascript скрипта и посмотрим как ему удастся справиться с нагрузкой и как мы можем повысить обрабатываемую нагрузку только конфигурированием php-fpm. В конце статьи можно будет найти ссылку на GitHub и попробовать своими руками.
Для начала посмотрим на стандартную конфигурацию php-fpm и попытаемся понять где могут быть проблемы в производительности с коробки.
Итак, у меня есть простое приложение на PHP с NGINX и PHP-FPM предустановленными в стандартных конфигурациях и маршрут Laravel.

Маршрут симулирует нагрузку через команду засыпания на одну секунду и возвращает простой json ответ.
Так же у меня есть Javascript файл который производит запрос на наш роут. Запускать его мы будем с помощью нагрузочной утилиты k6.

Для того чтобы провести нагрузочное тестирование давайте запустим утилиту k6 с пятью VU (virtual users)
k6 run --vus 5 --duration 30s script.js
Результат нагрузки:

Как видим в строке http_req_duration avg (среднее по всем показателям значение) равно 1.77 сек. Это сама нагрузка 1 секунда + время прохода запроса по сети и работа фреймворка.
Давайте проведём еще один такой же тест но на 10и пользователях
k6 run --vus 10 --duration 30s script.js
Результат нагрузки:

Как видим время возросло почти в два раза.
Давайте проведём еще один тест и будем разбираться в чём же дело.
На этот раз попробуем нагрузить сразу 50ью пользователями наше приложение.
k6 run --vus 50 --duration 30s script.js
Результат нагрузки:

Как видим результат стал совсем грустным. В среднем клиенту приходится ждать ответа от сервера 15.48 секунд. Давайте разбираться в чём дело.
Для начала давайте узнаем какая сейчас конфигурация php-fpm. Сделать это можно с помощью команды php-fpm -tt
Эта команда выведет все параметры php-fpm, самые важные параметры я обвел рамкой.

Самый важный параметр - это pm.max_children он равен сейчас 5. Этот параметр сообщает нашему php-fpm сколько он может максимально запустить дочерних процессов (обработчиков) запросов. Иными словами сколько параллельно процессов будет обрабатывать входящую нагрузку.
Для лучшей наглядности я нарисовал небольшую схему.

Когда в наше приложение одномоментно приходит 50 клиентов они сначала обращаются в наш NGINX, он является просто прокси сервером и пробрасывает запросы сквозь себя на PHP-FPM (за исключением запросов за статическими ресурсами/файлами) и дальше PHP-FPM пытается обработать все запросы с помощью своих процессов (воркеров). Если же ситуация как у нас, когда PHP-FPM располагает только пятью воркерами, то первые 5 клиентов обрабатываются, а остальные 45 становятся в очередь и ждут, когда первые 5 обработаются. Как только первые 5 отработали, следующие 5 зашли на их место и оставшиеся 40 ждут в очереди.
На этом этапе появляется две проблемы. Первая - мы заставляем таким образом ждать клиентов ответа, вторая - если время ожидания будет выше стандартного для NGINX в параметре fastcgi_read_timeout (стандартное 30 секунд) то мы можем получить 504 ошибку от NGINX. Вторую проблему мы можем исправить увеличив время ожид��ния, но это не спасет нас от первой проблемы.
Логичное решение проблемы - просто добавить воркеров для PHP-FPM и это вполне адекватная мысль, но стоит позаботиться о том, чтобы добавить достаточно воркеров и не добавить лишних воркеров, которые займут всю операционную память.
Давайте вернёмся к нашим конфигурационным параметрам и постараемся сделать правильную настройку.
Итак, нас интересуют следующие параметры:
pm = dynamic - php-fpm сам контролирует количество запущенных воркеров, при указанном параметре dynamic php-fpm будет в зависимости от нагрузки добавлять или удалять воркеры. Так же в этом параметре может быть значение static в таком случае количество воркеров будет статическим.
pm.max_children - максимальное количество процессов единовременно работающих. Часто это значение ставится в количестве 4 * на количество CPU. Для того чтобы узнать количество CPU можно воспользоваться командой lscpu.

Более правильно будет еще проверить количество свободной памяти, можно сделать это командой free -hl

Обязательно нужно понимать сколько памяти занимает один воркер.
Это можно сделать с помощью команды htop и посмотреть среднее количество памяти которое занимают воркеры php-fpm

pm.min_spare_servers - минимальное количество процессов в состоянии ожидания. Это количество нужно как резервное в случае внезапного появления нового количества клиентов. Чтобы клиенты не ждали пока новые процессы создадутся резервные процессы подхватят внезапную нагрузку. Значение обычно ставится в 2 * на количество CPU.
pm.max_spare_servers - максимальное количество процессов в состоянии ожидания. В случае если нет нагрузки на приложение php-fpm удалит лишние процессы с целью сохранить оперативную память.
Хорошо, давайте сконфигурируем наш PHP-FPM.
Для начала найдем где находится файл конфигурации с помощью команды
whereis php-fpm

Ставим start_server в 24.
min_spare_servers в 12.
max_spare_servers в 24.
Перезапускаем докер.
И повторим нагрузочное тестирование с 50ью пользователями.
Результат нагрузки:

Как видите результат нагрузки средний 1.6 секунд. Примерно как и был в первом тесте, с пятью пользователями.
Так же еще можно посмотреть как отрабатывает php-fpm при установке pm стратегии в dynamic. Если вы запустите htop, то увидите стартовое количество воркеров

и если запустите тест, то постепенно количество воркеров сначала увеличится до максимально доступного

и после окончания теста снизится до начального.

