22 677 Монеток
Привет! Сегодня я хочу поделиться с вами одной интересной идеей — а что если Laravel WebServer принимал бы все HTTP-запросы не напрямую, а через обычную очередь задач Laravel Queue? Вместо привычных PHP-FPM, Swoole, RoadRunner или FrankenPHP — весь запрос обрабатывался бы как задача в очереди.
Звучит странно? Давайте разбираться, как это могло бы работать и что из этого получилось бы.
Первое, что нам нужно — это получать HTTP-запросы «голыми» — простым текстом, как они приходят от браузера:
GET / HTTP/1.1
Host: example.com
Для этого в Laravel мы можем использовать ReactPHP — асинхронную библиотеку, которая умеет слушать TCP-сокеты.
Создадим Artisan-команду app:server
и в ней запустим сервер на порту 8080:
class ServerCommand extends Command
{
protected $signature = 'app:server';
public function handle(): void
{
$loop = Loop::get();
$server = new SocketServer('127.0.0.1:8080', [], $loop);
$server->on('connection', function (ConnectionInterface $connection): void {
$connection->on('data', function (string $data) use ($connection): void {
// Пока просто посмотрим, что пришло
dd($data);
});
});
$this->info('Server started on 127.0.0.1:8080');
$loop->run();
}
}
Запускаем php artisan app:server
, заходим в браузер по адресу http://127.0.0.1:8080/
— и видим, что сервер получает сырые данные запроса. Отлично, базовая часть работает!
Теперь основая идея — каждый пришедший запрос превратить в задачу (Job) и отправить в очередь на обработку:
$connection->on('data', function (string $data) {
HttpHandler::dispatch($data);
});
Но тут возникает проблема — обьект запроса Request::capture()
работает с глобальными переменными ($_GET
, $_POST
и т.д.), а не с сырым HTTP-текстом. Значит нам нужно самим разобрать HTTP-запрос из текста в объект Request
.
Для целей эксперимента я сделал простой парсер, но он нам не интересен, по этому опустим его листинг :)
Помимо этого наш, класс должен обратно обрабатывать обьект Response
в текст. А в самой Job-е мы вызываем Laravel, как будто это обычный HTTP-запрос:
class HttpHandler implements ShouldQueue
{
use Queueable;
public function __construct(protected string $http) {}
public function handle(): void
{
$bridge = new HttpRawRequestParser($this->http);
$illuminateRequest = $bridge->toIlluminateRequest();
$response = app()->handle($illuminateRequest);
$rawResponse = $bridge->fromIlluminateResponse($response);
// Здесь надо отправить rawResponse обратно клиенту — как? Об этом дальше.
}
}
Чтобы отдать результат запроса обратно клиенту, нам нужно как-то связать запрос и ответ. Для этого, почему бы не запускать сразу два ReactPHP-сервера:
class ServerCommand extends Command
{
protected array $clients = [];
public function handle(): void
{
$loop = Loop::get();
$this->startClientServer($loop);
$this->startJobServer($loop);
$this->info('Servers started on 8080 and 8088');
$loop->run();
}
protected function startClientServer(LoopInterface $loop): void
{
$server = new SocketServer('127.0.0.1:8080', [], $loop);
$server->on('connection', function (ConnectionInterface $connection): void {
$connection->on('data', function (string $data) use ($connection): void {
$requestId = Str::uuid()->toString();
$this->clients[$requestId] = $connection;
HttpHandler::dispatch($data, $requestId);
});
});
}
protected function startJobServer(LoopInterface $loop): void
{
$server = new SocketServer('127.0.0.1:8088', [], $loop);
$server->on('connection', function (ConnectionInterface $connection) {
$buffer = '';
$connection->on('data', function (string $data) use (&$buffer, $connection) {
$buffer .= $data;
if (substr($data, -1) !== '}') return;
$payload = json_decode($buffer, true);
if (!$payload) return;
$requestId = $payload['requestId'] ?? null;
$response = $payload['response'] ?? null;
if ($requestId && isset($this->clients[$requestId])) {
$clientConnection = $this->clients[$requestId];
$clientConnection->end($response);
unset($this->clients[$requestId]);
}
});
});
}
}
А внутри джобы HttpHandler
после обработки запроса отправляем JSON с requestId
и ответом на джобсервер:
public function handle(): void
{
// ...
$payload = json_encode([
'requestId' => $this->requestId,
'response' => $rawResponse,
]);
$socket = stream_socket_client("tcp://127.0.0.1:8088", $errno, $errstr, 2);
if (!$socket) {
Log::warning("Socket error: $errstr ($errno)");
return;
}
fwrite($socket, $payload);
fflush($socket);
fclose($socket);
}
Попробовав нагрузить сервер с помощью ab
(ApacheBench), и результаты оказались не самыми быстрыми… Хорошо, а если запусить сразу несколько фоновых процессов, через horizon, он будет автоматически скейлить их количество до настроенных значения (Для примера взял 10).
Переключение на Horizon с автоскейлингом воркеров улучшило ситуацию, но задержка всё ещё ощущается. Это потому, что очередь Laravel — это не realtime-механизм. Фоновый процесс опрашивает Redis с задержкой. Если убрать паузы — будет постоянная нагрузка и расход ресурсов. (Конечно с автоскейллинг позаботиться, но один процесс все равно будет постоянно опрашивать).
➜ ~ ab -c 100 -n 100 http://127.0.0.1:8080/
This is ApacheBench, Version 2.3 <$Revision: 1913912 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 127.0.0.1 (be patient).....done
Server Software:
Server Hostname: 127.0.0.1
Server Port: 8080
Document Path: /
Document Length: 80819 bytes
Concurrency Level: 100
Time taken for tests: 0.518 seconds
Complete requests: 100
Failed requests: 0
Total transferred: 8285000 bytes
HTML transferred: 8081900 bytes
Requests per second: 193.08 [#/sec] (mean)
Time per request: 517.923 [ms] (mean)
Time per request: 5.179 [ms] (mean, across all concurrent requests)
Transfer rate: 15621.67 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 1 0.3 1 2
Processing: 8 288 131.1 289 509
Waiting: 6 288 131.2 289 509
Total: 8 289 130.9 290 510
Percentage of the requests served within a certain time (ms)
50% 290
66% 364
75% 405
80% 427
90% 471
95% 492
98% 506
99% 510
100% 510 (longest request)
Для сравнения, Laravel Octane (Так же с 10 воркерами) показывает:
~ ab -c 100 -n 100 http://127.0.0.1:8000/
This is ApacheBench, Version 2.3 <$Revision: 1913912 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 127.0.0.1 (be patient).....done
Server Software:
Server Hostname: 127.0.0.1
Server Port: 8000
Document Path: /
Document Length: 80819 bytes
Concurrency Level: 100
Time taken for tests: 0.503 seconds
Complete requests: 100
Failed requests: 0
Total transferred: 8189700 bytes
HTML transferred: 8081900 bytes
Requests per second: 198.98 [#/sec] (mean)
Time per request: 502.566 [ms] (mean)
Time per request: 5.026 [ms] (mean, across all concurrent requests)
Transfer rate: 15913.84 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 4 1.0 4 6
Processing: 21 212 69.6 231 328
Waiting: 21 212 69.7 231 327
Total: 27 216 68.9 235 333
Percentage of the requests served within a certain time (ms)
50% 235
66% 258
75% 266
80% 274
90% 288
95% 298
98% 325
99% 333
100% 333 (longest request)
Backend, Frontend, Weekend
Привет, сообщество Laravel!
По вашим многочисленным запросам я выпустил третий релиз TG Support Bot — бота для технической поддержки на Laravel. За последние месяцы проект получил вдвое больше звёзд на GitHub, что мотивирует развивать его дальше.
В этом обновлении — API для подключения внешних источников, новые консольные команды, Swagger-документация и другие улучшения.
TG Support Bot — это решение для организации поддержки клиентов через Telegram и ВКонтакте.
Поддержите проект ⭐ на GitHub: https://github.com/prog-time/tg-support-bot
Присоединяйтесь к Telegram-группе для обсуждения: https://t.me/pt_tg_support
Youtube: https://youtu.be/yNiNtFWOF2w
Rutube: https://rutube.ru/video/bdd0cc5ab4e13530fd7e0c2413931211/
ВК Видео: https://vkvideo.ru/video-141526561_456239132
Реализовано универсальное API для подключения:
Доступные методы API:
Как подключить:
php artisan app:generate-token {название_источника}
Подробнее в Wiki на GitHub
Добавлен генератор Swagger-документации:
php artisan swagger:generate
php artisan telegram:set-webhook — настройка вебхука Telegram через консоль php artisan app:generate-token — генерация API-токена
Обновление функционала зависит только от вас. Предлагайте свои идеи в Telegram группе и голосуйте за них в теме “Голосование”. Если ваша идея наберёт много положительных голосов, то она обязательно будет включена в базовый функционал бота для технической поддержки.
Спасибо за поддержку! Если у вас есть вопросы по интеграции — пишите в Issues или Telegram.
Я здесь для тебя
Сегодня JetBrains объявила, что один из самых популярных плагинов Laravel Idea становится бесплатным для всех пользователей PhpStorm. Ведь он стал уже стал незаменимым помощником для разработчиков и был скачан более 1,5 миллиона раз
В ближайшее время Laravel Idea станет частью стандартной поставки PhpStorm, что сделает поддержку Laravel в IDE ещё лучше.
«Когда я начинал Laravel Idea, я даже мечтать не мог, что он войдёт в PhpStorm! А теперь... это происходит!»
— Адель Файзрахманов, создатель Laravel Idea
Отдельные поздравления Аделю — его труд и преданность сообществу привели к этому знаковому моменту. Особенно ценно, что в последние годы он поддерживал наших пользователей, предоставляя бесплатные лицензии русскоязчному сообществу. Спасибо!
{message}