📜 ⬆️ ⬇️

Compare PHP FPM, PHP PPM, Nginx Unit, React PHP and RoadRunner



Testing was done using Yandex Tank.
Symfony 4 and PHP 7.2 were used as applications.
The goal was to compare the characteristics of services under different loads and find the best option.
For convenience, everything is collected in docker-containers and lifted using docker-compose.
There are a lot of tables and graphs under the cut.

The source code is here .
All command examples described in the article should be executed from the project directory.


application


The application runs on symfony 4 and php 7.2.


It responds to only one route and returns:



Sample answer:


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 is configured in each container:



Logs are written to stderr:
/config/packages/prod/monolog.yaml


 monolog: handlers: main: type: stream path: "php://stderr" level: error console: type: console 

The cache is written to / 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; } } } ... 

Each docker-compose runs three main containers:



Request processing is limited to two instances of the application (by the number of processor cores).


Services


PHP FPM


PHP process manager. Written in C.


Pros:



Minuses:



The command to run the application with docker-compose:


 cd docker/php-fpm && docker-compose up -d 

PHP PPM


PHP process manager. Written in PHP.


Pros:



Minuses:



The command to run the application with docker-compose:


 cd docker/php-ppm && docker-compose up -d 

Nginx unit


Application server from the Nginx team. Written in C.


Pros:



Minuses:



To transfer environment variables from the nginx-unit configuration file, you need to fix php.ini:


 ; Nginx Unit variables_order=E 

The command to run the application with docker-compose:


 cd docker/nginx-unit && docker-compose up -d 

React PHP


Library for event programming. Written in PHP.


Pros:



Minuses:



If you use the --reboot-kernel-after-request flag for the worker, the symfony Kernel will be reinitialized for each request. With this approach, you do not need to monitor the memory.


Code worker
 #!/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(); 

The command to run the application with docker-compose:


 cd docker/react-php && docker-compose up -d --scale php=2 

Road runner


Web server and PHP process manager. Written in Golang.


Pros:



Minuses:



If you use the --reboot-kernel-after-request flag for the worker, the symfony Kernel will be reinitialized for each request. With this approach, you do not need to monitor the memory.


Code worker
 #!/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); } } 

The command to run the application with docker-compose:


 cd docker/road-runner && docker-compose up -d 

Testing


Testing was done using Yandex Tank.
The application and Yandex Tank were on different virtual servers.


Characteristics of the virtual server with the application:
Virtualization : KVM
CPU : 2 cores
RAM : 4096 MB
SSD : 50 GB
Connection : 100MBit
OS : CentOS 7 (64x)


Tested services:



Php-fpm-80 service added for 1000/1000 rps tests
For it, the php-fpm configuration was used:


 pm = dynamic pm.max_children = 80 

Yandex Tank determines in advance how many times he needs to shoot at the target, and does not stop until the ammo runs out. Depending on the speed of the service response, the test time may be longer than specified in the test configuration. Because of this, the graphics of different services may have different lengths. The slower the service responds, the longer its schedule will be.


For each service and configuration Yandex Tank was conducted only one test. Because of this, the numbers may be inaccurate. It was important to evaluate the characteristics of the services relative to each other.


100 rps


Phantom Yandex Tank configuration


 phantom: load_profile: load_type: rps schedule: line(1, 100, 60s) const(100, 540s) 

Links with a detailed report



Percentile response time


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (count)
php-fpm9.96.34.353.5910057030
php-ppm9.463.883.1610057030
nginx uniteleven6.64.433.6910057030
road-runner8.15.13.532.9210057030
road-runner-reboot128.65.33.8510057030
react-php8.54.913.292.7410057030
react-php-reboot138.55.53.9510057030

Monitoring


cpu median (%)cpu max (%)memory median (MB)memory max (MB)
php-fpm9.1512.58880.32907.97
php-ppm7.0813.68901.72913.80
nginx unit9.5612.54923.02943.90
road-runner5.578.61992.711,001.46
road-runner-reboot9.1812.67848.43870.26
react-php4.536.581,004.681,009.91
react-php-reboot9.6112.67885.92892.52

Charts



Graph 1.1 Average response time per second



Chart 1.2 Average processor load per second



Chart 1.3 Average Memory Consumption Per Second


500 rps


