
テストはYandex Tankを使用して実行されました。
Symfony 4とPHP 7.2がアプリケーションとして使用されました。
目標は、さまざまな負荷でのサービスの特性を比較し、最適なオプションを見つけることでした。
便宜上、すべてがdockerコンテナーに収集され、docker-composeを使用して発生します。
猫の下にはたくさんの表とグラフがあります。
ソースコードはこちらです。
この記事で説明されているコマンドの例はすべて、プロジェクトディレクトリから実行する必要があります。
アプリ
アプリケーションはSymfony 4およびPHP 7.2で実行されます。
1つのルートのみに応答し、戻ります。
- 乱数;
- 環境;
- プロセスのpid。
- 連携するサービスの名前。
- php.ini変数。
回答例:
curl 'http://127.0.0.1:8000/' | python -m json.tool { "env": "prod", "type": "php-fpm", "pid": 8, "random_num": 37264, "php": { "version": "7.2.12", "date.timezone": "Europe/Paris", "display_errors": "", "error_log": "/proc/self/fd/2", "error_reporting": "32767", "log_errors": "1", "memory_limit": "256M", "opcache.enable": "1", "opcache.max_accelerated_files": "20000", "opcache.memory_consumption": "256", "opcache.validate_timestamps": "0", "realpath_cache_size": "4096K", "realpath_cache_ttl": "600", "short_open_tag": "" } }
PHPは各コンテナーで構成されます。
- OPcacheは有効です。
- composerを使用して構成されたブートストラップキャッシュ。
- php.iniの設定は、 Symfonyのベストプラクティスに沿っています。
ログはstderrに書き込まれます。
/config/packages/prod/monolog.yaml
monolog: handlers: main: type: stream path: "php://stderr" level: error console: type: console
キャッシュは/ dev / shmに書き込まれます。
/src/Kernel.php
... class Kernel extends BaseKernel { public function getCacheDir() { if ($this->environment === 'prod') { return '/dev/shm/symfony-app/cache/' . $this->environment; } else { return $this->getProjectDir() . '/var/cache/' . $this->environment; } } } ...
各docker-composeは、3つの主要なコンテナーを起動します。
- Nginx-リバースプロキシサーバー。
- アプリ-すべての依存関係を備えた準備済みのアプリケーションコード。
- PHP FPM \ Nginx Unit \ Road Runner \ React PHP-アプリケーションサーバー。
要求処理は、2つのアプリケーションインスタンスに制限されます(プロセッサコアの数による)。
サービス
PHP FPM
PHPプロセスマネージャー。 Cで書かれた
長所:
- メモリを追跡する必要はありません。
- アプリケーションで何かを変更する必要はありません。
短所:
- PHPは、リクエストごとに変数を初期化する必要があります。
docker-composeでアプリケーションを起動するコマンド:
cd docker/php-fpm && docker-compose up -d
PHP PPM
PHPプロセスマネージャー。 PHPで書かれています。
長所:
- 変数を一度初期化してから使用します。
- アプリケーションで何かを変更する必要はありません(Symfony / Laravel、Zend、CakePHP用の既製のモジュールがあります)。
短所:
- メモリを追跡する必要があります。
docker-composeでアプリケーションを起動するコマンド:
cd docker/php-ppm && docker-compose up -d
Nginxユニット
Nginxチームのアプリケーションサーバー。 Cで書かれた
長所:
- HTTP APIを使用して構成を変更できます。
- 1つのアプリケーションの複数のインスタンスを、異なる構成と言語バージョンで同時に実行できます。
- メモリを追跡する必要はありません。
- アプリケーションで何かを変更する必要はありません。
短所:
- PHPは、リクエストごとに変数を初期化する必要があります。
nginx-unit設定ファイルから環境変数を渡すには、php.iniを修正する必要があります。
; Nginx Unit variables_order=E
docker-composeでアプリケーションを起動するコマンド:
cd docker/nginx-unit && docker-compose up -d
React PHP
イベントプログラミング用のライブラリ。 PHPで書かれています。
長所:
- ライブラリを使用して、変数を1回だけ初期化し、それらの操作を続行するサーバーを作成できます。
短所:
- サーバーのコードを記述する必要があります。
- メモリを追跡する必要があります。
ワーカーに--reboot-kernel-after-requestフラグを使用すると、 リクエストごとにSymfonyカーネルが再初期化されます。 このアプローチでは、メモリを監視する必要はありません。
#!/usr/bin/env php <?php use App\Kernel; use Symfony\Component\Debug\Debug; use Symfony\Component\HttpFoundation\Request; require __DIR__ . '/../config/bootstrap.php'; $env = $_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? 'dev'; $debug = (bool)($_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? ('prod' !== $env)); if ($debug) { umask(0000); Debug::enable(); } if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? $_ENV['TRUSTED_PROXIES'] ?? false) { Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST); } if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? $_ENV['TRUSTED_HOSTS'] ?? false) { Request::setTrustedHosts(explode(',', $trustedHosts)); } $loop = React\EventLoop\Factory::create(); $kernel = new Kernel($env, $debug); $kernel->boot(); $rebootKernelAfterRequest = in_array('--reboot-kernel-after-request', $argv); /** @var \Psr\Log\LoggerInterface $logger */ $logger = $kernel->getContainer()->get('logger'); $server = new React\Http\Server(function (Psr\Http\Message\ServerRequestInterface $request) use ($kernel, $logger, $rebootKernelAfterRequest) { $method = $request->getMethod(); $headers = $request->getHeaders(); $content = $request->getBody(); $post = []; if (in_array(strtoupper($method), ['POST', 'PUT', 'DELETE', 'PATCH']) && isset($headers['Content-Type']) && (0 === strpos($headers['Content-Type'], 'application/x-www-form-urlencoded')) ) { parse_str($content, $post); } $sfRequest = new Symfony\Component\HttpFoundation\Request( $request->getQueryParams(), $post, [], $request->getCookieParams(), $request->getUploadedFiles(), [], $content ); $sfRequest->setMethod($method); $sfRequest->headers->replace($headers); $sfRequest->server->set('REQUEST_URI', $request->getUri()); if (isset($headers['Host'])) { $sfRequest->server->set('SERVER_NAME', current($headers['Host'])); } try { $sfResponse = $kernel->handle($sfRequest); } catch (\Exception $e) { $logger->error('Internal server error', ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]); $sfResponse = new \Symfony\Component\HttpFoundation\Response('Internal server error', 500); } catch (\Throwable $e) { $logger->error('Internal server error', ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]); $sfResponse = new \Symfony\Component\HttpFoundation\Response('Internal server error', 500); } $kernel->terminate($sfRequest, $sfResponse); if ($rebootKernelAfterRequest) { $kernel->reboot(null); } return new React\Http\Response( $sfResponse->getStatusCode(), $sfResponse->headers->all(), $sfResponse->getContent() ); }); $server->on('error', function (\Exception $e) use ($logger) { $logger->error('Internal server error', ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]); }); $socket = new React\Socket\Server('tcp://0.0.0.0:9000', $loop); $server->listen($socket); $logger->info('Server running', ['addr' => 'tcp://0.0.0.0:9000']); $loop->run();
docker-composeでアプリケーションを起動するコマンド:
cd docker/react-php && docker-compose up -d --scale php=2
ロードランナー
WebサーバーとPHPプロセスマネージャー。 Golangで書かれています。
長所:
- 変数を一度だけ初期化して、引き続き作業するワーカーを作成できます。
短所:
- ワーカーのコードを記述する必要があります。
- メモリを追跡する必要があります。
ワーカーに--reboot-kernel-after-requestフラグを使用すると、 リクエストごとにSymfonyカーネルが再初期化されます。 このアプローチでは、メモリを監視する必要はありません。
#!/usr/bin/env php <?php use App\Kernel; use Spiral\Goridge\SocketRelay; use Spiral\RoadRunner\PSR7Client; use Spiral\RoadRunner\Worker; use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory; use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory; use Symfony\Component\Debug\Debug; use Symfony\Component\HttpFoundation\Request; require __DIR__ . '/../config/bootstrap.php'; $env = $_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? 'dev'; $debug = (bool)($_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? ('prod' !== $env)); if ($debug) { umask(0000); Debug::enable(); } if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? $_ENV['TRUSTED_PROXIES'] ?? false) { Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST); } if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? $_ENV['TRUSTED_HOSTS'] ?? false) { Request::setTrustedHosts(explode(',', $trustedHosts)); } $kernel = new Kernel($env, $debug); $kernel->boot(); $rebootKernelAfterRequest = in_array('--reboot-kernel-after-request', $argv); $relay = new SocketRelay('/tmp/road-runner.sock', null, SocketRelay::SOCK_UNIX); $psr7 = new PSR7Client(new Worker($relay)); $httpFoundationFactory = new HttpFoundationFactory(); $diactorosFactory = new DiactorosFactory(); while ($req = $psr7->acceptRequest()) { try { $request = $httpFoundationFactory->createRequest($req); $response = $kernel->handle($request); $psr7->respond($diactorosFactory->createResponse($response)); $kernel->terminate($request, $response); if($rebootKernelAfterRequest) { $kernel->reboot(null); } } catch (\Throwable $e) { $psr7->getWorker()->error((string)$e); } }
docker-composeでアプリケーションを起動するコマンド:
cd docker/road-runner && docker-compose up -d
テスト中
テストはYandex Tankを使用して実行されました。
アプリケーションとYandex Tankは異なる仮想サーバー上にありました。
アプリケーションを備えた仮想サーバーの機能:
仮想化 :KVM
CPU :2コア
RAM :4096 MB
SSD :50 GB
接続 :100MBit
OS :CentOS 7(64x)
テスト済みのサービス:
- php-fpm
- php-ppm
- nginx-unit
- ロードランナー
- road-runner-reboot(フラグ--reboot-kernel-after-requestを使用 )
- 反応php
- react-php-reboot(フラグ--reboot-kernel-after-requestを使用 )
テスト用に1000/10000 rps追加サービスphp-fpm-80
php-fpm構成が使用されました。
pm = dynamic pm.max_children = 80
Yandexタンクは、ターゲットで何回撃つ必要があるかを事前に決定し、カートリッジがなくなるまで停止しません。 サービスの応答速度によっては、テスト構成で指定されているよりもテスト時間が長くなる場合があります。 このため、異なるサービスのグラフィックスの長さは異なる場合があります。 サービスの応答が遅いほど、スケジュールは長くなります。
Yandex Tankの各サービスと構成について、1つのテストのみが実施されました。 このため、数値は不正確になる場合があります。 サービスの特性を相互に評価することが重要でした。
100 rps
Phantom Yandexタンクの構成
phantom: load_profile: load_type: rps schedule: line(1, 100, 60s) const(100, 540s)
詳細レポートリンク
- php-fpm https://overload.yandex.net/150666
- php-ppm https://overload.yandex.net/150670
- nginx-unit https://overload.yandex.net/150675
- ロードランナーhttps://overload.yandex.net/150681
- road-runner-reboot https://overload.yandex.net/151961
- react-php https://overload.yandex.net/150697
- react-php-reboot https://overload.yandex.net/152063
応答時間のパーセンタイル
95%(ミリ秒) | 90%(ミリ秒) | 80%(ミリ秒) | 50%(ミリ秒) | HTTP OK(%) | HTTP OK(カウント) | |
---|---|---|---|---|---|---|
php-fpm | 9.9 | 6.3 | 4.35 | 3.59 | 100 | 57030 |
php-ppm | 9.4 | 6 | 3.88 | 3.16 | 100 | 57030 |
nginx-unit | 11 | 6.6 | 4.43 | 3.69 | 100 | 57030 |
ロードランナー | 8.1 | 5.1 | 3.53 | 2.92 | 100 | 57030 |
ロードランナーリブート | 12 | 8.6 | 5.3 | 3.85 | 100 | 57030 |
反応php | 8.5 | 4.91 | 3.29 | 2.74 | 100 | 57030 |
react-php-reboot | 13 | 8.5 | 5.5 | 3.95 | 100 | 57030 |
モニタリング
CPU中央値(%) | CPU最大(%) | メモリ中央値(MB) | 最大メモリ(MB) | |
---|---|---|---|---|
php-fpm | 9.15 | 12.58 | 880.32 | 907.97 |
php-ppm | 7.08 | 13.68 | 901.72 | 913.80 |
nginx-unit | 9.56 | 12.54 | 923.02 | 943.90 |
ロードランナー | 5.57 | 8.61 | 992.71 | 1,001.46 |
ロードランナーリブート | 9.18 | 12.67 | 848.43 | 870.26 |
反応php | 4.53 | 6.58 | 1,004.68 | 1,009.91 |
react-php-reboot | 9.61 | 12.67 | 885.92 | 892.52 |
グラフ

