НейроАгент

Как работает RoadRunner PHP воркер: принципы и лучшие практики

Подробное объяснение работы RoadRunner PHP воркеров, оптимизации маршрутизации и управления соединениями с базой данных для максимальной производительности.

Как точно работает скрипт на RoadRunner?

У меня есть следующий скрипт, работающий на RoadRunner:

php
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()) выполняется для каждого запроса, а код снаружи этого цикла - только один раз при инициализации?

  2. Искусственный интеллект рекомендует не выполнять $router->map() внутри цикла waitRequest. Это корректный совет? (Скрипт унаследован от предыдущего разработчика, я разбираюсь с его логикой)

  3. Как лучше организовать работу с базой данных в таком скрипте? Следует ли открывать соединение в глобальной части, до цикла while ($req = $worker->waitRequest()), а внутри цикла проверять актуальность соединения следующим образом:

php
$connOk = false;
try {
    /* @var \mysqli $sql*/
    $connOk = @mysqli_ping($sql);
} catch (\Throwable $e) {
    // обработка исключения
}
if (!$connOk) {
    if (!empty($sql) && $sql instanceof \mysqli) {
        @mysqli_close($sql);
    }
    $sql = mysqli_connect(/**/);
}

Пожалуйста, объясните принципы работы RoadRunner и лучшие практики для данного случая.

НейроАгент

RoadRunner PHP worker scripts operate on a worker lifecycle model where initialization occurs once per worker process, and the worker handles multiple requests over its lifetime. In your script, the code outside the while ($req = $worker->waitRequest()) loop executes only once when the worker starts, while everything inside the loop processes each incoming request individually.

Содержание


Основы работы RoadRunner воркеров

RoadRunner реализует модель воркеров на основе Go-грутин, где каждый PHP воркер работает как отдельный процесс, обменивающийся сообщениями с основным процессом RoadRunner. Ключевые особенности:

  • Инициализация: Каждый PHP воркер инициализируется один раз при старте
  • Постоянная работа: Воркер остается активным между запросами, устраняя время загрузки
  • Обработка запросов: Код внутри цикла while обрабатывает каждый запрос последовательно
  • Автоматическая перезагрузка: RoadRunner автоматически перезапускает воркеры при ошибках или превышении лимитов памяти

Как указано в документации RoadRunner, “RoadRunner keeps PHP workers alive between incoming requests. This means that you can completely eliminate bootload time (such as framework initialization) and significantly speed up a heavy application.”


Анализ вашего скрипта

Ответ на вопрос 1: Разделение кода по времени выполнения

Ваше понимание абсолютно верное:

php
// Выполняется ОДИН РАЗ при инициализации воркера
$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()) {
    // весь код здесь выполняется при каждом запросе
}

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


Оптимизация маршрутизации

Ответ на вопрос 2: Router mapping внутри цикла

Совет ИИ корректен. Размещение $router->map() внутри цикла while является плохой практикой по нескольким причинам:

  1. Неэффективность: Маршруты можно определить один раз при инициализации
  2. Потенциальные утечки памяти: Каждый вызов map() может добавлять новые обработчики
  3. Логические ошибки: Дублирование маршрутов может привести к непредсказуемому поведению

Правильная реализация:

php
// Выполняется один раз при инициализации
$router = new League\Route\Router;
$router->map('POST', '/some1[/]', function (ServerRequestInterface $req, array $args) {
    // обработчик маршрута
});
$router->map('POST', '/some2[/]', function (ServerRequestInterface $req, array $args) {
    // обработчик маршрута
});

// Цикл обработки запросов
while ($req = $worker->waitRequest()) {
    try {
        $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) {
        // ваша обработка
    }
}

Как объясняется в руководстве по RoadRunner PHP Workers, RoadRunner ожидает, что PHP скрипты будут работать непрерывно, обрабатывая множество запросов через while loop.


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

Ответ на вопрос 3: Оптимальная организация работы с БД

Ваш подход с проверкой соединения в цикле разумный, но можно его оптимизировать. Рекомендуемый подход:

php
// Глобальная область - выполняется один раз
$sql = null;

function initDatabase() {
    global $sql;
    $sql = mysqli_connect(
        'localhost', 
        'user', 
        'password', 
        'database',
        3306,
        null,
        MYSQLI_CLIENT_FOUND_ROWS | MYSQLI_CLIENT_MULTI_STATEMENTS
    );
    
    if (!$sql) {
        throw new \RuntimeException("Database connection failed: " . mysqli_connect_error());
    }
    
    // Установка таймаутов и других параметров
    mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
}

// Инициализация при старте воркера
initDatabase();

