Vergleichen Sie PHP FPM, PHP PPM, Nginx Unit, React PHP und RoadRunner



Die Tests wurden mit Yandex Tank durchgeführt.
Symfony 4 und PHP 7.2 wurden als Anwendung verwendet.
Ziel war es, die Eigenschaften von Diensten bei unterschiedlichen Belastungen zu vergleichen und die beste Option zu finden.
Der Einfachheit halber wird alles in Docker-Containern gesammelt und mit Docker-Compose angehoben.
Unter einer Katze gibt es viele Tabellen und Grafiken.

Der Quellcode ist hier .
Alle im Artikel beschriebenen Beispiele für Befehle sollten aus dem Projektverzeichnis ausgeführt werden.


App


Die Anwendung läuft unter Symfony 4 und PHP 7.2.


Beantwortet nur eine Route und kehrt zurück:


  • Zufallszahl;
  • Umwelt
  • pid des Prozesses;
  • Name des Dienstes, mit dem es arbeitet;
  • php.ini Variablen.

Antwortbeispiel:


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 ist in jedem Container konfiguriert:


  • OPcache ist aktiviert;
  • konfigurierter Bootstrap-Cache mit Composer;
  • Die Einstellungen für php.ini entsprechen den Best Practices von Symfony .

Protokolle werden in stderr geschrieben:
/config/packages/prod/monolog.yaml


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

Der Cache ist in / dev / shm geschrieben:
/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; } } } ... 

Jeder Docker-Compose startet drei Hauptcontainer:


  • Nginx - Reverse-Proxy-Server;
  • App-vorbereiteter Anwendungscode mit allen Abhängigkeiten;
  • PHP FPM \ Nginx Unit \ Road Runner \ React PHP - Anwendungsserver.

Die Anforderungsverarbeitung ist auf zwei Anwendungsinstanzen beschränkt (durch die Anzahl der Prozessorkerne).


Dienstleistungen


PHP FPM


PHP-Prozessmanager. Geschrieben in C.


Vorteile:


  • keine Notwendigkeit, den Speicher im Auge zu behalten;
  • Sie müssen nichts an der Anwendung ändern.

Nachteile:


  • PHP muss Variablen für jede Anforderung initialisieren.

Befehl zum Starten der Anwendung mit Docker-Compose:


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

PHP PPM


PHP-Prozessmanager. Es ist in PHP geschrieben.


Vorteile:


  • initialisiert die Variablen einmal und verwendet sie dann;
  • Sie müssen nichts an der Anwendung ändern (es gibt vorgefertigte Module für Symfony / Laravel, Zend, CakePHP).

Nachteile:


  • müssen der Erinnerung folgen.

Befehl zum Starten der Anwendung mit Docker-Compose:


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

Nginx-Einheit


Anwendungsserver vom Nginx-Team. Geschrieben in C.


Vorteile:


  • Sie können die Konfiguration mithilfe der HTTP-API ändern.
  • Sie können mehrere Instanzen einer Anwendung gleichzeitig mit unterschiedlichen Konfigurationen und Sprachversionen ausführen.
  • keine Notwendigkeit, den Speicher im Auge zu behalten;
  • Sie müssen nichts an der Anwendung ändern.

Nachteile:


  • PHP muss Variablen für jede Anforderung initialisieren.

Um Umgebungsvariablen aus der Konfigurationsdatei der nginx-Einheit zu übergeben, müssen Sie die Datei php.ini korrigieren:


 ; Nginx Unit variables_order=E 

Befehl zum Starten der Anwendung mit Docker-Compose:


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

Reagiere auf PHP


Bibliothek zur Veranstaltungsprogrammierung. Es ist in PHP geschrieben.


Vorteile:


  • Mithilfe der Bibliothek können Sie einen Server schreiben, der die Variablen nur einmal initialisiert und mit ihnen weiterarbeitet.

Nachteile:


  • Sie müssen Code für den Server schreiben.
  • müssen den Speicher verfolgen.

Wenn Sie das Flag --reboot-kernel-after-request für den Worker verwenden, wird der Symfony-Kernel für jede Anforderung neu initialisiert. Bei diesem Ansatz müssen Sie den Speicher nicht überwachen.


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

Befehl zum Starten der Anwendung mit Docker-Compose:


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

Straßenläufer


Webserver und PHP-Prozessmanager. Geschrieben in Golang.