図1.1 1秒あたりの平均応答時間

図1.2 1秒あたりの平均プロセッサ負荷

図1.3 1秒あたりの平均メモリ消費量
500 rps
Phantom Yandexタンクの構成
phantom: load_profile: load_type: rps schedule: line(1, 500, 60s) const(500, 540s)
詳細レポートリンク
- php-fpm https://overload.yandex.net/150705
- php-ppm https://overload.yandex.net/150710
- nginx-unit https://overload.yandex.net/150711
- ロードランナーhttps://overload.yandex.net/150715
- road-runner-reboot https://overload.yandex.net/152011
- react-php https://overload.yandex.net/150717
- react-php-reboot https://overload.yandex.net/152064
応答時間のパーセンタイル
95%(ミリ秒) | 90%(ミリ秒) | 80%(ミリ秒) | 50%(ミリ秒) | HTTP OK(%) | HTTP OK(カウント) | |
---|---|---|---|---|---|---|
php-fpm | 13 | 8.4 | 5.3 | 3.69 | 100 | 285030 |
php-ppm | 15 | 9 | 4.72 | 3.24 | 100 | 285030 |
nginx-unit | 12 | 8 | 5.5 | 3.93 | 100 | 285030 |
ロードランナー | 9.6 | 6 | 3.71 | 2.83 | 100 | 285030 |
ロードランナーリブート | 14 | 11 | 7.1 | 4.45 | 100 | 285030 |
反応php | 9.3 | 5.8 | 3.57 | 2.68 | 100 | 285030 |
react-php-reboot | 15 | 12 | 7.2 | 4.21 | 100 | 285030 |
モニタリング
CPU中央値(%) | CPU最大(%) | メモリ中央値(MB) | 最大メモリ(MB) | |
---|---|---|---|---|
php-fpm | 41.68 | 48.33 | 1,006.06 | 1,015.09 |
php-ppm | 33.90 | 48.90 | 1,046.32 | 1,055.00 |
nginx-unit | 42.13 | 47.92 | 1,006.67 | 1,015.73 |
ロードランナー | 08/24 | 06/28 | 1,035.86 | 1,044.58 |
ロードランナーリブート | 46.23 | 52.04 | 939.63 | 948.08 |
反応php | 19.57 | 23.42 | 1,049.83 | 1,060.26 |
react-php-reboot | 41.30 | 47.89 | 957.01 | 958.56 |
グラフ

