Архитектура долгоживущих 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 get

Php 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

Сервер приложений PHP - comments

En
Php application server (php)