はじめに
こんにちは、DevOps グループの中島です。
最近急に寒くなり、年の瀬を感じる時節になってきましたね。そういえば去年の年末は Herman Miller でセールをしていたので、駆け込みでイスを買ったのを思い出しました。
今回は性能が高いと評判の PHP アプリケーションサーバー RoadRunner を Slim Framework (Slim 4) で使うための設定手順と、簡単な性能の検証結果を共有したいと思います。
RoadRunner とは
RoadRunner は Go で書かれたオープンソースの高性能な PHP アプリケーションサーバ & プロセスマネージャで、プラグイン機構をサポートしています。
RoadRunner is an open-source (MIT licensed) high-performance PHP application server, process manager written in Go and powered with plugins ❤️. It supports running as a service with the ability to extend its functionality on a per-project basis with plugins.
nginx + php-fpm ではその構成上 HTTP サーバとアプリケーションサーバが分離されているため、リクエストを受け付けるたびに PHP 側でフレームワークの初期化を行う必要がありましたが、
RoadRunner では付属の HTTP サーバでリクエストを受け付けたあと、すでに初期化が走ったあとでデーモン的に動作している PHP のワーカーがリクエストを処理する構成となっているため、パフォーマンスの向上に繋がっています。
細かい特徴は ドキュメント や Zenn の記事 に譲ります。
セットアップ
まずは必要なツール類をインストールします。
OS は Ubuntu 24.04 LTS を利用しました。
PHP インストール
$ sudo apt update
$ sudo apt install php php-xml php-mbstring unzip
Composer インストール
$ curl -s https://getcomposer.org/installer | php
$ sudo mv composer.phar /usr/local/bin/composer
RoadRunner インストール
$ wget https://github.com/roadrunner-server/roadrunner/releases/download/v2024.3.0/roadrunner-2024.3.0-linux-amd64.deb
$ sudo dpkg -i roadrunner-2024.3.0-linux-amd64.deb
$ rm roadrunner-2024.3.0-linux-amd64.deb
アプリケーション作成
次に、Slim のサンプルプロジェクトを作成します。
サンプルプロジェクト作成
$ composer create-project slim/slim-skeleton sample-app
ここまでで、サーバを起動してアクセスするとサンプルが動作することが確認できます。
$ php -S 0.0.0.0:8080 -t public
$ curl localhost:8080
Hello world!
RoadRunner 用設定
RoadRunner の組み込みに必要な設定をしていきます。
RoadRunner と関連ライブラリを依存関係に追加
$ cd sample-app
$ composer require spiral/roadrunner spiral/roadrunner-worker spiral/roadrunner-http nyholm/psr7
RoadRunner の設定ファイル作成
$ vi .rr.yaml
version: "3"
server:
command: "php psr-worker.php"
http:
address: 0.0.0.0:8080
logs:
level: error
ワーカーの作成
ここから、PHP のワーカーである psr-worker.php を作成していきます。
RoadRunnerのガイド による実装は以下の通りです。
<?php
require __DIR__ . '/vendor/autoload.php';
use Nyholm\Psr7\Response;
use Nyholm\Psr7\Factory\Psr17Factory;
use Spiral\RoadRunner\Worker;
use Spiral\RoadRunner\Http\PSR7Worker;
$worker = Worker::create();
$factory = new Psr17Factory();
$psr7 = new PSR7Worker($worker, $factory, $factory, $factory);
while (true) {
try {
$request = $psr7->waitRequest();
if ($request === null) {
break;
}
} catch (\Throwable $e) {
$psr7->respond(new Response(400));
continue;
}
try {
$psr7->respond(new Response(200, [], 'Hello RoadRunner!'));
} catch (\Throwable $e) {
$psr7->respond(new Response(500, [], 'Something Went Wrong!'));
$psr7->getWorker()->error((string)$e);
}
}
ただ、当然このままでは動作しません。Slim で利用するのですから、Slim のフレームワーク初期化処理やレスポンスの生成処理を加える必要があります。
Slim-Skeleton の index.php では、フレームワークの初期化が走ったあと、以下の部分でレスポンスを生成して出力しています。
<?php
...
$response = $app->handle($request);
$responseEmitter = new ResponseEmitter();
$responseEmitter->emit($response);
このため、ワーカーでは上記のレスポンス生成処理を、リクエストを取得して処理するループの中に加えてやれば OK です。
<?php
...
while (true) {
try {
$request = $psr7->waitRequest();
if ($request === null) {
break;
}
} catch (\Throwable $e) {
$psr7->respond(new Response(400));
continue;
}
try {
$response = $app->handle($request);
$psr7->respond($response);
} catch (\Throwable $e) {
$psr7->respond(new Response(500, [], 'Something Went Wrong!'));
$psr7->getWorker()->error((string)$e);
}
}
以下に完成したワーカーのコードを置いておきます。
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use App\Application\Handlers\HttpErrorHandler;
use App\Application\Handlers\ShutdownHandler;
use App\Application\ResponseEmitter\ResponseEmitter;
use App\Application\Settings\SettingsInterface;
use DI\ContainerBuilder;
use Slim\Factory\AppFactory;
use Slim\Factory\ServerRequestCreatorFactory;
use Nyholm\Psr7\Response;
use Nyholm\Psr7\Factory\Psr17Factory;
use Spiral\RoadRunner\Worker;
use Spiral\RoadRunner\Http\PSR7Worker;
$containerBuilder = new ContainerBuilder();
if (false) {
$containerBuilder->enableCompilation(__DIR__ . '/var/cache');
}
$settings = require __DIR__ . '/app/settings.php';
$settings($containerBuilder);
$dependencies = require __DIR__ . '/app/dependencies.php';
$dependencies($containerBuilder);
$repositories = require __DIR__ . '/app/repositories.php';
$repositories($containerBuilder);
$container = $containerBuilder->build();
AppFactory::setContainer($container);
$app = AppFactory::create();
$callableResolver = $app->getCallableResolver();
$middleware = require __DIR__ . '/app/middleware.php';
$middleware($app);
$routes = require __DIR__ . '/app/routes.php';
$routes($app);
@var
$settings = $container->get(SettingsInterface::class);
$displayErrorDetails = $settings->get('displayErrorDetails');
$logError = $settings->get('logError');
$logErrorDetails = $settings->get('logErrorDetails');
$serverRequestCreator = ServerRequestCreatorFactory::create();
$request = $serverRequestCreator->createServerRequestFromGlobals();
$responseFactory = $app->getResponseFactory();
$errorHandler = new HttpErrorHandler($callableResolver, $responseFactory);
$shutdownHandler = new ShutdownHandler($request, $errorHandler,
$displayErrorDetails);
register_shutdown_function($shutdownHandler);
$app->addRoutingMiddleware();
$app->addBodyParsingMiddleware();
$errorMiddleware = $app->addErrorMiddleware($displayErrorDetails,
$logError, $logErrorDetails);
$errorMiddleware->setDefaultErrorHandler($errorHandler);
$worker = Worker::create();
$factory = new Psr17Factory();
$psr7 = new PSR7Worker($worker, $factory, $factory, $factory);
while (true) {
try {
$request = $psr7->waitRequest();
if ($request === null) {
break;
}
} catch (\Throwable $e) {
$psr7->respond(new Response(400));
continue;
}
try {
$response = $app->handle($request);
$psr7->respond($response);
} catch (\Throwable $e) {
$psr7->respond(new Response(500, [], 'Something Went Wrong!'));
$psr7->getWorker()->error((string)$e);
}
}
性能検証
Apache Bench で性能を簡単に測定した結果を以下に示します。
条件
| 項目 |
内容 |
| サーバのインスタンスタイプ |
c5.large |
| クライアントのインスタンスタイプ |
c5.xlarge |
| HTTP サーバとアプリケーションサーバ間の通信 |
Unix ソケット |
| php-fpm のプロセス数 |
100 固定 |
| RoadRunner のワーカー数 |
100 固定 |
| コンテンツ |
Hello World! を返すだけ |
| ログ出力 |
しない |
結果
| 同時接続数 |
nginx + php-fpm |
RoadRunner |
| 1 |
660.75 |
925.74 |
| 2 |
1059.36 |
1659.52 |
| 3 |
1241.54 |
2191.29 |
| 4 |
1283.96 |
2475.24 |
| 5 |
1308.92 |
2591.36 |
| 10 |
1363.07 |
2820.52 |
| 100 |
1373.17 |
3268.43 |
| 150 |
1353.76 |
3261.71 |
RoadRunner は nginx+php-fpm の約 2.5 倍リクエストを捌けていました。すごい。*1
終わりに
Ubuntu と Slim の構成 (とイスの話題) でピンときた方もいると思いますが、これは ISUCON を意識したものです。
社内で声をかけて今回はじめて参加することが出来たので、素振りの時に試した内容を記事にしました。
いろいろ学びがあり、何よりやってて楽しかったので来年以降も参加したいと思っています。
We're hiring!
弊社では、一緒に働いてくださるエンジニアを募集しています。
rarejob-tech.co.jp