рабочее
April 25, 2023

nginx/php-fpm vs roadrunner

Недавно мы столкнулись с тем, что один наш сервер перестал вывозить относительно небольшой RPS. Грубо говоря, сотня +/- одновременных запросов дудосила сервер: nginx создавал отдельный воркер на каждый запрос, быстро исчерпывал пул доступных воркеров и уходил в чилл. Несмотря на то, что каждый отдельный запрос мог сам по себе отработать быстро, проблемой становилось именно исчерпание пула.

(Здесь можно оставить комментарий, что проблема классическая, и об этом стоило позаботиться заранее.)

Проблему быстро пофиксили (большей частью, это было неудачное решение в клиентском приложении), но осадочек остался.

Я занялся исследованием того, можно ли переехать с nginx+php-fpm на roadrunner+php. Цели исследования — выяснить, будет ли буст, в каких ситуациях этот буст будет ощутим, и сможем ли мы перетащить наш стек, если оно того будет стоить.

Методика тестирования: я создал docker-контейнер с сервисами для запуска php-скриптов в окружении nginx/php-fpm и roadrunner/php-cli (вот он). В этом контейнере четыре http-эндпойнта:

  1. Запуск простейшего «hello, world» в nginx/php-fpm.
  2. Запуск простейшего «hello, world» для RoadRunner. RR не может просто запустить тот же код, что php-fpm, он требует своей обвязки и подхода, поэтому скрипты различаются.
  3. Запуск самой простой конфигурации веб-приложения на yii2, выводящей минимальный «hello, yii» по обращению к контроллеру.
  4. Запуск переконфигурированного под RoadRunner yii2, выводящего то же самое.

Каждому скрипту я добавил возможность добавления задержки исполнения (для симуляции реального поведения — ведь ничто не выполняется мгновенно), и протестировал вызовы с задержкой в 0,05 секунды. Код скриптов вы можете посмотреть по ссылке выше.

Итого — восемь вариантов исполнения.

Нагрузочный тест запускается командой

ab -n 100000 -c 1000 #100000 запросов, 1000 единовременно

Для каждого окружения я изменял число воркеров (для nginx — директивой worker_connections, для RR — директивой num_workers) в следующей последовательности: 1, 4, 10, 25, 10, итого — 40 прогонов. После каждого прогона соответствующий сервис перезагружался.

Версия nginx — 1.23.4, RR — 2023.1.0, php — 8.2.0, конфиги php для обоих серверов одинаковы. Кеширование через opcache не использовалось. Лимитов на использования ресурсов для docker не устанавливалось, всё запускалось на Ryzen 9 5900HX (8c/16t).

Результаты

Сводная таблица

Для всех графиков далее используются следующие обозначения: по X — количество воркеров, по Y — RPS. Синее — nginx, зелёное — RoadRunner. Красные крестики — сервер не вывез, не смог отработать хотя бы один запрос.

В случае простейшего «хелловорлда» nginx медленнее в ~6 раз, при этом в случае для одного воркера он не смог обработать 0,5% запросов. Это немного, и, обычно, никто не делает один воркер, но всё-таки.

Теперь добавим задержку:

Оба сервера не вывезли работу с одним воркером, но немного по разному. nginx исчерпав пул коннектов, отбил 99% запросов, RR — 98%, но 100% ответов пришло больше, чем через минуту. В целом, фейл, что тут, что там — но это проблема конфигурации, никто не будет ставить один воркер под нагрузку.

RPS у nginx такой большой именно потому, что он сразу отвечал ошибкой. Если же исключить из графика значения для одного воркера (чтобы получить лучший масштаб), получим такое:

Почему график для nginx не меняется (~95 RPS) с увеличением числа воркеров, я не очень понимаю, но эта линейность сохраняется и далее, так что вряд ли тут ошибка. У RR же RPS растёт пропорционально количеству воркеров, «прибирающих» «долгие» скрипты.

Теперь Yii2.
Yii2 не предназначен для работы с RoadRunner, однако есть способы их «подружить», хотя бы для эксперимента (ни в коем случае — не для продакшена!). Как это удалось сделать мне, вы можете посмотреть всё в том же репозитории.

Сначала без задержки:

nginx с одним воркером снова потерял 99% запросов, а по мере увеличения количества воркеров выдавал ~43 RPS. RoadRunner ушёл за 4000 (4580 в пике).

Тут стоит заметить, что на «хелловорлде» без фреймворка получалось в пике 1775 RPS для nginx и 10500 RPS для RR. Разница огромна для первого и ощутима — для второго. В случае nginx дело явно в бутстраппинге кода, составляющем в этом тесте практически всю нагрузку; каждый раз всё окружение должно инициализироваться заново, и включение opcache preloading помогло бы этот этап исключить. RoadRunner же кеширует код самостоятельно, поэтому получается 10500/4580 ≈ в 2,3 раза код на yii2 медленее при написании хелловорлдов =)

Теперь добавляю задержку:

Абсолютно такой же результат, как и для простого «хелловорлда» без фреймворка. При введении задержки бутстраппинг составляет крайне незначительную долю от всего времени исполнения.

Выводы

Вывод очевидный — RR значительно (в разы) быстрее nginx (держите в уме: это только запуск кода, не исполнение). Скорее всего — за счёт кеширования скриптов, поэтому нужно провести ещё одно сравнение, с включённым opcache.

Другой, не настолько очевидный вывод, кажется мне куда более интересным. RoadRunner гораздо лучше держит нагрузку: там, где nginx задохнётся и даст отбой, RR отработает. Также RR отлично масштабируется — можно наращивать количество воркеров, получая линейный рост производительности (понятно, что до определённой планки).

А что opcache?

В качестве быстрого и грязного эксперимента я добавлю в конфигурацию php кеширование opcache:

[opcache] opcache.enable=1 opcache.enable_cli=1 opcache.preload=/usr/local/preload.php opcache.preload_user=root

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

Прогон осуществлялся при количестве воркеров = 100, без установленной задержки исполнения. Результаты:

Слева — результаты для простого php-скрипта, справа — для скрипта на yii2.

Отсюда видно следующее:

— opcache ускоряет запуск скриптов для php-fpm. Чем больше скриптов бутстрапится — тем, очевидно, сильнее выигрыш (~x2 для «сырого» кода, ~x20 — для фреймворка).

— opcache, скорее, негативно влияет на запуск из RoadRunner. Почему так — судить не берусь, но видно, что в случае с yii2 потери на уровне погрешности, видимо, из-за того, что относительные потери на дополнительное кеширование тут составляют незначительную часть от общего времени исполнения.

— даже с включённым opcache связка nginx/php-fpm проигрывает RoadRunner, причём — значительно.