// В цикле обработки запросов
while ($req = $worker->waitRequest()) {
    try {
        // Проверка соединения перед использованием
        if (!$sql || !mysqli_ping($sql)) {
            initDatabase(); // Переподключение при необходимости
        }
        
        // Использование соединения
        $result = mysqli_query($sql, "SELECT * FROM users LIMIT 1");
        
    } catch (\Throwable $e) {
        // Обработка ошибок
        if ($e instanceof mysqli_sql_exception) {
            // Специфическая обработка ошибок MySQL
            error_log("Database error: " . $e->getMessage());
        }
        // ... остальная обработка
    }
}

Ключевые рекомендации:

  1. Единое соединение: Используйте одно соединение на весь воркер
  2. Ленивая проверка: Проверяйте соединение только перед использованием
  3. Автоматическое переподключение: Реализуйте механизм восстановления соединения
  4. Обработка таймаутов: Установите разумные таймауты для операций с БД

Как отмечают разработчики RoadRunner, “Database connections and any pipe/socket is the potential point of failure. Close all the connections after each iteration. Consider calling gc_collect_cycles after each execution if you want to keep memory low.”


Рекомендации по производительности

Оптимизация вашего скрипта

php
<?php
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();
$psr7Worker = new RoadRunner\Http\PSR7Worker($worker, $psrFactory, $psrFactory, $psrFactory);

// Маршруты определяются один раз
$router = new League\Route\Router;
$router->map('POST', '/some1[/]', function (ServerRequestInterface $req, array $args) {
    return new JsonResponse(['status' => 'success for some1']);
});
$router->map('POST', '/some2[/]', function (ServerRequestInterface $req, array $args) {
    return new JsonResponse(['status' => 'success for some2']);
});

// Инициализация базы данных
$sql = null;
function getDbConnection() {
    global $sql;
    if (!$sql || !mysqli_ping($sql)) {
        $sql = mysqli_connect('localhost', 'user', 'password', 'database');
        if (!$sql) {
            throw new \RuntimeException("Database connection failed");
        }
    }
    return $sql;
}

// === ОБРАБОТКА ЗАПРОСОВ (для каждого запроса) ===
while ($req = $psr7Worker->waitRequest()) {
    try {
        // Проверка и получение соединения с БД при необходимости
        $db = getDbConnection();
        
        // Обработка запроса
        $response = $router->dispatch($req);
        $psr7Worker->respond($response);
        
    } catch (League\Route\Http\Exception\MethodNotAllowedException $e) {
        $psr7Worker->respond(new HtmlResponse('<b>Error</b>: ' . $e->getMessage(), 405));
    } catch (League\Route\Http\Exception\NotFoundException $e) {
        $psr7Worker->respond(new HtmlResponse('<b>Error</b>: ' . $e->getMessage(), 404));
    } catch (\Throwable $e) {
        // Логирование ошибки
        error_log("Worker error: " . $e->getMessage());
        
        // Возврат ошибки клиенту
        $psr7Worker->respond(new JsonResponse([
            'error' => 'Internal server error',
            'message' => $e->getMessage()
        ], 500));
    }
}

Дополнительные рекомендации

  1. Использование пула соединений: Для высокой нагрузки рассмотрите использование пула соединений
  2. Оптимизация памяти: Периодически вызывайте gc_collect_cycles() при работе с большими объемами данных
  3. Мониторинг: Реализуйте логирование для отслеживания состояния воркера
  4. Конфигурация: Настройте параметры RoadRunner для вашей нагрузки:
    yaml
    # .rr.yaml
    server:
      command: "php worker.php"
      env:
        - APP_ENV=production
    workers:
      pool:
        num_workers: 4
        max_jobs: 1000
        exec_ttl: 30s
        supervisor:
          watch_tick: 1s
          ttl: 30s
    

Заключение

  1. Архитектура RoadRunner: Весь код вне цикла while выполняется один раз при инициализации воркера, а внутри цикла - для каждого запроса
  2. Маршрутизация: Определение маршрутов должно происходить вне цикла для производительности и предсказуемости
  3. База данных: Используйте одно соединение на воркер с механизмом автоматического восстановления
  4. Обработка ошибок: Реализуйте комплексную обработку исключений с логированием для диагностики проблем

Корректная организация работы RoadRunner воркеров позволяет достичь значительного прироста производительности по сравнению с традиционным FPM подходом, особенно для приложений с большим временем инициализации фреймворков.

Источники

  1. RoadRunner PHP Workers Documentation
  2. Scaling PHP Applications with RoadRunner
  3. RoadWorker GitHub Documentation
  4. RoadRunner Features Overview
  5. PHP RoadRunner Community Discussion