Архитектура долгоживущих PHP процессов выбор сервера приложений
Сервер приложений PHP от FastCGI к долгоживущим процессам
Как организовать высокопроизводительный сервер приложений на PHP с бесшовной интеграцией?
Основное решение RoadRunner. Это сервер приложений на Go управляющий PHP воркерами через потоки ввода вывода. Он даёт долгоживущие процессы балансировку HTTP/2 gRPC очереди и другое.
Цели микросервисная архитектура API с высокой пропускной способностью фоновые задачи обработка вебхуков.
Пример установки и запуска:
composer require spiral/roadrunner:v2.0
composer require spiral/roadrunner-http
vendor/bin/rr getPhp application server (сервер приложений php)
Конфигурация .rr.yaml:
http:
address: 0.0.0.0:8080
workers:
pool:
num_workers: 4
max_jobs: 100
supervisor:
max_worker_memory: 128
Код PHP воркера (worker.php):
<?php
use Spiral\RoadRunner\Worker;
use Spiral\RoadRunner\Http\PSR7Worker;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Response;
require __DIR__ . '/vendor/autoload.php';
$worker = new Worker();
$psr7 = new PSR7Worker($worker, new Psr17Factory(), new Psr17Factory(), new Psr17Factory());
while ($req = $psr7->waitRequest()) {
$resp = new Response(200, [], 'Hello from RoadRunner');
$psr7->respond($resp);
}
Запуск ./rr serve -c .rr.yaml
Типичные ошибки
- Несовпадение версий RoadRunner и PHP пакетов. Решение обновление через Composer с точными версиями.
- Утечки памяти в воркере. Решение настройка супервизора на перезапуск при превышении порога (max_worker_memory).
- Проблемы с загрузкой классов при первом запросе. Решение прогрев приложения перед запуском.
Как получить встроенный сервер приложений на основе Caddy с PHP без отдельного бинарного файла?
Вариант FrankenPHP. Это сервер объединяющий Caddy Go и PHP в одном бинарнике. Он поддерживает Worker режим автоматическое обновление конфигурации встроенную поддержку HTTP/2 и HTTPS.
Цели простой деплой быстрое прототипирование когда не хочется настраивать отдельный сервер и PHP FPM.
# Установка (Linux)
curl -L https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-$(uname -m) -o frankenphp
chmod +x frankenphp
./frankenphp run
Создать файл index.php:
<?php echo 'Hello FrankenPHP';
Конфигурация через Caddyfile (файл Caddyfile):
localhost:8080 {
root * /app/public
php_fastcgi /app/public/worker.php
file_server
}
Для работы в режиме Worker указывается директива php_fastcgi с путём к PHP скрипту поддерживающему FastCGI. FrankenPHP обрабатывает запросы сам.
Проблемы
- Ограниченная поддержка сторонних расширений PHP по сравнению с FPM. Решение сборка кастомного бинарника с нужными расширениями.
- Молодой проект возможны изменения в API. Решение следить за changelog.
Как использовать асинхронный PHP на уровне расширения для создания собственного сервера приложений?
Swoole это C расширение для PHP добавляющее асинхронность корутины собственный HTTP сервер и другое. Оно позволяет создавать долгоживущие процессы без внешних инструментов.
Цели real time приложения (чаты игры) высокая конкуренция при обработке большого числа соединений.
# Установка расширения
pecl install swoole
# или через Docker
docker run -v $(pwd):/app -w /app --rm phpswoole/swoole php server.php
Пример простого HTTP сервера:
<?php
use Swoole\Http\Server;
$server = new Server("0.0.0.0", 8080);
$server->on("request", function ($request, $response) {
$response->end("Hello from Swoole");
});
$server->start();
Типичные ошибки
- Необходимость сборки расширения под конкретную версию PHP. Решение использование официального Docker образа.
- Совместимость с библиотеками использующими блокирующие вызовы (например PDO без корутинного драйвера). Решение использование swoole_table и swoole_database_pool.
Как построить событийно ориентированное приложение на чистом PHP без установки расширений?
ReactPHP библиотека реализующая событийный цикл на PHP. Позволяет создавать свои серверы приложений без внешних зависимостей на уровне ядра но с меньшей производительностью.
Цели учебные проекты прототипы невысокая нагрузка.
composer require react/http:^1.6
<?php
require __DIR__ . '/vendor/autoload.php';
$loop = React\EventLoop\Loop::get();
$server = new React\Http\Server($loop, function (Psr\Http\Message\ServerRequestInterface $request) {
return new React\Http\Message\Response(
200,
['Content-Type' => 'text/plain'],
"Hello ReactPHP"
);
});
$socket = new React\Socket\SocketServer('127.0.0.1:8080');
$server->listen($socket);
echo "Server running at http://127.0.0.1:8080\n";
$loop->run();
Проблемы
- Производительность ниже чем у RoadRunner и Swoole. Решение использовать только для задач не требующих высокой пропускной способности.
- Поддержка только одного процесса. Решение запускать несколько экземпляров за обратным прокси (Nginx).
Расширенные примеры использования серверов приложений PHP
RoadRunner обработка gRPC запросов
Для работы с gRPC требуется определить протокол в файле .proto. Пример proto greeter.proto:
syntax = "proto3";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }
Генерация кода PHP:
protoc --php_out=. greeter.proto
Регистрация сервиса в RoadRunner через конфигурацию (.rr.yaml):
grpc:
listen: tcp://127.0.0.1:9001
workers:
pool:
num_workers: 2
proto:
- "greeter.proto"
PHP реализация воркера с использованием RoadRunner gRPC:
<?php
use Spiral\RoadRunner\GRPC\Server;
use Spiral\RoadRunner\Worker;
use Greeter\GreeterInterface;
use Greeter\HelloRequest;
use Greeter\HelloReply;
require __DIR__ . '/vendor/autoload.php';
$worker = new Worker();
$server = new Server($worker);
$server->registerService(GreeterInterface::class, new class implements GreeterInterface {
public function SayHello($ctx, HelloRequest $request): HelloReply {
$reply = new HelloReply();
$reply->setMessage('Hello ' . $request->getName());
return $reply;
}
});
$server->serve();
Запуск с помощью ./rr serve -c .rr.yaml. Клиентский запрос через gRPCurl:
grpcurl -d '{"name": "World"}' -plaintext 127.0.0.1:9001 Greeter/SayHello
{
"message": "Hello World"
}
FrankenPHP интеграция с Mercure для real time уведомлений
Mercure встроен в FrankenPHP. Конфигурация Caddyfile:
localhost:8080 {
php_fastcgi /app/public/worker.php
route /mercure/* {
mercure {
publisher_jwt_key "secret"
subscriber_jwt_key "secret"
allow_anonymous
}
}
}
PHP код отправки события (publisher.php):
<?php
$hub = 'http://localhost:8080/.well-known/mercure';
$token = 'eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0'; // подписано секретом
foreach (['new_post', 'update'] as $topic) {
$data = json_encode(['message' => 'Hello từ FrankenPHP']);
$ch = curl_init($hub);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $token,
'Content-Type: application/x-www-form-urlencoded',
],
CURLOPT_POSTFIELDS => http_build_query(['topic' => $topic, 'data' => $data]),
]);
curl_exec($ch);
}
Подписка через JavaScript в браузере:
<script>
const url = new EventSource('http://localhost:8080/.well-known/mercure?topic=new_post');
url.onmessage = event => console.log(event.data);
</script>
Результат – в консоли появится сообщение при запуске publisher.php.
Swoole создание WebSocket сервера с корутинами
Реализация чата с использованием корутин:
<?php
use Swoole\WebSocket\Server;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
$server = new Server("0.0.0.0", 9501);
$server->on("open", function (Server $server, Request $request) {
echo "Connection open: {$request->fd}\n";
});
$server->on("message", function (Server $server, Frame $frame) {
foreach ($server->connections as $fd) {
if ($server->isEstablished($fd)) {
$server->push($fd, $frame->data);
}
}
});
$server->on("close", function (Server $server, int $fd) {
echo "Connection closed: {$fd}\n";
});
$server->start();
Запуск: php server.php. Клиент через wscat:
wscat -c ws://127.0.0.1:9501 Connected (press CTRL+C to quit) > Hello < Hello (от сервера обратно всем)
ReactPHP асинхронная работа с базой данных MySQL
Установка пакета:
composer require react/mysql:^0.4
Пример запроса:
<?php
require __DIR__ . '/vendor/autoload.php';
$loop = React\EventLoop\Loop::get();
$factory = new React\MySQL\Factory($loop);
$connection = $factory->createLazyConnection('user:pass@127.0.0.1/testdb');
$connection->query('SELECT * FROM users LIMIT 2')->then(
function (React\MySQL\QueryResult $result) {
foreach ($result->resultRows as $row) {
echo "User: {$row['name']}\n";
}
},
function (Exception $e) {
echo 'Error: ' . $e->getMessage() . "\n";
}
);
$loop->run();
Результат выполнения (при наличии таблицы):
User: Alice User: Bob