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



Las pruebas se realizaron con Yandex Tank.
Symfony 4 y PHP 7.2 se utilizaron como una aplicación.
El objetivo era comparar las características de los servicios en diferentes cargas y encontrar la mejor opción.
Por conveniencia, todo se recolecta en contenedores acoplables y se eleva utilizando docker-compose.
Debajo de un gato hay muchas tablas y gráficos.

El código fuente está aquí .
Todos los ejemplos de comandos descritos en el artículo deben ejecutarse desde el directorio del proyecto.


App


La aplicación se ejecuta en Symfony 4 y PHP 7.2.


Responde solo una ruta y regresa:


  • número aleatorio
  • medio ambiente
  • pid del proceso;
  • nombre del servicio con el que trabaja;
  • php.ini variables.

Ejemplo de respuesta:


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 está configurado en cada contenedor:


  • OPcache está habilitado;
  • caché de arranque configurado usando el compositor;
  • La configuración de php.ini está en línea con las mejores prácticas de Symfony .

Los registros están escritos en stderr:
/config/packages/prod/monolog.yaml


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

El caché está escrito en / 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; } } } ... 

Cada docker-compose lanza tres contenedores principales:


  • Nginx - servidor proxy inverso;
  • Aplicación: código de aplicación preparado con todas las dependencias;
  • PHP FPM \ Nginx Unit \ Road Runner \ React PHP - servidor de aplicaciones.

El procesamiento de solicitudes está limitado a dos instancias de aplicación (por el número de núcleos de procesador).


Servicios


PHP FPM


Administrador de procesos PHP. Escrito en C.


Pros:


  • no es necesario hacer un seguimiento de la memoria;
  • No es necesario cambiar nada en la aplicación.

Contras:


  • PHP debe inicializar variables para cada solicitud.

Comando para iniciar la aplicación con docker-compose:


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

PHP PPM


Administrador de procesos PHP. Está escrito en PHP.


Pros:


  • inicializa las variables una vez y luego las usa;
  • no es necesario cambiar nada en la aplicación (hay módulos listos para Symfony / Laravel, Zend, CakePHP).

Contras:


  • Necesito seguir la memoria.

Comando para iniciar la aplicación con docker-compose:


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

Unidad Nginx


Servidor de aplicaciones del equipo Nginx. Escrito en C.


Pros:


  • Puede cambiar la configuración utilizando la API HTTP;
  • Puede ejecutar varias instancias de una aplicación simultáneamente con diferentes configuraciones y versiones de idioma;
  • no es necesario hacer un seguimiento de la memoria;
  • No es necesario cambiar nada en la aplicación.

Contras:


  • PHP debe inicializar variables para cada solicitud.

Para pasar variables de entorno desde el archivo de configuración de la unidad nginx, debe corregir php.ini:


 ; Nginx Unit variables_order=E 

Comando para iniciar la aplicación con docker-compose:


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

Reaccionar PHP


Biblioteca para programación de eventos. Está escrito en PHP.


Pros:


  • utilizando la biblioteca, puede escribir un servidor que inicializará las variables solo una vez y continuará trabajando con ellas.

Contras:


  • debes escribir código para el servidor;
  • necesita hacer un seguimiento de la memoria.

Si utiliza el indicador --reboot-kernel-after-request para el trabajador, Symfony Kernel se reiniciará para cada solicitud. Con este enfoque, no necesita monitorear la memoria.


Código de trabajador
 #!/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(); 

Comando para iniciar la aplicación con docker-compose:


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

Correcaminos


Servidor web y gestor de procesos PHP. Escrito en Golang.


Pros:


  • puede escribir un trabajador que inicializará las variables solo una vez y continuará trabajando con ellas.

Contras:


  • debe escribir el código para el trabajador;
  • necesita hacer un seguimiento de la memoria.