図2.1 1秒あたりの平均応答時間

図2.2 1秒あたりの平均プロセッサ負荷

図2.3 1秒あたりの平均メモリ消費量
1000 rps
Phantom Yandexタンクの構成
phantom: load_profile: load_type: rps schedule: line(1, 1000, 60s) const(1000, 60s)
詳細レポートリンク
- php-fpm https://overload.yandex.net/150841
- php-fpm-80 https://overload.yandex.net/153612
- php-ppm https://overload.yandex.net/150842
- nginx-unit https://overload.yandex.net/150843
- ロードランナーhttps://overload.yandex.net/150844
- road-runner-reboot https://overload.yandex.net/152068
- react-php https://overload.yandex.net/150846
- react-php-reboot https://overload.yandex.net/152065
応答時間のパーセンタイル
95%(ミリ秒) | 90%(ミリ秒) | 80%(ミリ秒) | 50%(ミリ秒) | HTTP OK(%) | HTTP OK(カウント) | |
---|---|---|---|---|---|---|
php-fpm | 11050 | 11050 | 9040 | 195 | 80.67 | 72627 |
php-fpm-80 | 3150 | 1375 | 1165 | 152 | 99.85 | 89895 |
php-ppm | 2785 | 2740 | 2685 | 2545 | 100 | 90030 |
nginx-unit | 98 | 80 | 60 | 21 | 100 | 90030 |
ロードランナー | 27 | 15 | 7.1 | 3.21 | 100 | 90030 |
ロードランナーリブート | 1110 | 1100 | 1085 | 1060 | 100 | 90030 |
反応php | 23 | 13 | 5.6 | 2.86 | 100 | 90030 |
react-php-reboot | 28 | 24 | 19 | 11 | 100 | 90030 |
モニタリング
CPU中央値(%) | CPU最大(%) | メモリ中央値(MB) | 最大メモリ(MB) | |
---|---|---|---|---|
php-fpm | 12.66 | 78.25 | 990.16 | 1,006.56 |
php-fpm-80 | 83.78 | 91.28 | 746.01 | 937.24 |
php-ppm | 66.16 | 91.20 | 1,088.74 | 1,102.92 |
nginx-unit | 78.11 | 88.77 | 1,010.15 | 1,062.01 |
ロードランナー | 42.93 | 54.23 | 1,010.89 | 1,068.48 |
ロードランナーリブート | 77.64 | 85.66 | 976.44 | 1,044.05 |
反応php | 36.39 | 46.31 | 1,018.03 | 1,088.23 |
react-php-reboot | 72.11 | 81.81 | 911.28 | 961.62 |
グラフ

