Compare PHP FPM, PHP PPM, unidade Nginx, React PHP e RoadRunner



O teste foi realizado usando o Yandex Tank.
Symfony 4 e PHP 7.2 foram usados ​​como uma aplicação.
O objetivo era comparar as características dos serviços em diferentes cargas e encontrar a melhor opção.
Por conveniência, tudo é coletado em contêineres do docker e gerado usando o docker-compose.
Sob um gato, existem muitas tabelas e gráficos.

O código fonte está aqui .
Todos os exemplos de comandos descritos no artigo devem ser executados no diretório do projeto.


App


O aplicativo é executado no Symfony 4 e PHP 7.2.


Responde apenas uma rota e retorna:


  • número aleatório;
  • meio ambiente
  • pid do processo;
  • nome do serviço com o qual trabalha;
  • variáveis ​​php.ini.

Exemplo de resposta:


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": "" } } 

O PHP está configurado em cada contêiner:


  • OPcache está ativado;
  • cache de bootstrap configurado usando o compositor;
  • As configurações do php.ini estão alinhadas com as melhores práticas do Symfony .

Os logs são escritos em stderr:
/config/packages/prod/monolog.yaml


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

O cache é gravado em / 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-compor lança três contêineres principais:


  • Nginx - servidor proxy reverso;
  • Código de aplicativo preparado por aplicativo com todas as dependências;
  • PHP FPM \ Unidade Nginx \ Road Runner \ React PHP - servidor de aplicativos.

O processamento de solicitações é limitado a duas instâncias do aplicativo (pelo número de núcleos do processador).


Serviços


PHP FPM


Gerenciador de processos PHP. Escrito em C.


Prós:


  • não há necessidade de acompanhar a memória;
  • não há necessidade de alterar nada no aplicativo.

Contras:


  • O PHP deve inicializar variáveis ​​para cada solicitação.

Comando para iniciar o aplicativo com docker-compose:


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

PHP PPM


Gerenciador de processos PHP. Está escrito em PHP.


Prós:


  • inicializa as variáveis ​​uma vez e depois as usa;
  • não é necessário alterar nada no aplicativo (existem módulos prontos para Symfony / Laravel, Zend, CakePHP).

Contras:


  • precisa seguir a memória.

Comando para iniciar o aplicativo com docker-compose:


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

Unidade Nginx


Servidor de aplicativos da equipe Nginx. Escrito em C.


Prós:


  • Você pode alterar a configuração usando a API HTTP;
  • Você pode executar várias instâncias de um aplicativo simultaneamente com diferentes configurações e versões de idiomas;
  • não há necessidade de acompanhar a memória;
  • não há necessidade de alterar nada no aplicativo.

Contras:


  • O PHP deve inicializar variáveis ​​para cada solicitação.

Para passar variáveis ​​de ambiente do arquivo de configuração da unidade nginx, você precisa corrigir o php.ini:


 ; Nginx Unit variables_order=E 

Comando para iniciar o aplicativo com docker-compose:


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

Reagir PHP


Biblioteca para programação de eventos. Está escrito em PHP.


Prós:


  • usando a biblioteca, você pode escrever um servidor que inicializará as variáveis ​​apenas uma vez e continuará a trabalhar com elas.

Contras:


  • você deve escrever o código para o servidor;
  • precisa acompanhar a memória.

Se você usar o sinalizador --reboot-kernel-after-request para o trabalhador, o Symfony Kernel será reinicializado para cada solicitação. Com essa abordagem, você não precisa monitorar a memória.


Código do Trabalhador
 #!/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 o aplicativo com docker-compose:


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

Corredor de estrada


Servidor Web e gerenciador de processos PHP. Escrito em Golang.


Prós:


  • você pode escrever um trabalhador que inicializará as variáveis ​​apenas uma vez e continuará trabalhando com elas.

Contras:


  • você deve escrever o código para o trabalhador;
  • precisa acompanhar a memória.

Se você usar o sinalizador --reboot-kernel-after-request para o trabalhador, o Symfony Kernel será reinicializado para cada solicitação. Com essa abordagem, você não precisa monitorar a memória.


Código do Trabalhador
 #!/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 o aplicativo com docker-compose:


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

Teste


O teste foi realizado usando o Yandex Tank.
O aplicativo e o Yandex Tank estavam em diferentes servidores virtuais.


Recursos do servidor virtual com o aplicativo:
Virtualização : KVM
CPU : 2 núcleos
RAM : 4096 MB
SSD : 50 GB
Conexão : 100MBit
SO : CentOS 7 (64x)


Serviços testados:


  • php-fpm
  • php-ppm
  • nginx-unit
  • corredor da estrada
  • road-runner-reboot (com o sinalizador --reboot-kernel-after-request )
  • react-php
  • react-php-reboot (com o sinalizador --reboot-kernel-after-request )

Para testes 1000/10000 rps, serviço php-fpm-80 adicionado
A configuração php-fpm foi usada para isso:


 pm = dynamic pm.max_children = 80 

