Привет. 

Сегодня хочу поговорить о том, как ускорить приложение через конфигурирование 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, то увидите стартовое количество воркеров

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

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