Статья посвящена сравнению производительности различных веб-серверов для приложения, построенного на базе laravel. Ниже будет много графиков и параметров конфигурации и мои личные выводы, никак не претендующие на вселенскую истину. Сам я давно работаю с nginx-unit (+ lumen), однако при получении новых проектов очень часто вижу использование php-fpm. На предложения перейти на nginx-unit, возникает вполне логичный вопрос - "а чем он лучше?". Поиск соответствующих статей в интернете дал мало результатов, в основном это достаточно старые и малоинформативные статьи на зарубежных сайтах, а так же статья от 2019 года на хабре, где проводилось аналогичное сравнение нескольких веб-серверов для приложения на базе symfony. Но, во-первых, php и nginx-unit уже ушли далеко вперед за время, прошедшее после публикации статьи, а во-вторых, меня интересовала именно производительность в связке с laravel и lumen. В связи с этим я собрал простейший тестовый стенд из нескольких веб-серверов и сделал небольшое нагрузочное тестирование, результатами которого и хочу поделиться. Характеристики тестового стенда: CPU: AMD Ryzen 9 5900X 12-Core RAM: DDR4 4000 MHz 32GiB x2 SSD: Samsung SSD 980 PRO 500GB nvme OS: xubuntu 20.04 Приложения для тестирования: Laravel 8.69.0 Laravel является одним из наиболее популярных фреймворков для php. Из коробки содержит в себе огромное количество функционала, позволяющего решать почти любые задачи бизнеса в разумные сроки. При этом он достаточно гибок, и почти любую задачу можно было реализовать далеко не 1 способом, ровно как и спроектировать само приложение (правда здесь кроется и минус - задачу можно решить так, что потом еще и куча проблем добавится от поддержки говнокода, до падения производительности и утечек памяти). Lumen 8.3.1 Облегченная версия Laravel, созданная в основном для реализации api-приложений. В части функциональности сохранил все самые важные компоненты Laravel, а то, что не является критически важным (например, фасады), урезано. При этом он более производителен чем Laravel, но далеко не все задачи на нем решать выгодно. Как это обычно бывает, для каждой конкретной задачи есть свой оптимальный инструмент. Для одних задач выгоднее использовать laravel, для других lumen, для третьих php использовать не выгодно вовсе. Веб серверы: Php-fpm Стандартный менеджер php процессов. Для каждого запроса требуется инициализация фреймворка. Nginx unit Веб-сервер приложений, разработанный командой nginx. Для каждого запроса требуется инициализация фреймворка. Laravel-octane Строго говоря, это не веб-сервер, а пакет управления приложением от команды laravel. А вот под капотом использует веб-сервер Swoole или RoadRunner . Фреймворк инициализируется при первом запросе, далее хранится в памяти и не реинициализируется при последующих запросах. Версия php во всех сборках: 8.0.12 Инструменты для тестирования: Yandex tank для проведения нагрузочного тестирования. Используется самый свежий из доступных docker-образ. Telegraf (входит в набор инструментов yandex tank) для сбора статистики по используемым приложениями ресурсам компьютера. Overload (так же из комплекта yandex tank) для построения графиков. Приложения Все приложения имеют стандартную конфигурацию. Логирование ошибок отключено. Кэш собирается при первом обращении только для компонентов laravel/lumen (таких, как роутер и т.п.), для контроллеров кэша нет. У приложения 1 эндпоинт, который при обращении обрабатывает реквест, и возвращает встроенный json респонс, содержащий случайное число, параметр конфигурации, статическую строку и заголовки запроса. Пример ответа сервиса: Все приложения поднимаются в docker-контейнерах. Во всех приложениях используется кастомный php.ini, со следующими параметрами: Отдельные изменения, если таковые имеются, описаны для каждого приложения. Для тестирования php-fpm используется дополнительный контейнер с nginx, поскольку php-fpm работает через протокол FastCGI. Используется официальный образ nginx:1.19.6-alpine. nginx.conf default.conf Подопытные кролики, они же собранные контейнеры 1. nginx + php-fpm + laravel Php-fpm: используется официальный образ php:8.0.12-fpm Изменения php.ini: Тут сразу стоит оговориться: изначально планировалось что каждому веб-серверу будет выделено 16 воркеров по числу логических ядер (об этом ниже), но с php-fpm возникли проблемы в процессе тестирования, пришлось выдавать ему больше воркеров. 2. nginx + php-fpm + lumen Конфигурация полностью идентична "nginx + php-fpm + laravel" 3. nginx-unit + laravel Так вышло, что немногим ранее я уже собирал контейнер с nginx-unit для php8, поскольку нормально работающих контейнеров, отвечающих моим производственным задачам не было. Не долго думая, именно этот контейнер я и использовал как основу, чуть доработав под текущее тестирование. Можно заметить, что сборка производилась на базе ubuntu:hirsute, поскольку с apline я не очень дружу, да и разбираться тогда времени с ним не было. Нужна была сборка здесь и сейчас, при этом не планировалось что она дойдет до прода в первозданном виде. Хотя, учитывая результаты тестов, теперь может и дойти. Параметры nginx-unit: 4. nginx-unit + lumen Конфигурация полностью идентична "nginx-unit + laravel" 5. laravel-octane (swoole) + laravel Добавить к тестируемым приложениям octane меня побудило сообщение Тейлора (автора laravel) в твиттере. Меня смутили его утверждения "октан быстрее люмена" и "немного увеличить скорость". В связи с чем к тестам был добавлен наскоро собранный контейнер с laravel-octane, работающим на основе swoole. Laravel-octane поддерживает помимо swoole еще и roadranner, и изначально я планировал протестировать и его. Но как ни странно, мне не удалось заставить работать последний нормально даже на костылях внутри докера (если быть точнее, удалось, но автодеплой он не переживал, только ручной старт), в связи с чем в последствие я от него отказался. Тем не менее, swoole мне было достаточно. Кроме того, Laravel-octane+swoole добавляют дополнительные инструменты, которые могут пригодиться при решении ряда стоящих передо мной задач. .env Все остальные параметры передаются прямо в команде запуска сервера: Yandex tank Для тестирование было принято решение использовать 2 различных профиля нагрузки, отражающих 2 ситуации: "стандарт" для ежедневных часто встречающихся нагрузок, "стресс" для понимания границ работоспособности системы. Задача теста - понять насколько эффективно приложение справится со штатными нагрузками, с которыми я встречаюсь постоянно. Идея теста в том, чтобы определить на что способен каждый конкретный веб-сервис и какое количество пользователей он способен выдержать стабильно и без сбоев. Профиль нагрузки: Таймаут 2 секунды был выбран не случайно. Во-первых, в в api-приложениях если эндпоинт отвечает слишком долго, чаще всего это бывает фатально для работоспособности приложения (я часто применяю api-gateway в рамках микросервисной архитектуры, и в нем всегда жестко задаю таймаут на запрос в зависимости от задач от 1 до 3 секунд, не более. Но при этом, если эндпоинт действительно ворочает тяжелую логику - я всегда вытаскиваю подобные вещи в джобы). Во-вторых, при тестировании php-fpm yandex tank'у с дефолтным таймаутом 11 секунд не хватало ресурсов для обеспечения нагрузки, т.к. он слишком долго ожидал ответа (об этом далее). Telegraf С ним все просто, создана минимальная конфигурация для сбора данных о потребляемых ресурсах Для тестирования все участники действия были жестко ограничены по количеству логических ядер процессора: Контейнер с приложением: 16 ядер Nginx (для тестов с php-fpm): 2 ядра Yandex tank: 4 ядра (на 2 ядрах нехватало ресурсов для работы с php-fpm даже с уменьшенным таймаутом) Чрезвычайно важный и крайне приоритетный процесс проталкивания /dev/zero в /dev/null (вопросы излишни, мой личный костыль, чтобы упростить себе жизнь): строго 1 полностью загруженное ядро Все процессы ОС: оставшееся 1 ядро, фактически свободное, про запас Все приложения тестировались с "холодным стартом", т.е. эндпоинт не подогревался, кэш не собирался до запуска теста. Далее пойдут графики тестирования. Если нет графика ошибок - значит их не было. График нагрузки на память не прикладывал, чтобы не увеличивать объем статьи до бесконечности. В синтетических тестах память почти не потребляется, и разница минимальна. Примерно по тем же соображениям у контейнеров не было ограничений по используемой памяти, благо ее хватает. Профиль нагрузки "стандарт" 1. nginx + php-fpm + laravel Детальный отчет: https://overload.yandex.net/479324 2. nginx + php-fpm + lumen Детальный отчет: https://overload.yandex.net/479325 3. nginx-unit + laravel Детальный отчет: https://overload.yandex.net/479330 4. nginx-unit + lumen Детальный отчет: https://overload.yandex.net/479332 5. octane (swoole) + laravel Детальный отчет: https://overload.yandex.net/479339 Перцентили времени ответа (ms)  99% 98% 95% 90% 85% 80% 75% 50% HTTP OK % nginx + php-fpm + laravel 60 59 56 52 48 46 45 44 100 nginx + php-fpm + lumen 18 18 17 16 16 15 15 14 100 nginx-unit + laravel 7.6 7 6.5 5.8 5.3 5.2 5.2 4.059 100 nginx-unit + lumen 1.930 1.870 1.640 1.520 1.460 1.410 1.320 1.070 100 octane (swoole) + laravel 1.230 1.200 1.160 1.110 1.050 1.010 0.980 0.800 100 Профиль нагрузки "стресс" 1. nginx + php-fpm + laravel Детальный отчет: https://overload.yandex.net/479202 Тестировать пришлось несколько раз. Первый раз, когда php-fpm было выделено 16 воркеров, а nginx имел настройки по умолчанию, вся эта сборка схлопнулась в черную дыру и на 120 rps начала орать об ошибках и недостаточном количестве воркеров (причем оба одновременно). После этого nginx был перенастроен на адекватную конфигурацию, а php было выделено 100 и 250 воркеров (2 теста, в итоге остановился на 100, для 250 не хватало логических ядер). Поскольку тестирование этим профилем нагрузки я проводил первым, в дальнейшем эта конфигурация и была использована на всех остальных тестах. Кроме того, я столкнулся с тем, что на отметке ~120 rps php-fpm резко перестает справляться с нагрузками, запросы зависают, и yandex tank с дефолтным таймаутом запроса в 11 секунд начинает загибаться, ему банально не хватает ресурсов для обеспечения требуемого количества запросов. В связи с этим я выделил танку 4 ядра (изначально было 2), и снизил таймаут до 2 секунд. На графике хорошо видна эта 2-секундная отсечка, когда php-fpm загнулся. Но выводы будут ниже. 2. nginx + php-fpm + lumen Детальный отчет: https://overload.yandex.net/479210 3. nginx-unit + laravel Детальный отчет: https://overload.yandex.net/479184 4. nginx-unit + lumen Детальный отчет: https://overload.yandex.net/479207 В процессе тестирования произошла 1 ошибка с http кодом 500 именно от самого приложения. В связи с отключенными логами, понять что именно случилось невозможно, и больше она не воспроизводилась при повторных прогонах. Однако, учитывая что на 300299 успешных запросов 1 оказался ошибочным, я посчитал что эту ошибку можно проигнорировать. 5. octane (swoole) + laravel Детальный отчет: https://overload.yandex.net/479204 Перцентили времени ответа (ms) * Звездочкой в таблице будут отмечены сборки на базе php-fpm, которые не выдержали нагрузки вообще. У них будет указана отсечка rps, после которой они начали умирать. Как следствие, перцентили даны до этой отсечки. Я решил все же добавить их в таблицу, хотя бы в таком виде. Сборка на базе laravel-octane указана дважды. Полная статистика, когда сервер не выдержал нагрузок, и статистика до точки смерти. Как и с php-fpm - отмечена звездочкой с указанием порогового rps.  99% 98% 95% 90% 85% 80% 75% 50% HTTP OK % * nginx + php-fpm + laravel ~120 rps 62 59 55 52 49 47 45 43 - * nginx + php-fpm + lumen ~400 rps 34 25 19 17 16 16 15 14 - nginx-unit + laravel 6.6 6 5.5 5.2 4.96 4.7 4.5 3.79 100 nginx-unit + lumen 1.77 1.56 1.4 1.25 1.17 1.13 1.08 0.91 100 octane (swoole) + laravel 18 8.3 4.85 3.88 3.45 3.1 2.85 1.87 82.807 * octane (swoole) + laravel ~600 rps 3.23 3.01 2.85 2.55 2.2 2.029 1.92 0.78 - Итоги тестирования меня, надо признать, удивили. Я конечно ожидал, что php-fpm будет медленнее чем, nginx-unit, но не настолько: разница во времени генерации ответа почти в 10 раз. Так же меня удивило то, что php-fpm оказался единственным, который под стресс тестом захлебнулся вообще. Возможно, это связано с тем, что я не являюсь экспертом в конфигурации nginx и php, но остальные сервера, работавшие на том же конфиге, стресс тест выдержали, хоть и с потерями. В итоге, я пришел к выводу, что я не зря давно уже пересел на nginx-unit. Что касается сравнения производительности: если не брать в расчет php-fpm, то nginx-unit единственный, кто выдержал стресс тест без потерь, хотя на octane я возлагал большие надежды. В связи с этим, php-fpm я дальше учитывать в своих рассуждениях не буду. Скачки времени выполнения для 100% перцентиля наблюдаются и у nginx-unit и у octane+swoole, но в обоих случаях не сказать, чтобы они были катастрофическими, а причины их могут быть вовсе в том, что все тестирование произодилось на "слегка" не предназначенном для этого железе и окружении. Опять же, все тесты были сугубо синтетическими, и в реальных условиях, когда приложение будет помимо всего прочего и бизнес логику ворочать, и в базу и кэш ходить, ситуация будет иная. Технически, octane будет быстрее в реальных условиях, за счет сохранения состояния приложения и всех соединений. Кроме того, у swoole есть собственные реализации высокопроизводительного кэша и некоторые другие "примочки", которые позволят ускорить работу тяжелой бизнес логики. Но есть и своя цена: во-первых, все упрется в то, что ему нужно больше оперативной памяти (я понимаю, что цена на оперативную память в дата-центрах не высока для бизнеса, но учитывать это стоит), а во-вторых, что еще важнее, octane крайне недружелюбен к говнокоду - ошибок он не прощает. На официальном сайте есть даже раздел с примерами реализации, работающими на других серверах,  но которые приведут к неработоспособности приложения конкретно на octane. С другой стороны, nginx-unit оказался более стабилен при высоких нагрузках. И при этом в синтетических тестах в связке с lumen почти не проигрывает octane, так что я не зря сомневался в словах Тейлора. Lumen еще поживет и продолжит удивлять (во всяком случае меня, я не ожидал от него такой прыти по сравнению с laravel). В итоге для себя я сделал вывод, что в случае, если требуется предельно стабильный сервис, который будет жить даже там, где другие умирают, то это nginx-unit. А чтобы он жил очень хорошо, с ним все же выгодно использовать lumen, если речь об api. Если же требуется сервис, который должен быть предельно быстрым, и обсчитывать сложную бизнес логику на лету - то это octane, если того позволяют ресурсы (учитывая как оперативную память, так и то, что при ожидании высоких нагрузок, стоит позаботиться о балансировщике и горизонтальном масштабировании заранее). В данном случае octane является тем самым "оружием массового поражения", которое далеко не везде стоит применять, тут Тейлор абсолютно прав. Для каждой задачи - свой инструмент. Надеюсь, статья кому-либо поможет выбрать инструмент для решения своих задач.