Si utiliza el indicador --reboot-kernel-after-request para el trabajador, Symfony Kernel se reiniciará para cada solicitud. Con este enfoque, no necesita monitorear la memoria.


Código de trabajador
 #!/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); } } 

Comando para iniciar la aplicación con docker-compose:


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

Prueba


Las pruebas se realizaron con Yandex Tank.
La aplicación y Yandex Tank estaban en diferentes servidores virtuales.


Características del servidor virtual con la aplicación:
Virtualización : KVM
CPU : 2 núcleos
RAM : 4096 MB
SSD : 50 GB
Conexión : 100 MBit
SO : CentOS 7 (64x)


Servicios probados:


  • php-fpm
  • php-ppm
  • unidad nginx
  • correcaminos
  • road-runner-reboot (con la bandera --reboot-kernel-after-request )
  • reaccionar-php
  • react-php-reboot (con el indicador --reboot-kernel-after-request )

Para pruebas 1000/10000 rps servicio php-fpm-80 agregado
Se usó la configuración php-fpm para ello:


 pm = dynamic pm.max_children = 80 

Yandex Tank determina de antemano cuántas veces necesita disparar al objetivo, y no se detiene hasta que se agoten los cartuchos. Dependiendo de la velocidad de respuesta del servicio, el tiempo de prueba puede ser mayor que el especificado en la configuración de prueba. Debido a esto, los gráficos de diferentes servicios pueden tener diferentes longitudes. Cuanto más lento responda el servicio, más largo será su horario.


Para cada servicio y configuración de Yandex Tank, solo se realizó una prueba. Debido a esto, los números pueden ser inexactos. Era importante evaluar las características de los servicios entre sí.


100 rps


Configuración del tanque Phantom Yandex


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

Enlaces de informes detallados



Percentiles de tiempo de respuesta


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (recuento)
php-fpm9,96.34.353,5910057030
php-ppm9.46 63.883.1610057030
unidad nginx116.64.433,6910057030
correcaminos8.15.13,532,9210057030
road-runner-reinicio128.65.33.8510057030
reaccionar-php8.54.913.292,7410057030
reaccionar-reiniciar-php138.55.53.9510057030

Monitoreo


mediana de la CPU (%)CPU máx. (%)mediana de memoria (MB)memoria máxima (MB)
php-fpm9.1512,58880,32907,97
php-ppm7.0813,68901,72913,80
unidad nginx9.5612,54923.02943,90
correcaminos5.578.61992,711.001,46
road-runner-reinicio9.1812,67848,43870,26
reaccionar-php4.536.581.004,681.009,91
reaccionar-reiniciar-php9.6112,67885,92892,52

Gráficos



Gráfico 1.1 Tiempo de respuesta promedio por segundo



Gráfico 1.2 Carga promedio del procesador por segundo



Gráfico 1.3 Consumo promedio de memoria por segundo


500 rps


Configuración del tanque Phantom Yandex


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

Enlaces de informes detallados



Percentiles de tiempo de respuesta


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (recuento)
php-fpm138.45.33,69100285030
php-ppm159 94.723.24100285030
unidad nginx1285.53.93100285030
correcaminos9.66 63.712,83100285030
road-runner-reinicio14117.14.45100285030
reaccionar-php9.35.83,572,68100285030
reaccionar-reiniciar-php15127.24.21100285030

Monitoreo


mediana de la CPU (%)CPU máx. (%)mediana de memoria (MB)memoria máxima (MB)
php-fpm41,6848,331.006,061,015.09
php-ppm33,9048,901.046,321,055.00
unidad nginx42,1347,921.006,671.015,73
correcaminos24/0828/061.035,861,044.58
road-runner-reinicio46,2352.04939,63948.08
reaccionar-php19,5723,421.049,831.060,26
reaccionar-reiniciar-php41,3047,89957,01958,56

Gráficos



Gráfico 2.1 Tiempo de respuesta promedio por segundo



Gráfico 2.2 Carga promedio del procesador por segundo