Phantom Yandex Tank configuration


 phantom: load_profile: load_type: rps schedule: line(1, 500, 60s) const(500, 540s) 

Links with a detailed report



Percentile response time


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (count)
php-fpm138.45.33.69100285030
php-ppm1594.723.24100285030
nginx unit12eight5.53.93100285030
road-runner9.663.712.83100285030
road-runner-reboot14eleven7.14.45100285030
react-php9.35.83.572.68100285030
react-php-reboot15127.24.21100285030

Monitoring


cpu median (%)cpu max (%)memory median (MB)memory max (MB)
php-fpm41.6848.331,006.061,015.09
php-ppm33.9048.901,046.321,055.00
nginx unit42.1347.921,006.671,015.73
road-runner24.0828.061,035.861,044.58
road-runner-reboot46.2352.04939.63948.08
react-php19.5723.421,049.831,060.26
react-php-reboot41.3047.89957.01958.56

Charts



Graph 2.1 Average response time per second



Graph 2.2 Average CPU load per second



Graph 2.3 Average Memory Consumption Per Second


1000 rps


Phantom Yandex Tank configuration


 phantom: load_profile: load_type: rps schedule: line(1, 1000, 60s) const(1000, 60s) 

Links with a detailed report



Percentile response time


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (count)
php-fpm1105011050904019580.6772627
php-fpm-8031501375116515299.8589895
php-ppm278527402685254510090030
nginx unit9880602110090030
road-runner27157.13.2110090030
road-runner-reboot111011001085106010090030
react-php23135.62.8610090030
react-php-reboot2824nineteeneleven10090030

Monitoring


cpu median (%)cpu max (%)memory median (MB)memory max (MB)
php-fpm12.6678.25990.161,006.56
php-fpm-8083.7891.28746.01937.24
php-ppm66.1691.201,088.741,102.92
nginx unit78.1188.771,010.151,062.01
road-runner42.9354.231,010.891,068.48
road-runner-reboot77.6485.66976.441,044.05
react-php36.3946.311,018.031,088.23
react-php-reboot72.1181.81911.28961.62

Charts



Graph 3.1 Average response time per second



Graph 3.2 Average response time per second (without php-fpm, php-ppm, road-runner-reboot)



Chart 3.3 Average processor load per second



Graph 3.4 Average memory consumption per second


10,000 rps


Phantom Yandex Tank configuration


 phantom: load_profile: load_type: rps schedule: line(1, 10000, 30s) const(10000, 30s) 

Links with a detailed report



Percentile response time


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (count)
php-fpm110501105011050188070.466317107
php-fpm-80326031401360114599.619448301
php-ppm2755273026952605100450015
nginx unit102010101000980100450015
road-runner640630615580100450015
road-runner-reboot1130112011101085100450015
react-php1890109010455899.996449996
react-php-reboot3480307012559199.72448753

Monitoring


cpu median (%)cpu max (%)memory median (MB)memory max (MB)
php-fpm5.5779.35984.47998.78
php-fpm-8085.0592.19936.64943.93
php-ppm66.8682.411,089.311,097.41
nginx unit86.1493.941,067.711,069.52
road-runner73.4182.721,129.481,134.00
road-runner-reboot80.3286.29982.69984.80
react-php73.7682.181,101.711,105.06
react-php-reboot85.7791.92975.85978.42


Chart 4.1 Average response time per second



Graph 4.2 Average response time per second (without php-fpm, php-ppm)



Chart 4.3 Average processor load per second



Graph 4.4 Average memory consumption per second


Results


Here are collected graphs showing the change in the characteristics of services depending on the load. When viewing charts it is worth considering that not all services answered 100% of requests.



Chart 5.1 95% response time percentile



Graph 5.2 95% response percentile (without php-fpm)



Chart 5.3 Maximum CPU Load



Chart 5.4 Maximum Memory Consumption


The best solution (without changing the code), in my opinion, is the Nginx Unit process manager. He shows good results in speed of response and has the support of the company.


In any case, the development approach and tools must be chosen individually, depending on your workload, server resources, and developer capabilities.


UPD
Php-fpm-80 service added for 1000/1000 rps tests
For it, the php-fpm configuration was used:


 pm = dynamic pm.max_children = 80 

')

Source: https://habr.com/ru/post/431818/


All Articles