Как точно работает скрипт на 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) {
//моя обработка
}
}
У меня есть несколько вопросов по работе этого скрипта:
-
Какая часть кода выполняется один раз при старте воркера, а какая часть - при каждом запросе от пользователей? Правильно ли я понимаю, что всё внутри цикла
while ($req = $worker->waitRequest())выполняется для каждого запроса, а код снаружи этого цикла - только один раз при инициализации? -
Искусственный интеллект рекомендует не выполнять
$router->map()внутри циклаwaitRequest. Это корректный совет? (Скрипт унаследован от предыдущего разработчика, я разбираюсь с его логикой) -
Как лучше организовать работу с базой данных в таком скрипте? Следует ли открывать соединение в глобальной части, до цикла
while ($req = $worker->waitRequest()), а внутри цикла проверять актуальность соединения следующим образом:
$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 воркеров
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: Разделение кода по времени выполнения
Ваше понимание абсолютно верное:
// Выполняется ОДИН РАЗ при инициализации воркера
$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 является плохой практикой по нескольким причинам:
- Неэффективность: Маршруты можно определить один раз при инициализации
- Потенциальные утечки памяти: Каждый вызов
map()может добавлять новые обработчики - Логические ошибки: Дублирование маршрутов может привести к непредсказуемому поведению
Правильная реализация:
// Выполняется один раз при инициализации
$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: Оптимальная организация работы с БД
Ваш подход с проверкой соединения в цикле разумный, но можно его оптимизировать. Рекомендуемый подход:
// Глобальная область - выполняется один раз
$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());
}
// ... остальная обработка
}
}
Ключевые рекомендации:
- Единое соединение: Используйте одно соединение на весь воркер
- Ленивая проверка: Проверяйте соединение только перед использованием
- Автоматическое переподключение: Реализуйте механизм восстановления соединения
- Обработка таймаутов: Установите разумные таймауты для операций с БД
Как отмечают разработчики 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
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));
}
}
Дополнительные рекомендации
- Использование пула соединений: Для высокой нагрузки рассмотрите использование пула соединений
- Оптимизация памяти: Периодически вызывайте
gc_collect_cycles()при работе с большими объемами данных - Мониторинг: Реализуйте логирование для отслеживания состояния воркера
- Конфигурация: Настройте параметры 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
Заключение
- Архитектура RoadRunner: Весь код вне цикла
whileвыполняется один раз при инициализации воркера, а внутри цикла - для каждого запроса - Маршрутизация: Определение маршрутов должно происходить вне цикла для производительности и предсказуемости
- База данных: Используйте одно соединение на воркер с механизмом автоматического восстановления
- Обработка ошибок: Реализуйте комплексную обработку исключений с логированием для диагностики проблем
Корректная организация работы RoadRunner воркеров позволяет достичь значительного прироста производительности по сравнению с традиционным FPM подходом, особенно для приложений с большим временем инициализации фреймворков.