Vorteile:


  • Sie können einen Worker schreiben, der die Variablen nur einmal initialisiert und mit ihnen weiterarbeitet.

Nachteile:


  • Sie müssen den Code für den Arbeiter schreiben.
  • müssen den Speicher verfolgen.

Wenn Sie das Flag --reboot-kernel-after-request für den Worker verwenden, wird der Symfony-Kernel für jede Anforderung neu initialisiert. Bei diesem Ansatz müssen Sie den Speicher nicht überwachen.


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

Befehl zum Starten der Anwendung mit Docker-Compose:


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

Testen


Die Tests wurden mit Yandex Tank durchgeführt.
Die Anwendung und Yandex Tank befanden sich auf verschiedenen virtuellen Servern.


Funktionen des virtuellen Servers mit der Anwendung:
Virtualisierung : KVM
CPU : 2 Kerne
RAM : 4096 MB
SSD : 50 GB
Verbindung : 100 MBit
Betriebssystem : CentOS 7 (64x)


Getestete Dienste:


  • PHP-Fpm
  • PHP-ppm
  • Nginx-Einheit
  • Straßenläufer
  • Road-Runner-Neustart (mit dem Flag --reboot-kernel-after-request )
  • reagieren-php
  • React -PHP-Reboot (mit dem Flag --reboot-kernel-after-request )

Für Tests mit 1000/10000 U / min wurde der Dienst php-fpm-80 hinzugefügt
Die php-fpm Konfiguration wurde dafür verwendet:


 pm = dynamic pm.max_children = 80 

Yandex Tank bestimmt im Voraus, wie oft auf das Ziel geschossen werden muss, und stoppt erst, wenn die Patronen aufgebraucht sind. Abhängig von der Antwortgeschwindigkeit des Dienstes kann die Testzeit länger sein als in der Testkonfiguration angegeben. Aus diesem Grund können Grafiken verschiedener Dienste unterschiedliche Längen haben. Je langsamer der Dienst reagiert, desto länger ist sein Zeitplan.


Für jeden Service und jede Konfiguration des Yandex-Tanks wurde nur ein Test durchgeführt. Aus diesem Grund können Zahlen ungenau sein. Es war wichtig, die Merkmale der Dienstleistungen im Verhältnis zueinander zu bewerten.


100 rps


Phantom Yandex Tank Konfiguration


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

Detaillierte Berichtslinks



Perzentile der Antwortzeit


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (Anzahl)
PHP-Fpm9.96.34.353.5910057030
PHP-ppm9.463,883.1610057030
Nginx-Einheit116.64.433.6910057030
Straßenläufer8.15.13.532.9210057030
Road-Runner-Neustart128.65.33,8510057030
reagieren-php8.54.913.292.7410057030
React-PHP-Neustart138.55.53,9510057030

Überwachung


CPU-Median (%)CPU max (%)Speichermedian (MB)Speicher max (MB)
PHP-Fpm9.1512.58880,32907,97
PHP-ppm7.0813.68901,72913,80
Nginx-Einheit9.5612.54923.02943,90
Straßenläufer5.578.61992,711.001,46
Road-Runner-Neustart9.1812.67848,43870,26
reagieren-php4.536.581.004,681.009,91
React-PHP-Neustart9.6112.67885,92892,52

Grafiken



Grafik 1.1 Durchschnittliche Antwortzeit pro Sekunde



Grafik 1.2 Durchschnittliche Prozessorlast pro Sekunde



Grafik 1.3 Durchschnittlicher Speicherverbrauch pro Sekunde


500 rps


Phantom Yandex Tank Konfiguration


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

Detaillierte Berichtslinks



Perzentile der Antwortzeit


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (Anzahl)
PHP-Fpm138.45.33.69100285030
PHP-ppm1594.723.24100285030
Nginx-Einheit1285.53.93100285030
Straßenläufer9.663.712.83100285030
Road-Runner-Neustart14117.14.45100285030
reagieren-php9.35.83.572.68100285030
React-PHP-Neustart15127.24.21100285030

Überwachung


CPU-Median (%)CPU max (%)Speichermedian (MB)Speicher max (MB)
PHP-Fpm41,6848,331.006,061.015,09
PHP-ppm33,9048,901.046,321.055,00
Nginx-Einheit42.1347,921.006,671.015,73
Straßenläufer24.0828.061.035,861.044,58
Road-Runner-Neustart46.2352.04939,63948.08
reagieren-php19.5723.421.049,831.060,26
React-PHP-Neustart41.3047,89957.01958,56

