Как ускорить приложение за счёт PHP-FPM (няшим FPM conf)
Привет.
Сегодня хочу поговорить о том, как ускорить приложение через конфигурирование 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, то увидите стартовое количество воркеров
и если запустите тест, то постепенно количество воркеров сначала увеличится до максимально доступного
и после окончания теста снизится до начального.