Gráfico 2.3 Consumo promedio de memoria por segundo


1000 rps


Configuración del tanque Phantom Yandex


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

Enlaces de informes detallados



Percentiles de tiempo de respuesta


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (recuento)
php-fpm1105011050904019580,6772627
php-fpm-8031501375116515299,8589895
php-ppm278527402685254510090030
unidad nginx988060 602110090030
correcaminos27157.13.2110090030
road-runner-reinicio111011001085106010090030
reaccionar-php23135.62,8610090030
reaccionar-reiniciar-php2824191110090030

Monitoreo


mediana de la CPU (%)CPU máx. (%)mediana de memoria (MB)memoria máxima (MB)
php-fpm12,6678,25990,161.006,56
php-fpm-8083,7891,28746,01937,24
php-ppm66,1691,201.088,741.102,92
unidad nginx78,1188,771,010.151.062,01
correcaminos42,9354,231,010.891.068,48
road-runner-reinicio77,6485,66976,441,044.05
reaccionar-php36,3946,311.018,031.088,23
reaccionar-reiniciar-php72,1181,81911.28961,62

Gráficos



Gráfico 3.1 Tiempo de respuesta promedio por segundo



Gráfico 3.2 Tiempo de respuesta promedio por segundo (sin php-fpm, php-ppm, road-runner-reinicio)



Gráfico 3.3 Carga promedio del procesador por segundo



Gráfico 3.4 Consumo promedio de memoria por segundo


10000 rps


Configuración del tanque Phantom Yandex


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

Enlaces de informes detallados



Percentiles de tiempo de respuesta


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (recuento)
php-fpm110501105011050188070,466317107
php-fpm-80326031401360114599,619448301
php-ppm2755273026952605100450015
unidad nginx102010101000980100450015
correcaminos640630615580100450015
road-runner-reinicio1130112011101085100450015
reaccionar-php1890109010455899,9964,49996
reaccionar-reiniciar-php34803070125591 9199,72448753

Monitoreo


mediana de la CPU (%)CPU máx. (%)mediana de memoria (MB)memoria máxima (MB)
php-fpm5.5779,35984,47998,78
php-fpm-8085.0592,19936,64943,93
php-ppm66,8682,411.089,311.097,41
unidad nginx86,1493,941.067,711.069,52
correcaminos73,4182,721.129,481,134.00
road-runner-reinicio80,3286,29982,69984.80
reaccionar-php73,7682,181.101,711.105,06
reaccionar-reiniciar-php85,7791,92975,85978,42


Gráfico 4.1 Tiempo de respuesta promedio por segundo



Gráfico 4.2 Tiempo de respuesta promedio por segundo (sin php-fpm, php-ppm)



Gráfico 4.3 Carga promedio del procesador por segundo



Gráfico 4.4 Consumo promedio de memoria por segundo


Resumen


Aquí hay gráficos que muestran el cambio en las características de los servicios dependiendo de la carga. Al ver los gráficos, vale la pena considerar que no todos los servicios respondieron el 100% de las solicitudes.



Gráfico 5.1 percentil 95% del tiempo de respuesta



Gráfico 5.2 percentil 95% del tiempo de respuesta (sin php-fpm)



Gráfico 5.3 Carga máxima de CPU



Gráfico 5.4 Consumo máximo de memoria


La solución óptima (sin cambiar el código), en mi opinión, es el administrador de procesos de la Unidad Nginx. Muestra buenos resultados en velocidad de respuesta y cuenta con el apoyo de la empresa.


En cualquier caso, el enfoque de desarrollo y las herramientas deben seleccionarse individualmente, dependiendo de sus cargas de trabajo, recursos del servidor y capacidades del desarrollador.


UPD
Para pruebas 1000/10000 rps servicio php-fpm-80 agregado
Se usó la configuración php-fpm para ello:


 pm = dynamic pm.max_children = 80 

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


All Articles