図3.1 1秒あたりの平均応答時間

図3.2 1秒あたりの平均応答時間(php-fpm、php-ppm、road-runner-rebootなし)

図3.3 1秒あたりの平均プロセッサ負荷

図3.4 1秒あたりの平均メモリ消費量
10000 rps
Phantom Yandexタンクの構成
phantom: load_profile: load_type: rps schedule: line(1, 10000, 30s) const(10000, 30s)
詳細レポートリンク
- php-fpm https://overload.yandex.net/150849
- php-fpm-80 https://overload.yandex.net/153615
- php-ppm https://overload.yandex.net/150874
- nginx-unit https://overload.yandex.net/150876
- ロードランナーhttps://overload.yandex.net/150881
- road-runner-reboot https://overload.yandex.net/152069
- react-php https://overload.yandex.net/150885
- react-php-reboot https://overload.yandex.net/152066
応答時間のパーセンタイル
95%(ミリ秒) | 90%(ミリ秒) | 80%(ミリ秒) | 50%(ミリ秒) | HTTP OK(%) | HTTP OK(カウント) | |
---|---|---|---|---|---|---|
php-fpm | 11050 | 11050 | 11050 | 1880 | 70.466 | 317107 |
php-fpm-80 | 3260 | 3140 | 1360 | 1145 | 99.619 | 448301 |
php-ppm | 2755 | 2730 | 2695 | 2605 | 100 | 450015 |
nginx-unit | 1020 | 1010 | 1000 | 980 | 100 | 450015 |
ロードランナー | 640 | 630 | 615 | 580 | 100 | 450015 |
ロードランナーリブート | 1130 | 1120 | 1110 | 1085 | 100 | 450015 |
反応php | 1890 | 1090 | 1045 | 58 | 99.996 | 449996 |
react-php-reboot | 3480 | 3070 | 1255 | 91 | 99.72 | 448753 |
モニタリング
CPU中央値(%) | CPU最大(%) | メモリ中央値(MB) | 最大メモリ(MB) | |
---|---|---|---|---|
php-fpm | 5.57 | 79.35 | 984.47 | 998.78 |
php-fpm-80 | 85.05 | 92.19 | 936.64 | 943.93 |
php-ppm | 66.86 | 82.41 | 1,089.31 | 1,097.41 |
nginx-unit | 86.14 | 93.94 | 1,067.71 | 1,069.52 |
ロードランナー | 73.41 | 82.72 | 1,129.48 | 1,134.00 |
ロードランナーリブート | 80.32 | 86.29 | 982.69 | 984.80 |
反応php | 73.76 | 82.18 | 1,101.71 | 1,105.06 |
react-php-reboot | 85.77 | 91.92 | 975.85 | 978.42 |

図4.1 1秒あたりの平均応答時間

図4.2 1秒あたりの平均応答時間(php-fpm、php-ppmなし)

図4.3 1秒あたりの平均プロセッサ負荷

図4.4 1秒あたりの平均メモリ消費量
まとめ
以下は、負荷に応じたサービスの特性の変化を示すグラフです。 チャートを表示するとき、すべてのサービスがリクエストの100%に応答したわけではないことに注意してください。

図5.1応答時間の95%パーセンタイル

図5.2応答時間の95%パーセンタイル(php-fpmなし)

グラフ5.3最大CPU負荷

図5.4最大メモリ消費
私の意見では、最適なソリューション(コードを変更せずに)はNginx Unitプロセスマネージャーです。 応答速度に良い結果を示し、会社のサポートがあります。
いずれの場合でも、ワークロード、サーバーリソース、開発者の能力に応じて、開発アプローチとツールを個別に選択する必要があります。
UPD
テスト用に1000/10000 rps追加サービスphp-fpm-80
php-fpm構成が使用されました。
pm = dynamic pm.max_children = 80