Анализ и оптимизация PHP-воркера для RoadRunner

    Рассмотрим скрипт, работающий в среде RoadRunner, и разберем ключевые вопросы по его архитектуре.

    Исходный код

    Исходный скрипт имеет следующую структуру:

    use Nyholm\Psr7;
    use Psr\Http\Message\ServerRequestInterface;
    use Spiral\RoadRunner;
    use Zend\Diactoros\Response\HtmlResponse;
    use Zend\Diactoros\Response\JsonResponse;
    
    $worker = RoadRunner\Worker::create();
    $psrFactory = new Psr7\Factory\Psr17Factory();
    
    $worker = new RoadRunner\Http\PSR7Worker($worker, $psrFactory, $psrFactory, $psrFactory);
    $router = new League\Route\Router;
    
    while ($req = $worker->waitRequest()) {
        $response = "";
        try {
            $post = $req->getParsedBody();
            $path = $req->getUri()->getPath();
            $router->map('POST', '/some1[/]', function (ServerRequestInterface $req, array $args) {
                //...
            });
            $router->map('POST', '/some2[/]', function (ServerRequestInterface $req, array $args) {/**/});
    
            $response = $router->dispatch($req);
            $worker->respond($response);
        } catch (League\Route\Http\Exception\MethodNotAllowedException $e) {
            $worker->respond(new HtmlResponse('<b>Error</b>: ' . $e->getMessage(), 500));
        } catch (League\Route\Http\Exception\NotFoundException $e) {
            $worker->respond(new HtmlResponse('<b>Error</b>: ' . $e->getMessage(), 404));
        } catch (\Throwable $e) {
            // моя обработка
        }
    }

    Ответы на вопросы

    1. Области выполнения кода

    Верно: код внутри цикла while ($req = $worker->waitRequest()) выполняется для каждого входящего HTTP-запроса. Код, расположенный до этого цикла, инициализируется один раз при запуске воркера и сохраняется в памяти между запросами.

    Это важная особенность работы с RoadRunner и аналогичными решениями (Swoole, ReactPHP), которые позволяют избежать накладных расходов на повторную инициализацию фреймворков, подключение к БД и других ресурсоемких операций.

    2. Оптимизация роутинга

    Да, это верная рекомендация. Определение маршрутов ($router->map()) следует вынести за пределы цикла обработки запросов. В текущей реализации маршруты переопределяются при каждом запросе, что:

    • Создает ненужную нагрузку на процессор
    • Занимает дополнительную память
    • Не дает никаких преимуществ, так как маршруты обычно статичны

    Оптимальный подход: инициализировать роутер один раз, до входа в основной цикл. Это стандартная практика для PSR-совместимых приложений.

    3. Работа с базой данных

    Предложенный подход с проверкой соединения через mysqli_ping() является разумным, но требует доработки:

    • Инициализация соединения: действительно лучше создавать подключение к БД в глобальной области (до цикла), чтобы использовать одно соединение для всех запросов в рамках жизни воркера.
    • Проверка актуальности: механизм проверки соединения необходим, так как долгоживущие соединения могут разрываться из-за таймаутов сервера БД или сетевых проблем.
    • Рекомендации по реализации:
      • Используйте более современный PDO вместо прямого mysqli для лучшей переносимости и безопасности
      • Рассмотрите возможность использования connection pool, если поддерживается вашим драйвером
      • Добавьте логирование случаев переподключения для мониторинга
      • Убедитесь, что обработка исключений не скрывает реальные проблемы с подключением

    Пример улучшенной структуры:

    // Инициализация ДО цикла
    $db = new PDO($dsn, $user, $password, [
        PDO::ATTR_PERSISTENT => true,
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    ]);
    
    while ($req = $worker->waitRequest()) {
        try {
            // Проверка и восстановление соединения при необходимости
            if (!$db || $db->getAttribute(PDO::ATTR_CONNECTION_STATUS) === false) {
                $db = new PDO($dsn, $user, $password, [
                    PDO::ATTR_PERSISTENT => true,
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
                ]);
            }
            // Обработка запроса с использованием $db
        } catch (\Throwable $e) {
            // Обработка ошибок
        }
    }

    Такая архитектура обеспечивает баланс между производительностью (постоянное соединение) и надежностью (автоматическое восстановление при обрывах).