Анализ и оптимизация 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) {
// Обработка ошибок
}
}Такая архитектура обеспечивает баланс между производительностью (постоянное соединение) и надежностью (автоматическое восстановление при обрывах).