O Yandex Tank determina antecipadamente quantas vezes ele precisa atirar no alvo e não pára até que os cartuchos acabem. Dependendo da velocidade da resposta do serviço, o tempo de teste pode ser maior que o especificado na configuração de teste. Por esse motivo, os gráficos de diferentes serviços podem ter comprimentos diferentes. Quanto mais lento o serviço responder, maior será o cronograma.


Para cada serviço e configuração do Yandex Tank, apenas um teste foi realizado. Por esse motivo, os números podem ser imprecisos. Era importante avaliar as características dos serviços em relação um ao outro.


100 rps


Configuração do tanque fantasma Yandex


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

Links detalhados do relatório



Percentis de tempo de resposta


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (contagem)
php-fpm9,96.3.4,353.5910057030
php-ppm9,463,883,1610057030
nginx-unit116.64,433,6910057030
corredor da estrada8.15.13.532,9210057030
road-runner-reboot128.65.33,8510057030
react-php8,54,913,292,7410057030
react-php-reboot138,55.53,9510057030

Monitoramento


mediana da CPU (%)CPU max (%)mediana da memória (MB)memória máxima (MB)
php-fpm9.1512,58880,32907,97
php-ppm7.0813,68901,72913,80
nginx-unit9,5612,54923,02943,90
corredor da estrada5,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

Gráficos



Gráfico 1.1 Tempo médio de resposta por segundo



Gráfico 1.2 Carga média do processador por segundo



Gráfico 1.3 Consumo médio de memória por segundo


500 rps


Configuração do tanque fantasma Yandex


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

Links detalhados do relatório



Percentis de tempo de resposta


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (contagem)
php-fpm138.45.33,69100285030
php-ppm1594,723,24100285030
nginx-unit1285.53,93100285030
corredor da estrada9,663,712,83100285030
road-runner-reboot14117.14,45100285030
react-php9,35,83.572,68100285030
react-php-reboot15127.24,21100285030

Monitoramento


mediana da CPU (%)CPU max (%)mediana da memória (MB)memória máxima (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
corredor da estrada24/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

Gráficos



Gráfico 2.1 Tempo médio de resposta por segundo



Gráfico 2.2 Carga média do processador por segundo



Gráfico 2.3 Consumo médio de memória por segundo


1000 rps


Configuração do tanque fantasma Yandex


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

Links detalhados do relatório



Percentis de tempo de resposta


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (contagem)
php-fpm1105011050904019580,6772627
php-fpm-8031501375116515299,8589895
php-ppm278527402685254510090030
nginx-unit9880602110090030
corredor da estrada27157.13,2110090030
road-runner-reboot111011001085106010090030
react-php23135.62,8610090030
react-php-reboot28.24191110090030

Monitoramento


mediana da CPU (%)CPU max (%)mediana da memória (MB)memória 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
nginx-unit78,1188,771.010,151.062,01
corredor da estrada42,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

Gráficos



Gráfico 3.1 Tempo médio de resposta por segundo



Gráfico 3.2 Tempo médio de resposta por segundo (sem php-fpm, php-ppm, road-runner-reboot)



Gráfico 3.3 Carga média do processador por segundo



Gráfico 3.4 Consumo médio de memória por segundo


10000 rps


Configuração do tanque fantasma Yandex


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

Links detalhados do relatório



Percentis de tempo de resposta


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (contagem)
php-fpm110501105011050188070.466317107
php-fpm-80326031401360114599.619448301
php-ppm2755273026952605100450015
nginx-unit102010101000980100450015
corredor da estrada640630615580100450015
road-runner-reboot1130112011101085100450015
react-php18901090104558.99.9964.49996
react-php-reboot3480307012559199,72448753

Monitoramento


mediana da CPU (%)CPU max (%)mediana da memória (MB)memória máxima (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
corredor da estrada73,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


Gráfico 4.1 Tempo médio de resposta por segundo



Gráfico 4.2 Tempo médio de resposta por segundo (sem php-fpm, php-ppm)



Gráfico 4.3 Carga média do processador por segundo



Gráfico 4.4 Consumo médio de memória por segundo


Sumário


Aqui estão gráficos que mostram a alteração nas características dos serviços, dependendo da carga. Ao visualizar gráficos, vale considerar que nem todos os serviços responderam 100% das solicitações.



Gráfico 5.1 Percentil 95% do tempo de resposta



Gráfico 5.2 Percentil 95% do tempo de resposta (sem php-fpm)



Gráfico 5.3 Carga máxima da CPU



Gráfico 5.4 Consumo máximo de memória


A solução ideal (sem alterar o código), na minha opinião, é o gerente de processos da Unidade Nginx. Apresenta bons resultados em velocidade de resposta e conta com o apoio da empresa.


De qualquer forma, a abordagem e as ferramentas de desenvolvimento precisam ser selecionadas individualmente, dependendo das cargas de trabalho, recursos do servidor e recursos do desenvolvedor.


UPD
Para testes 1000/10000 rps, serviço php-fpm-80 adicionado
A configuração php-fpm foi usada para isso:


 pm = dynamic pm.max_children = 80 

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


All Articles