Комментарии 24
Тесты проводились на "чистой" свеже-установленной версии Laravel. В случае наличия какой-либо логики поведение не сильно, но может отличаться
есть некоторые сомнения насчет "не сильно", как и во всех хеллоуворлд-бенчмарках
PHP-FPM медленнее работает, не стал его втягивать в эту передрягу. Но если интересно, в октябре прошлого года в другой среде тестировал и не на чистом проект. Результат был таким:

Октан был на RoadRunner.
Интересно как раз насколько медленнее, спасибо. cpu/memory usage не сравнивали?
странные показатели, на живом проекте fpm давал на однотипные запросы (мускуль, редис, ничего сложного) порядка 200мс на запрос, тогда как rr/franken те же запросы выполняли за 50мс
Странные показатели. Пустой эндпоинт на octane+swoole отрабатывает за 2мс. C RR не работаю, один раз тестировал - цифра была похожая.
И да, пустой эндпоинт может работать на скорости 1к рпс.
Возможно у вас добавляется систематическая погрешность из-за того что клиент и сервер в разных странах.
Стоит ещё проверить параметры хостинга, а точнее точно ли вам выделено 100% CPU.
Чтобы сравнение было честным, стоит наверное явно задать количество потоков сервера.
А чтобы оно было не абстрактными попугаями, а близким к реальности - включать opcache для cli.
Ну и в идеале, конечно, результаты тестирования лучше смотреть в динамике. Итоговая сводка в 80мс в теории может складываться как среднее из 10мс и 900мс. Тут как минимум нужно указывать min, max, avg, а лучше графики.
И нет. При наличии логики поведение будет отличаться очень сильно. Типичная для php сайтов io-bound нагрузка легко может увести разницу между обсуждаемыми технологиями в разряд погрешности, ато и вовсе "сравнять" их по применимости с php-fpm.
Уже год использую RR. Встал вопрос о переходе с FPM, и сначала я выбрал FrankenPHP. Октан выполняет большую часть работы по адаптации PHP-сервера к приложению, но не обошлось без проблем. Все основные проблемы возникали с статичными классами, значения которых передавались от сессии к сессии. Например, таймзона клиента хранилась в статичных свойствах, и если один клиент менял таймзону в рамках одного воркера, она изменялась и для другого клиента. Я откатил обратно на FPM и начал более подробно изучать тему.
После детального изучения, что именно нужно доработать, я решил остановиться на RR. Основные причины:
Он всё ещё невероятно быстрый по сравнению с FPM.
RR — это не только PHP-сервер, но и, например, сервер очередей, кешей и других полезных вещей.
Интеграция с Temporal можно отнести ко второму пункту, но я бы выделил её отдельно. Для тех, кто работает с глобальными workflow или с Temporal в частности, RR из коробки поддерживает поднятие Temporal-сервера на PHP.
Ну и не стоит забывать, что RR делают те же ребята, которые разрабатывают Spiral и другие проекты. Активно поддерживающие PHP-сообщество ну и говорят по-русски. Это даёт им несколько дополнительных очков форы )
При использовании Swoole, вы прогревали endpoint? Swoole при старте загружает приложение в себя и "висит" некоторые время, что негативно сказывается на бенчмарках. Для сферического теста в вакууме этим можно пренебречь, в реальности я бы этот фактор учел.
У вас на графиках отражена загрузка приложения тем, что перфомансы в начальный момент времени хуже у Swoole, а со временем улучшаются
P.S одна из причина по которой лучше использовать OpenSwoole
Прогревом октан занимается при первом запросе к нему. Я проверял с точки зрения простейшего использования по принципу "установил зависимость - запустил фрейм - протестировал". Никаких улучшений и тонких настроек. Только дефолт. Только хардкор.
Вы устроили драг-рейсинг заезд, в котором учитывается время на "подбежать до машины, пристегнуться, завести мотор и включить передачу"
С хардкорами и дефолтами нет ничего общего.
В бенчах RR-Swoole-Franken обычно все допускают одни и те же ошибки в настройках RR:
- не выключают логи и тестирую пропускную способность stdout.
- не устанавливают protobuf и теряют очень много на (де)сериализации протобафов. При тестировании пустых эндпоинтов это существенно.
- не тестируют разные важные опции типа кол-ва воркеров (относится ко всем серверам)
Несмотря на то, что Swoole в HTTP будет всегда объективно быстрее других (если только RR или Франкен не перепишут с go на что-то без интеропа), всё-таки хотелось бы видеть какую-то объективность в цифрах.
Короче, рекомендую читать документацию к серверам, чтобы устанавливать и настраивать их правильно, а не полагаться на дефолты Octane.
Интересно бы посмотреть тест с прогретыми соединениями к БД, и какой-нить мессадж брокер. **От ФПМ отрыв будет однозначно больше.
Автору спасибо за статью :pray:
Один из моих проектов через FPM отдавал данные примерно за 300-350 мс. С Октаном на RoadRunner возвращает примерно за 90-100мс.
Выглядит так, будто бы включен opcache.revalidate_path
, или же вообще отсутствует opcache.
Натравил вот ab -n1000 -c16
на свой прод, полноценный проц, в котором и БД, и кэши, и рендеринг, а не хэлловорлд-стаб. В связке nginx+nginx+php-fpm (8.3), и вот что-то как-то не верится в то, что php-fpm может давать вот так вот сходу +200ms латенси.
Connection Times (ms)
min mean[+/-sd] median max
Connect: 5 9 3.7 8 34
Processing: 40 63 8.5 63 119
Waiting: 30 40 5.6 41 64
Total: 46 72 10.7 71 152
А у прода какая конфигурация? Само железо?
Вдобавок, мои цифры указаны с расчётом клиентского запроса, то есть с компьютера через DNS и провайдеров в другую страну к серверу. Это честный запрос, т.к. мной рассматривается скорость отклика от сайта, а не производительность сервера.
Касательно производительности самого сервера, то при дефолтных настройках ab
тест на RoadRunner показывал около 145 rps, а FrankenPHP около 250 rps. PHP-Fpm при этом не замерял.
И, опять же, насколько замечаю тенденцию в комментариях, многие путают производительность сервера с отзывчивостью сайта.
Всё в виртуалках, поверх Silver 4216 (8CPU+8RAM), без лимитирования. Ко всему прочему, скрипты лежат на NFS-сторадже, а не в каком-нибудь локальном NVMe.
Вдобавок, мои цифры указаны с расчётом клиентского запроса
Так вы меряете скорость своего роутера, своего впна, коммутаторов провайдера, днс серверов и ещё черт знает каких железок. С такой экспертизой можно скатиться до бенчмарков в браузерах.
И выше верно замечено было, что надо тестировать не хэлловорлд, а хоть какую-то реальную логику, а всё потому что, если при 10ms и при 100ms на запрос картина может оказаться совершенно не такой, как могло бы показаться.
Ну и при бенчмаркингах принято увеличивать конкурентность, а не время теста. Время теста вообще ни о чем не говорит. Особенно если оно живёт при c=10, но сдыхает при c=20...
какие-то дохлые цифры даже для 2 ядер. Я сам использую часто OpenSwoole и RoadRunner как с Symfony, так и с Laravel. Пустой контроллер типа health-check на Symfony под OpenSwoole и RoadRunner выдает 8k RPS на 8 ядрах (ограничение в Docker, физический процессор - Intel 13900). Без фреймворка чистый RoadRunner показывает примерно те же цифры.
Спасибо за статью, сможете ли вы выложить код проектов в репозиторий? Хочется увидеть все настройки в том числе .rr файл.
Кода там нет никакого кроме роута, который я выложил. Конфиг роадраннера тоже дефолтный и составляется самим Laravel Octane в момент его запуска. Руками в файл нельзя лазить иначе его можно запросто сломать и он не будет запускаться.
Да и урл создавал чисто для проверки, чтобы не на главную ломиться где сессии работают.
По сути, всё что Вам нужно сделать для запуска нового проекта под roadrunner, это выполнить эти команды:
laravel new test && cd test
composer require laravel/octane spiral/roadrunner-cli spiral/roadrunner-http
php vendor/bin/rr get-binary
И, либо в супервизор конфигурацию воткнуть, либо руками локально запустить:
php artisan octane:start
Содержимое файла .rr.yaml
Laravel Octane формирует самостоятельно, но при запущенном инстансе он выглядит так (копипаст с прода):
Показать содержимое .rr.yaml
version: '3'
rpc:
listen: 'tcp://127.0.0.1:6001'
server:
command: 'php app.php'
relay: pipes
http:
address: '0.0.0.0:8080'
middleware:
- gzip
- static
static:
dir: public
forbid:
- .php
- .htaccess
pool:
num_workers: 1
supervisor:
max_worker_memory: 100
jobs:
pool:
num_workers: 2
max_worker_memory: 100
consume: { }
kv:
local:
driver: memory
config:
interval: 60
metrics:
address: '127.0.0.1:2112'
Также при старте создаётся ещё один файл - storage/logs/octane-server-state.json
со следующим содержимым:
Показать содержимое octane-server-state.json
{
"masterProcessId": 808,
"state": {
"appName": "Test App",
"host": "127.0.0.1",
"port": "9000",
"rpcPort": "6001",
"workers": 0,
"maxRequests": "500",
"octaneConfig": {
"server": "roadrunner",
"https": false,
"listeners": {
"Laravel\\Octane\\Events\\WorkerStarting": [
"Laravel\\Octane\\Listeners\\EnsureUploadedFilesAreValid",
"Laravel\\Octane\\Listeners\\EnsureUploadedFilesCanBeMoved"
],
"Laravel\\Octane\\Events\\RequestReceived": [
"Laravel\\Octane\\Listeners\\CreateConfigurationSandbox",
"Laravel\\Octane\\Listeners\\CreateUrlGeneratorSandbox",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToAuthorizationGate",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToBroadcastManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToDatabaseManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToDatabaseSessionHandler",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToFilesystemManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToHttpKernel",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToLogManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToMailManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToNotificationChannelManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToPipelineHub",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToCacheManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToSessionManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToQueueManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToRouter",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToValidationFactory",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToViewFactory",
"Laravel\\Octane\\Listeners\\FlushDatabaseRecordModificationState",
"Laravel\\Octane\\Listeners\\FlushDatabaseQueryLog",
"Laravel\\Octane\\Listeners\\RefreshQueryDurationHandling",
"Laravel\\Octane\\Listeners\\FlushLogContext",
"Laravel\\Octane\\Listeners\\FlushArrayCache",
"Laravel\\Octane\\Listeners\\FlushMonologState",
"Laravel\\Octane\\Listeners\\FlushStrCache",
"Laravel\\Octane\\Listeners\\FlushTranslatorCache",
"Laravel\\Octane\\Listeners\\PrepareInertiaForNextOperation",
"Laravel\\Octane\\Listeners\\PrepareLivewireForNextOperation",
"Laravel\\Octane\\Listeners\\PrepareScoutForNextOperation",
"Laravel\\Octane\\Listeners\\PrepareSocialiteForNextOperation",
"Laravel\\Octane\\Listeners\\FlushLocaleState",
"Laravel\\Octane\\Listeners\\FlushQueuedCookies",
"Laravel\\Octane\\Listeners\\FlushSessionState",
"Laravel\\Octane\\Listeners\\FlushAuthenticationState",
"Laravel\\Octane\\Listeners\\EnforceRequestScheme",
"Laravel\\Octane\\Listeners\\EnsureRequestServerPortMatchesScheme",
"Laravel\\Octane\\Listeners\\GiveNewRequestInstanceToApplication",
"Laravel\\Octane\\Listeners\\GiveNewRequestInstanceToPaginator"
],
"Laravel\\Octane\\Events\\RequestHandled": [],
"Laravel\\Octane\\Events\\RequestTerminated": [],
"Laravel\\Octane\\Events\\TaskReceived": [
"Laravel\\Octane\\Listeners\\CreateConfigurationSandbox",
"Laravel\\Octane\\Listeners\\CreateUrlGeneratorSandbox",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToAuthorizationGate",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToBroadcastManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToDatabaseManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToDatabaseSessionHandler",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToFilesystemManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToHttpKernel",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToLogManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToMailManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToNotificationChannelManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToPipelineHub",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToCacheManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToSessionManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToQueueManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToRouter",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToValidationFactory",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToViewFactory",
"Laravel\\Octane\\Listeners\\FlushDatabaseRecordModificationState",
"Laravel\\Octane\\Listeners\\FlushDatabaseQueryLog",
"Laravel\\Octane\\Listeners\\RefreshQueryDurationHandling",
"Laravel\\Octane\\Listeners\\FlushLogContext",
"Laravel\\Octane\\Listeners\\FlushArrayCache",
"Laravel\\Octane\\Listeners\\FlushMonologState",
"Laravel\\Octane\\Listeners\\FlushStrCache",
"Laravel\\Octane\\Listeners\\FlushTranslatorCache",
"Laravel\\Octane\\Listeners\\PrepareInertiaForNextOperation",
"Laravel\\Octane\\Listeners\\PrepareLivewireForNextOperation",
"Laravel\\Octane\\Listeners\\PrepareScoutForNextOperation",
"Laravel\\Octane\\Listeners\\PrepareSocialiteForNextOperation"
],
"Laravel\\Octane\\Events\\TaskTerminated": [],
"Laravel\\Octane\\Events\\TickReceived": [
"Laravel\\Octane\\Listeners\\CreateConfigurationSandbox",
"Laravel\\Octane\\Listeners\\CreateUrlGeneratorSandbox",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToAuthorizationGate",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToBroadcastManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToDatabaseManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToDatabaseSessionHandler",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToFilesystemManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToHttpKernel",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToLogManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToMailManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToNotificationChannelManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToPipelineHub",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToCacheManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToSessionManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToQueueManager",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToRouter",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToValidationFactory",
"Laravel\\Octane\\Listeners\\GiveNewApplicationInstanceToViewFactory",
"Laravel\\Octane\\Listeners\\FlushDatabaseRecordModificationState",
"Laravel\\Octane\\Listeners\\FlushDatabaseQueryLog",
"Laravel\\Octane\\Listeners\\RefreshQueryDurationHandling",
"Laravel\\Octane\\Listeners\\FlushLogContext",
"Laravel\\Octane\\Listeners\\FlushArrayCache",
"Laravel\\Octane\\Listeners\\FlushMonologState",
"Laravel\\Octane\\Listeners\\FlushStrCache",
"Laravel\\Octane\\Listeners\\FlushTranslatorCache",
"Laravel\\Octane\\Listeners\\PrepareInertiaForNextOperation",
"Laravel\\Octane\\Listeners\\PrepareLivewireForNextOperation",
"Laravel\\Octane\\Listeners\\PrepareScoutForNextOperation",
"Laravel\\Octane\\Listeners\\PrepareSocialiteForNextOperation"
],
"Laravel\\Octane\\Events\\TickTerminated": [],
"Laravel\\Octane\\Contracts\\OperationTerminated": [
"Laravel\\Octane\\Listeners\\FlushOnce",
"Laravel\\Octane\\Listeners\\FlushTemporaryContainerInstances"
],
"Laravel\\Octane\\Events\\WorkerErrorOccurred": [
"Laravel\\Octane\\Listeners\\ReportException",
"Laravel\\Octane\\Listeners\\StopWorkerIfNecessary"
],
"Laravel\\Octane\\Events\\WorkerStopping": [
"Laravel\\Octane\\Listeners\\CloseMonologHandlers"
]
},
"warm": [
"auth",
"cache",
"cache.store",
"config",
"cookie",
"db",
"db.factory",
"db.transactions",
"encrypter",
"files",
"hash",
"log",
"router",
"routes",
"session",
"session.store",
"translator",
"url",
"view",
"App\\Http\\Webhooks\\MainHandler",
"App\\Queries\\AdministratorQuery",
"App\\Queries\\ChatQuery",
"App\\Queries\\CommandQuery",
"App\\Queries\\MemberQuery",
"App\\Queries\\MessageQuery",
"App\\Queries\\ModuleQuery",
"App\\Queries\\RoleQuery",
"App\\Services\\AdministratorService",
"App\\Services\\ChatService",
"App\\Services\\MemberService",
"App\\Services\\MessageService",
"App\\Services\\ModuleService",
],
"flush": [],
"tables": {
"example:1000": {
"name": "string:1000",
"votes": "int"
}
},
"cache": {
"rows": 1000,
"bytes": 10000
},
"watch": [
"app",
"bootstrap",
"config\/**\/*.php",
"database\/**\/*.php",
"public\/**\/*.php",
"resources\/**\/*.php",
"routes",
"composer.lock",
".env"
],
"garbage": 50,
"max_execution_time": 30
}
}
}
При этом, в команде запуска октана можно указать лишь уровень логов параметром --log-level
:
'-o', 'logs.mode=production',
'-o', 'logs.level='.($this->option('log-level') ?: (app()->environment('local') ? 'debug' : 'warn')),
'-o', 'logs.output=stdout',
'-o', 'logs.encoding=json',
RoadRunner vs OpenSwoole vs FrankenPHP с Laravel Octane