比较PHP FPM,PHP PPM,Nginx单元,React PHP和RoadRunner



使用Yandex Tank执行测试。
Symfony 4和PHP 7.2被用作应用程序。
目的是比较不同负载下的服务特征并找到最佳选择。
为了方便起见,所有内容都收集在Docker容器中,并使用docker-compose进行募集。
猫下有很多桌子和图表。

源代码在这里
本文中描述的所有命令示例都应从项目目录中执行。


应用程式


该应用程序在Symfony 4和PHP 7.2上运行。


仅回答一条路线并返回:


  • 随机数
  • 环境
  • 过程的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都会启动三个主要容器:


  • Nginx-反向代理服务器;
  • 应用-编写的具有所有依赖项的应用代码;
  • PHP FPM \ Nginx单元\ Road Runner \ React PHP-应用程序服务器。

请求处理仅限于两个应用程序实例(根据处理器内核的数量)。


服务项目


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团队的Application Server。 用C写。


优点:


  • 您可以使用HTTP API更改配置。
  • 您可以使用不同的配置和语言版本同时运行一个应用程序的多个实例;
  • 无需跟踪内存;
  • 无需更改应用程序中的任何内容。

缺点:


  • PHP必须为每个请求初始化变量。

要从nginx-unit配置文件传递环境变量,您需要修复php.ini:


 ; Nginx Unit variables_order=E 

使用docker-compose启动应用程序的命令:


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

反应PHP


用于事件编程的库。 它是用PHP编写的。


优点:


  • 使用该库,您可以编写一个服务器,该服务器仅初始化一次变量,然后继续使用它们。

缺点:


  • 您必须为服务器编写代码;
  • 需要跟踪内存。

如果为工作进程使用--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核
内存 :4096 MB
固态硬盘 :50 GB
连线 :100MBit
作业系统 :CentOS 7(64x)


经过测试的服务:


  • php-fpm
  • php-ppm
  • nginx单位
  • 道路行人
  • 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的每种服务和配置,仅进行了一项测试。 因此,数字可能不正确。 评估服务相对于彼此的特征非常重要。


100 rps


Phantom Yandex Tank配置


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

详细报告链接



响应时间百分比


95%(毫秒)90%(毫秒)80%(毫秒)50%(毫秒)HTTP OK(%)HTTP OK(计数)
php-fpm9.96.34.353.5910057030
php-ppm9.463.883.1610057030
nginx单位116.64.433.6910057030
道路行人8.15.13.532.9210057030
公路跑者重启128.65.33.8510057030
反应PHP8.54.913.292.7410057030
react-php-reboot138.55.53.9510057030

监控方式


cpu中位数(%)最高CPU(%)内存中位数(MB)最大记忆体(MB)
php-fpm9.1512.58880.32907.97
php-ppm7.0813.68901.72913.80
nginx单位9.5612.54923.02943.90
道路行人5.578.61992.711,001.46
公路跑者重启9.1812.67848.43870.26
反应PHP4.536.581,004.681,009.91
react-php-reboot9.6112.67885.92892.52

图表



图1.1每秒平均响应时间



图表1.2每秒平均处理器负载



图1.3每秒平均内存消耗


500 rps


Phantom Yandex Tank配置


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

详细报告链接



响应时间百分比


95%(毫秒)90%(毫秒)80%(毫秒)50%(毫秒)HTTP OK(%)HTTP OK(计数)
php-fpm138.45.33.69100285030
php-ppm1594.723.24100285030
nginx单位1285.53.93100285030
道路行人9.663.712.83100285030
公路跑者重启14117.14.45100285030
反应PHP9.35.83.572.68100285030
react-php-reboot15127.24.21100285030

监控方式


cpu中位数(%)最高CPU(%)内存中位数(MB)最大记忆体(MB)
php-fpm41.6848.331,006.061,015.09
php-ppm33.9048.901,046.321,055.00
nginx单位42.1347.921,006.671,015.73
道路行人08/2406/281,035.861,044.58
公路跑者重启46.2352.04939.63948.08
反应PHP19.5723.421,049.831,060.26
react-php-reboot41.3047.89957.01958.56

图表



图2.1每秒平均响应时间



图2.2每秒平均处理器负载



图2.3每秒平均内存消耗


1000每秒


Phantom Yandex Tank配置


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

详细报告链接



响应时间百分比


95%(毫秒)90%(毫秒)80%(毫秒)50%(毫秒)HTTP OK(%)HTTP OK(计数)
php-fpm1105011050904019580.6772627
php-fpm-8031501375116515299.8589895
php-ppm278527402685254510090030
nginx单位9880602110090030
道路行人27157.13.2110090030
公路跑者重启111011001085106010090030
反应PHP23135.62.8610090030
react-php-reboot2824191110090030

监控方式


cpu中位数(%)最高CPU(%)内存中位数(MB)最大记忆体(MB)
php-fpm12.6678.25990.161,006.56
php-fpm-8083.7891.28746.01937.24
php-ppm66.1691.201,088.741,102.92
nginx单位78.1188.771,010.151,062.01
路行者42.9354.231,010.891,068.48
公路跑者重启77.6485.66976.441,044.05
反应PHP36.3946.311,018.031,088.23
react-php-reboot72.1181.81911.28961.62

图表



图3.1每秒平均响应时间



图3.2每秒平均响应时间(无php-fpm,php-ppm,road-runner-reboot)



图3.3每秒平均处理器负载



图3.4每秒平均内存消耗


10000 rps


Phantom Yandex Tank配置


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

详细报告链接



响应时间百分比


95%(毫秒)90%(毫秒)80%(毫秒)50%(毫秒)HTTP OK(%)HTTP OK(计数)
php-fpm1105011050110501880年70.466317107
php-fpm-80326031401360114599.619448301
php-ppm2755273026952605100450015
nginx单位102010101000980100450015
道路行人640630615580100450015
公路跑者重启1130112011101085100450015
反应PHP1890年109010455899.9964,49996
react-php-reboot3480307012559199.72448753

监控方式


cpu中位数(%)最高CPU(%)内存中位数(MB)最大记忆体(MB)
php-fpm5.5779.35984.47998.78
php-fpm-8085.0592.19936.64943.93
php-ppm66.8682.411,089.311,097.41
nginx单位86.1493.941,067.711,069.52
道路行人73.4182.721,129.481,134.00
公路跑者重启80.3286.29982.69984.80
反应PHP73.7682.181,101.711,105.06
react-php-reboot85.7791.92975.85978.42


图4.1每秒平均响应时间



图表4.2每秒平均响应时间(不包括php-fpm,php-ppm)



图4.3每秒平均处理器负载



图4.4每秒平均内存消耗


总结


以下图表显示了根据负载而变化的服务特性。 在查看图表时,值得考虑的是,并非所有服务都可以100%答复请求。



图5.1响应时间的95%



图表5.2 95%的响应时间百分位数



图5.3最大CPU负载



图5.4最大内存消耗


我认为,最佳解决方案(无需更改代码)是Nginx Unit流程管理器。 它在响应速度方面显示出良好的结果,并得到了公司的支持。


无论如何,根据您的工作负载,服务器资源和开发人员的能力,需要分别选择开发方法和工具。


UPD
对于测试1000/10000 rps添加了php-fpm-80服务
使用了php-fpm配置:


 pm = dynamic pm.max_children = 80 

Source: https://habr.com/ru/post/zh-CN431818/


All Articles