Grafiken



Grafik 2.1 Durchschnittliche Antwortzeit pro Sekunde



Grafik 2.2 Durchschnittliche Prozessorlast pro Sekunde



Grafik 2.3 Durchschnittlicher Speicherverbrauch pro Sekunde


1000 rps


Phantom Yandex Tank Konfiguration


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

Detaillierte Berichtslinks



Perzentile der Antwortzeit


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (Anzahl)
PHP-Fpm1105011050904019580,6772627
php-fpm-8031501375116515299,8589895
PHP-ppm278527402685254510090030
Nginx-Einheit9880602110090030
Straßenläufer27157.13.2110090030
Road-Runner-Neustart111011001085106010090030
reagieren-php23135.62.8610090030
React-PHP-Neustart2824191110090030

Überwachung


CPU-Median (%)CPU max (%)Speichermedian (MB)Speicher 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-Einheit78.1188,771.010,151.062,01
Straßenläufer42,9354,231.010,891.068,48
Road-Runner-Neustart77,6485,66976,441.044,05
reagieren-php36.3946.311.018,031.088,23
React-PHP-Neustart72.1181,81911.28961,62

Grafiken



Grafik 3.1 Durchschnittliche Antwortzeit pro Sekunde



Grafik 3.2 Durchschnittliche Antwortzeit pro Sekunde (ohne PHP-Fpm, PHP-ppm, Roadrunner-Neustart)



Grafik 3.3 Durchschnittliche Prozessorlast pro Sekunde



Grafik 3.4 Durchschnittlicher Speicherverbrauch pro Sekunde


10000 U / min


Phantom Yandex Tank Konfiguration


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

Detaillierte Berichtslinks



Perzentile der Antwortzeit


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (Anzahl)
PHP-Fpm110501105011050188070,466317107
php-fpm-80326031401360114599,619448301
PHP-ppm2755273026952605100450015
Nginx-Einheit102010101000980100450015
Straßenläufer640630615580100450015
Road-Runner-Neustart1130112011101085100450015
reagieren-php1890109010455899,9964,49996
React-PHP-Neustart3480307012559199,72448753

Überwachung


CPU-Median (%)CPU max (%)Speichermedian (MB)Speicher max (MB)
PHP-Fpm5.5779,35984,47998,78
php-fpm-8085.0592,19936,64943,93
PHP-ppm66,8682,411.089,311.097,41
Nginx-Einheit86,1493,941.067,711.069,52
Straßenläufer73,4182,721.129,481.134,00
Road-Runner-Neustart80,3286,29982,69984,80
reagieren-php73,7682.181.101,711.105,06
React-PHP-Neustart85,7791,92975,85978,42


Grafik 4.1 Durchschnittliche Reaktionszeit pro Sekunde



Grafik 4.2 Durchschnittliche Reaktionszeit pro Sekunde (ohne php-fpm, php-ppm)



Grafik 4.3 Durchschnittliche Prozessorlast pro Sekunde



Grafik 4.4 Durchschnittlicher Speicherverbrauch pro Sekunde


Zusammenfassung


Hier sind Diagramme, die die Änderung der Eigenschaften von Diensten in Abhängigkeit von der Last zeigen. Beim Anzeigen von Diagrammen ist zu berücksichtigen, dass nicht alle Dienste 100% der Anfragen beantwortet haben.



Grafik 5.1 95% -Perzentil der Antwortzeit



Grafik 5.2 95% Perzentil der Antwortzeit (ohne PHP-Fpm)



Grafik 5.3 Maximale CPU-Auslastung



Tabelle 5.4 Maximaler Speicherverbrauch


Die optimale Lösung (ohne den Code zu ändern) ist meiner Meinung nach der Prozessmanager der Nginx-Einheit. Es zeigt gute Ergebnisse in der Reaktionsgeschwindigkeit und wird vom Unternehmen unterstützt.


In jedem Fall müssen der Entwicklungsansatz und die Tools individuell ausgewählt werden, abhängig von Ihren Workloads, Serverressourcen und Entwicklerfähigkeiten.


UPD
Für Tests mit 1000/10000 U / min wurde der Dienst php-fpm-80 hinzugefügt
Die php-fpm Konfiguration wurde dafür verwendet:


 pm = dynamic pm.max_children = 80 

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


All Articles