Платформа PHP часто подвергается критике за отсутствие встроенных возможностей для создания конкурентных приложений. В версии 8.1 был добавлен класс Fiber, который, согласно RFC, должен упростить создание конкурентных приложений. Однако, материалов, демонстрирующих использование данного функционала для построения приложений практически нет, напротив, говорится, что файберы - это функционал, предназначенный для использования разработчиками фреймворков и приводятся какие-то малоинформативные отрывки кода. В этой статье будет продемонстрирован концептуальный пример конкурентного приложения на PHP с использованием файберов. Приложение представляет собой HTTP-сервер, осуществляющий регистрацию и подсчёт анонимных пользователей с выдачей каждому из них JWT-токена (нужно было добавить какую-то функцию с вводом-выводом, а не просто "Hello-World!"). В качестве хранилища используется сервер Redis. Прототип характеризуется следующими основными свойствами с точки зрения многозадачности:
один HTTP-запрос - один новый файбер, после обработки запроса файбер уничтожается;
файберы передают управление друг другу самостоятельно, программист может этим управлять, это кооперативная многозадачность;
ввод-вывод неблокирующий;
системный поток выполнения только один, системные инструменты обеспечения конкурентности (процессы, потоки) внутри приложения не используются, за счёт отсутствия переключений контекста между большим количеством процессов или потоков должна быть обеспечена высокая производительность;
для эффективной работы на многоядерном или многопроцессорном окружении стоит запустить несколько инстансов данного приложения за балансировщиком;
фреймворки ReactPHP/Swoole/AMPHP/Workerman а также libevent и аналоги не используются намеренно, в первой части статьи поставлена задача обойтись без данных инструментов;
Разбор кода прототипа
Полный код прототипа можно получить из репозитория, в нём предусмотрено окружение на базе Docker Compose.
Основа, на которой построен данный прототип - сокеты и файберы. Сокеты обеспечивают возможность неблокирующего ввода-вывода, файберы обеспечивают конкурентность. Фактически в прототипе реализована кооперативная (не вытесняющая) многозадачность. Потоки PHP было решено не использовать по причине несовершенства их API, которое не позволяет получить код состояния операции ввода-вывода. К тому же, API сокетов очень схож с системными сетевыми функциями Linux, интерфейс которых проверен временем и есть отличные man-страницы.
Входная точка в программу - файл bin/server.php, Она выполняет подключение автолоадера, загрузку переменных окружения из файла .env,
запуск сервера. Главный цикл сервера, управляющий запуском файберов находится в файле src/App.php:
private function __construct(FiberedHandlerFactory $factory)
{
$this->handlerCollection = new FiberedHandlerCollection();
$this->factory = $factory;
pcntl_async_signals(true);
pcntl_signal(SIGINT, [$this, 'interrupt']);
$serverSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($serverSocket === false) {
exit('Error creating server socket.');
}
if (!socket_set_nonblock($serverSocket)) {
exit('Error switching socket to the nonblocking mode.');
}
if (!socket_bind($serverSocket, getenv('HTTP_BIND_HOST'), (int)getenv('HTTP_BIND_PORT'))) {
exit('Error binding socket.');
}
if (!socket_listen($serverSocket, SOMAXCONN)) {
exit('Error switching socket to the listening mode.');
}
$this->handlerCollection->push($this->factory->createServerHandler($serverSocket, $this->handlerCollection));
}
public function interrupt(): void
{
$this->interrupted = true;
}
В качестве аргумента конструктор принимает фабрику обработчиков src/FiberedHandlerFactory.php, которые построены на базе файберов. Их работа будет описана далее. Также в первых строках создаётся коллекция обработчиков src/FiberedHandlerCollection.php , фактически представляющая собой очередь из обработчиков для выполнения. Вызовы функций pcntl_async_signals()
и pcntl_signal()
необходимы для установки обработчика системного сигнала SIGINT
, чтобы операционная система имела возможность корректно остановить данный сервер. Далее создаётся основной сокет сервера, который сразу переводится в неблокирующий режим и передаётся отдельному обработчику входящий соединений, работающему на базе файбера.
Далее предлагаю взглянуть на код главного цикла сервера в файле src/App.php:
public function run(): void
{
App::debug("HTTP server has been started. Waiting for connections...");
while (true) {
$readSockets = $this->handlerCollection->getReadSockets();
$writeSockets = $this->handlerCollection->getWriteSockets();
$exceptSockets = $this->handlerCollection->getExceptSockets();
//блокируемся на всех известных сокетах, чтобы процессор не крутил цикл
if(false === socket_select(
$readSockets,
$writeSockets,
$exceptSockets,
null
)) {
exit(sprintf('Fatal error: socket_select() call failed: %s', socket_strerror(socket_last_error())));
}
if ($this->interrupted) {
//получили сигнал завершения работы
break;
}
$fiberedHandlers = $this->handlerCollection->getHandlers($readSockets, $writeSockets, $exceptSockets);
App::debug("Count of fibers: %d", $this->handlerCollection->getCount());
foreach ($fiberedHandlers as $fiberedHandler) {
$fiberedHandler->resetSockets();
if ($fiberedHandler->isSuspended()) {
$fiberedHandler->resume();
}
if (!$fiberedHandler->isStarted()) {
$fiberedHandler->start();
}
if ($fiberedHandler->isTerminated()) {
$this->handlerCollection->remove($fiberedHandler);
}
}
}
}
Центральное место в цикле занимает вызов socket_select()
, который позволяет понять, какие из сокетов в приложении готовы к вводу-выводу. В первых строках цикла можно видеть сбор массивов всех сокетов, обработки которых нужно подождать. После вызова socket_select()
в массивах останутся только те сокеты, которые готовы к вводу-выводу. Основной сокет сервера также передаётся в вызов socket_select()
, если он останется в массиве $readSockets
после вызова, это означает, что появилось новое подключение к серверу. Обработчик новых подключений выделен в самостоятельный файберный обработчик, который работает с обработчиками генерации ответов на HTTP-запросы в конкурентном режиме. Это сделано намеренно, чтобы сервер равномерно принимал новые подключения и выдавал ответы на уже принятые. Важный код обработчика новых соединений src/NewConnectionsHandler.php:
private function run(): void
{
while (true) {
//ограничение из-за FD_SETSIZE для типа fd_set в системном вызове select()
if ($this->handlerCollection->getCount() > self::MAX_FIBERS) {
Fiber::suspend();
continue;
}
$acceptedSocket = socket_accept($this->serverSocket);
if ($acceptedSocket === false) {
Fiber::suspend();
continue;
}
if (!socket_set_nonblock($acceptedSocket)) {
App::error("Error switching socket to the nonblocking mode.");
socket_close($acceptedSocket);
continue;
}
$this->handlerCollection->push($this->factory->createRequestHandler($acceptedSocket));
Fiber::suspend();
}
}
Здесь есть свой бесконечный цикл, управление из которого периодически передаётся основному циклу сервера с помощью вызова Fiber::suspend()
. Сокет, появившийся после вызова socket_accept()
(получение нового подключения к серверу), также сразу переводится в неблокирующий режим. Для обслуживания принятого сокета с помощью упомянутой фабрики обработчиков FiberedHandlerFactory
создаётся обработчик принятого соединения, в обязанности которого входит получение и разбор данных HTTP-запроса, вызов шаблонного метода генерации ответа, а также отправка ответа по принятому соединению.
Вернёмся к основному циклу в файле src/App.php. После socket_select()
приложение вызывает методFiberedRequestHandlerCollection::getHandlersBySockets()
, передав в качестве аргумента массив готовых к работе сокетов. Коллекция на основе соответствия сокетов и обработчиков, а также собственных правил выдаёт в качестве ответа на данный вызов массив обработчиков, которые затем будут выполнены:
$fiberedHandlers = $this->handlerCollection->getHandlers($readSockets, $writeSockets, $exceptSockets);
App::debug("Count of fibers: %d", $this->handlerCollection->getCount());
foreach ($fiberedHandlers as $fiberedHandler) {
$fiberedHandler->resetSockets();
if ($fiberedHandler->isSuspended()) {
$fiberedHandler->resume();
}
if (!$fiberedHandler->isStarted()) {
$fiberedHandler->start();
}
if ($fiberedHandler->isTerminated()) {
$this->handlerCollection->remove($fiberedHandler);
}
}
Методы FiberedRequestHandler::isSuspended()
, FiberedRequestHandler::resume()
, FiberedRequestHandler::isStarted()
, FiberedRequestHandler::start()
, FiberedRequestHandler::isTerminated()
, как можно догадаться, являются прокси над одноимёнными методами класса Fiber
. Логика, реализованная в цикле, довольно проста: если обработка файбера была приостановлена - продолжаем, если ещё не начиналась - начинаем, если она завершилась - нужно удалить данный файберный обработчик из коллекции.
Базовый файберный обработчик HTTP-запросов
Теперь пора поговорить о классе файберного обработчика запросов src/HttpRequestHandler.php. Его функция заключается в получении из сокета и разборе HTTP-запроса, вызове шаблонного метода по генерации ответа на запрос, сериализации и записи ответа на запрос в сокет. Всё это делается внутри функции, которая передаётся в конструктор файбера:
public function __construct(int $id, Socket $acceptedSocket)
{
$this->id = $id;
$this->acceptedSocket = $acceptedSocket;
$this->fiber = new Fiber([$this, 'run']);
$this->addReadSocket($acceptedSocket);
}
public function __destruct()
{
$this->closeResources();
}
Метод run()
, передаваемый в конструктор файбера в качестве единственного аргумента - коллбэка:
private function run(): void
{
try {
$requestHeaders = $this->readRequestHeaders();
$stream = $this->readRequestStream($requestHeaders);
$this->handleHttpRequestInternal($stream);
} catch (HttpRequestException $e) {
$code = $e->getCode();
if (!in_array($code, [400, 500])) {
$code = 500;
}
$this->sendHttpResponse(new TextResponse($e->getMessage(), $code));
} finally {
$this->closeResources();
}
}
Отдельно стоит отметить чтение HTTP-запроса и его запись. Чтение из сокета производится неблокирующим способом, сначала читаются и проверяются HTTP-заголовки, затем тело. Когда нужно подождать готовности сокета, управление с помощью вызова Fiber::suspend()
отдаётся обратно основному циклу сервера. который может во время ожидания ввода-вывода вызвать другой файберный обработчик из сформированного массива на выполнение или просмотреть список сокетов на предмет к готовности к работе. В этом и заключается основной принцип на котором построена конкурентность в данном приложении - отдать управление, когда оно не нужно и делать что-то полезное, пока ядро системы занимается вводом-выводом и готовит сокет. Если ни один из сокетов не готов (в том числе принимающий новые соединения), то приложение заблокируется на вызове socket_select()
и ничего не будет делать. Данный подход очень похож на реализованный в ReactPHP/Swoole/AMPHP паттерн Реактор, но без применения событийных расширений для PHP. Возможно, в следующих статьях стоит реализовать данную незамысловатую функциональность с помощью перечисленных фреймворков и выполнить сравнение их производительности с данным решением. Код чтения HTTP-заголовков:
private function readRequestHeaders(): string
{
$requestHeaders = '';
$start = time();
$byteCounter = 0;
while (true) {
$headerLine = socket_read($this->acceptedSocket, 1);
if ($headerLine === false) {
$errorCode = socket_last_error($this->acceptedSocket);
socket_clear_error($this->acceptedSocket);
if (in_array($errorCode, [SOCKET_EAGAIN, SOCKET_EWOULDBLOCK])) {
$this->addReadSocket($this->acceptedSocket);
Fiber::suspend();
if ($this->isTimeoutReached($start)) {
App::debugFiber($this, 'Connection timed out while reading request headers. Socket error code: %d. Data: %s', $errorCode, json_encode($requestHeaders));
throw new HttpRequestException('Connection timed out while reading request headers.');
}
continue;
}
if (!in_array($errorCode, [0, SOCKET_EINPROGRESS])) {
throw new HttpRequestException(sprintf("Error reading data: %s", socket_strerror($errorCode)));
}
}
$byteCounter++;
if ($byteCounter > self::MAX_HTTP_HEADERS_SIZE) {
throw new HttpRequestException('HTTP Request headers are too large.');
}
$requestHeaders .= $headerLine;
if (str_ends_with($requestHeaders, "\r\n\r\n")
|| str_ends_with($requestHeaders, "\r\r")
|| str_ends_with($requestHeaders, "\n\n")
|| strlen($headerLine) === 0) {
return $requestHeaders;
}
}
}
Для облегчения логики разбора запросов используется реализация стандарта PSR-7 и PSR-17 из пакета Laminas/Diactoros
, высококачественный код которого отказался очень удобным для решения данной задачи. Реализация поддерживает только те функции протокола HTTP, которые необходимы для построение совсем простого API (GET/POST методы, application/json
в теле ограниченного размера).
Метод FiberedRequestHandler::handleHttpRequestInternal()
выполняет вызов шаблонного метода (паттерн Template Method) FiberedRequestHandler::handleHttpRequest()
и отправку ответа в сокет, а также обработку ошибок. Приведём здесь только код отправки ответа в сокет:
private function sendHttpResponse(ResponseInterface $response): void
{
$responseString = ResponseSerializer::toString($response);
$written = 0;
$start = time();
while ($written < strlen($responseString)) {
$buffer = substr($responseString, $written);
$result = socket_write($this->acceptedSocket, $buffer);
if ($result === false) {
$errorCode = socket_last_error($this->acceptedSocket);
socket_clear_error($this->acceptedSocket);
if (in_array($errorCode, [SOCKET_EAGAIN, SOCKET_EWOULDBLOCK])) {
$this->addWriteSocket($this->acceptedSocket);
Fiber::suspend();
if ($this->isTimeoutReached($start)) {
App::errorFiber($this,'Connection timed out while sending HTTP response. Socket error code: %d. Bytes written: %d.', $errorCode, $written);
return;
}
continue;
}
if (!in_array($errorCode, [0, SOCKET_EINPROGRESS])) {
App::errorFiber($this, "Socket error: %s", socket_strerror($errorCode));
break;
}
} else {
$written += $result;
}
}
App::debugFiber($this, "Response has been sent.");
}
Код неблокирующей записи в сокет напоминает код чтения. Когда нужно подождать, управление с помощью файбера отдаётся основному циклу программы для выполнения какой-нибудь другой полезной работы вместо ожидания завершения ввода/вывода.
Шаблонный метод генерации ответа на HTTP-запрос имеет простую реализацию и предназначен для переопределения в дочерних классах:
/** @throws Throwable */
protected function handleHttpRequest(RequestInterface $request): ResponseInterface
{
return new TextResponse("Hello from fibered php http server!\n");
}
Обработчик запроса на регистрацию анонимного пользователя и возврат JWT
Класс src/JWTRequestHandler.php, наследующий базовый обработчик HttpRequestHandler
, реализует логику генерации UUID для анонимного пользователя, сохраняет его вместе с датой регистрации в Redis и генерирует JWT-токен, который возвращается в виде ответа на HTTP-запрос. Помимо этого обработчик ведёт подсчёт количества пользователей. Интерес здесь представляет код ввода-вывода данных в Redis. Как можно догадаться, блокирующий драйвер Predis
здесь не подойдёт, но, к счастью, Redis имеет простой текстовый протокол реализованный в библиотеке clue/redis-protocol, который отлично работает через неблокирующий TCP-сокет:
private function connectToRedisUsingSocket(): void
{
$redisSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($redisSocket === false) {
return;
}
if (!socket_set_nonblock($redisSocket)) {
return;
}
$start = time();
while (true) {
$connected = socket_connect($redisSocket, getenv('REDIS_HOST'), (int)getenv('REDIS_PORT'));
if (!$connected) {
$errorCode = socket_last_error($redisSocket);
socket_clear_error($redisSocket);
if (in_array($errorCode, [SOCKET_EAGAIN, SOCKET_EWOULDBLOCK, SOCKET_EALREADY])) {
$this->addWriteSocket($redisSocket);
Fiber::suspend(); //передача управления, чтобы программа занималась полезной работой, а не ждала подключения
if ($this->isTimeoutReached($start)) {
App::debugFiber($this, 'Connection timed out while connecting to Redis. Socket error code: %d.', $errorCode);
throw new RuntimeException('Connection timed out while connecting to Redis.');
}
continue;
}
if (!in_array($errorCode, [0, SOCKET_EINPROGRESS])) {
throw new RuntimeException(sprintf("Socket error: %s", socket_strerror($errorCode)));
}
}
break;
}
$this->redisSocket = $redisSocket;
}
Как и в случае с обработкой HTTP-соединений, используется неблокирующий ввод-вывод. Если возникает операция ввода-вывода, результата которой нужно подождать, эта операция должна быть выполнена в неблокирующем режиме в цикле, с передачей управления основному циклу если ввод-вывод ещё не завершился. Инкремент счётчика пользователей и сохранение данных пользователя в Redis реализованы именно таким способом:
$requestMessage = $this->serializer->getRequestMessage('INCR', ['user_counter']);
$written = 0;
$start = time();
while ($written < strlen($requestMessage)) {
$buffer = substr($requestMessage, $written);
App::debugFiber($this,'Trying to write %s to the socket.', json_encode($buffer));
$result = socket_write($redisSocket, $buffer);
if ($result === false) {
$errorCode = socket_last_error($redisSocket);
socket_clear_error($redisSocket);
if (in_array($errorCode, [SOCKET_EAGAIN, SOCKET_EWOULDBLOCK])) {
$this->addWriteSocket($redisSocket);
Fiber::suspend();
if ($this->isTimeoutReached($start)) {
App::debugFiber($this, 'Connection timed out while incrementing counter. Socket error code: %d. Bytes written: %d.', $errorCode, $written);
throw new RuntimeException('Connection timed out while incrementing counter.');
}
continue;
}
if (!in_array($errorCode, [0, SOCKET_EINPROGRESS])) {
throw new RuntimeException(sprintf("Socket error: %s", socket_strerror($errorCode)));
}
} else {
$written += $result;
}
}
Стоит отметить, что очень немногие драйверы хранилищ поддерживают такие простые текстовые протоколы взаимодействия. В возможных следующих статьях стоит постараться реализовать взаимодействие с другими хранилищами (может для этого и потребуется как-то доработать их драйверы). Тем не менее, сокеты PHP это уже немало, ведь они фактически представляют собой системные вызовы Linux для работы с сетью, которые используются в качестве транспорта в большинстве драйверов хранилищ.
Сравнение с реализацией на базе PHP-FPM
В качестве кандидата на сравнение решено было использовать реализацию на базе PHP-FPM, которая, учитывая встроенные в PHP-возможности по разбору HTTP-запросов и отправке ответов, а также серверный цикл PHP-FPM, получилась очень простой, в виде одного файла webroot/index.php:
require __DIR__ . '/../vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
$dotenv->load();
header('Content-Type', 'application/json');
$predis = new Client([
'scheme' => 'tcp',
'host' => getenv('REDIS_HOST'),
'port' => getenv('REDIS_PORT'),
]);
$counter = $predis->incr('user_counter');
if ($counter === false) {
header('HTTP/1.1 500 Internal Server Error');
echo 'Error while incrementing user counter.';
return;
}
$user = new User(
Uuid::uuid4(),
new DateTimeImmutable(),
$counter
);
if (!$predis->set($user->getId()->getHex()->toString(), json_encode($user, JSON_THROW_ON_ERROR))) {
header('HTTP/1.1 500 Internal Server Error');
echo 'Error while saving user to Redis.';
return;
}
$jwtKey = InMemory::plainText(trim(getenv('JWT_KEY')));
$tokenBuilder = new Builder(
new JoseEncoder(),
ChainedFormatter::default()
);
$now = $user->getDate();
$jwt = $tokenBuilder->issuedAt($now)
->expiresAt($now->modify('+1 hour'))
->withHeader('user_id', $user->getId())
->withHeader('counter', $user->getCounter())
->getToken(new Sha256(), $jwtKey);
echo json_encode([
'jwt' => $jwt->toString()
]);
В конфигурацию Docker Compose были добавлены контейнеры Nginx и PHP-FPM, после запуска данный обработчик будет принимать соединения в порт 8086
. Файберная реализация доступна в порту 8085
. Для выполнения нагрузочного тестирования использовалась простая консольная утилита Apache Benchmark. Далее приводятся отрывки из результатов запуска нагрузочных тестов. Для каждой реализации было выполнено по 10000 запросов в 1000 потоков. Для оценки производительности используется общее время обработки (чем меньше - тем лучше) и количество обрабатываемых запросов в секунду (чем больше - тем лучше). Бенчмарки выполнялись в окружении, развёрнутом при помощи Docker Compose на ноутбуке автора статьи. Для более объективного сравнения количество ядер процессора, используемых контейнерами файберной реализации и PHP-FPM было ограничено 1 ядром.
Результаты бенчмарка реализации на базе Файберов
$ ab -c 1000 -n 10000 http://localhost:8085/
Server Software:
Server Hostname: localhost
Server Port: 8085
Document Path: /
Document Length: 244 bytes
Concurrency Level: 1000
Time taken for tests: 54.569 seconds
Complete requests: 10000
Failed requests: 1020
(Connect: 0, Receive: 0, Length: 1020, Exceptions: 0)
Total transferred: 2946584 bytes
HTML transferred: 2436584 bytes
Requests per second: 183.25 [#/sec] (mean)
Time per request: 5456.885 [ms] (mean)
Time per request: 5.457 [ms] (mean, across all concurrent requests)
Transfer rate: 52.73 [Kbytes/sec] received
Общее время ~ 55 сек., ~ 183 запроса в секунду.
Результаты бенчмарка реализации на базе PHP-FPM
$ ab -c 1000 -n 10000 http://localhost:8086/
Server Software: nginx/1.27.2
Server Hostname: localhost
Server Port: 8086
Document Path: /
Document Length: 244 bytes
Concurrency Level: 1000
Time taken for tests: 94.832 seconds
Complete requests: 10000
Failed requests: 1011
(Connect: 0, Receive: 0, Length: 1011, Exceptions: 0)
Total transferred: 3666659 bytes
HTML transferred: 2436659 bytes
Requests per second: 105.45 [#/sec] (mean)
Time per request: 9483.242 [ms] (mean)
Time per request: 9.483 [ms] (mean, across all concurrent requests)
Transfer rate: 37.76 [Kbytes/sec] received
Общее время ~ 94 сек., ~ 106 запросов в секунду.
Если вы заметили ошибку в реализации или процессе оценки, которая могла привести к значительным огрехам в результирующих данных - прошу написать об этом в комментариях к статье или сообщить автору с помощью личных сообщений.
Итоги
Как можно видеть из результатов бенчмарков, производительность примера, использующего файберы, оказалась выше реализации на базе PHP-FPM. Безусловно, последняя показала бы себя более эффективно, будучи запущенной на нескольких ядрах, однако реализацию на базе файберов также можно запустить в виде нескольких экземпляров (лучше всего в количестве, равном числу доступных ядер) за балансировщиком, что обеспечит ещё большую производительность на многоядерной или многопроцессорной архитектуре. Также непроанализированным осталось потребление ресурсов. Возможно, его анализ будет приведен в следующих статьях.
Представленный в статье подход пока трудно рекомендовать к использованию в production. Сокеты и pctnl
включены во многих сборках PHP, однако, константа FD_SETSIZE
ограничивает количество одновременно обслуживаемых одним вызовом socket_select()
сокетов. При нагрузочном тестировании в три процесса, каждый из которых создавал по 1000 соединений одновременно, не было обнаружено каких-либо проблем, т.к. сокеты быстро обслуживаются и закрываются, ведь это HTTP-сервер, где не нужны постоянные соединения. Кроме того, применение ограничивается малым количеством драйверов хранилищ, поддерживающих неблокирующий ввод-вывод (другие неблокирующие драйверы будут продемонстрированы в следующих статьях). Также стоит отметить низкую популярность неблокирующего ввода-вывода и кооперативной многозадачности среди PHP-разработчиков, по субъективным ощущениям, все ждут какого-то аналога потоков (Thread) и пула потоков (ThreadPool) из Python или Runnable из Java Concurrency. Стоит отметить, что файберы работают в одном потоке, а такие приложения гораздо легче отлаживать, чем многопоточные, отсутствует состояние гонки. Остаётся надеяться, что появление файберов и всё возрастающие требования к времени отклика и устойчивости серверных приложений к нагрузкам в совокупности с требованиями к снижению затрат на непомерно дорожающую инфраструктуру будут способствовать повышению популярности более эффективного конкурентного программирования в PHP-сообществе. В следующей статье будет продемонстрировано развитие данного примера, в котором вызов socket_select()
будет заменён на более масштабируемое решение, которое позволит обслуживать гораздо большее количество соединений.
Ссылка на репозиторий
Материалы, использованные при подготовке данной статьи
Комментарии (9)
vadimushka_d
13.12.2024 08:43Огромное развитие в сторону файберов, вопрос такой можно ли какой-то приземленный вариант? например в виде чата? который будет работать на таком сервере?
cleancodemonkey Автор
13.12.2024 08:43Да, файберы и на сервере вебсокетов(который лучше всего для чата) смогут пригодиться, пример тоже можно сделать, пока простой HTTP-сервер
roxblnfk
13.12.2024 08:43Можно, кстати, попробовать заменить
$headerLine = socket_read($this->acceptedSocket, 1);
на использованиеsocket_recv()
и забенчить снова.
Сдаётся мне, на этом можно здорово сэкономить
roxblnfk
13.12.2024 08:43Можно поизучать репозиторий https://github.com/buggregator/trap/
Реализованы в какой-то мере HTTP, WebSocket, SMTP.
Я бы, конечно, не рекомендовал на PHP писать серверы прям на замену RoadRunner, FPM и т.д., как минимум потому что:
всегда что-то будет работать в блокирующем режиме (работа с файловой системой, STDIN/OUT, PDO или вендорный пакет)
пыховский zval всегда будет тормознее примитивов в C/Go/Rust...
Но погружаться в эту тему всё-равно как-то надо. Для лёгкого старта можно попробовать phasync, который поможет юзать файберы в ограниченном контексте на готовом проекте. Останется только найти, что параллелить :) говорят под доктрину есть async драйвера
FanatPHP
13.12.2024 08:43Ну если PDO и блокирует, то mysqli как раз может работать асинхронно. Только пул надо создать, на несколько соединений чтобы использовать их паралельно :)
roxblnfk
13.12.2024 08:43Под многие вещи у amphp есть асинхронные драйвера. Можно сразу туда идти и набирать. Или сразу на AMP писать :)
Однако вендорные штуки или ORM переписывать всё-равно не будешь¯\_(ツ)_/¯
. А там и PDO, и чего только нет.Ещё одна большая проблема — протекание контекста. Например один общий Unit of Work или Entity Manager. В некоторых случаях даже разные инстансы контейнера на отдельный запрос не помогут.
cleancodemonkey Автор
13.12.2024 08:43STDOUT/ERR не должны особо мешать, т.к. если через fprintf туда писать, то должен будет использоваться буферизованный ввод/вывод, а это считай без блокирования.
Пакетов с блокирующими вызовами, да, конечно, больше, чем без. Но это практически для любой платформы (кроме Node.js и подобных) актуально.
C RoadRunner и FPM беда в том, что там для обработки каждого отдельного запроса нужен свой отдельный процесс, который будет блокироваться на блокирующих вызовах, а потому если много запросов - будет много процессов, а к этому операционки не готовы, вот если бы вместо процессов были треды...
square
По ощущениям появление файберов немного запоздало, из-за чего немалая часть php-разработчиков уже предпочитает подобные вещи написать на golang)
Но за статью огромное вам человеческое спасибо, давно хотелось посмотреть более "боевой" пример использования.
cleancodemonkey Автор
Golang это отлично, но желательно и в нашем любимом и ламповом PHP тоже такое уметь делать и понимать как оно работает.