Comparez PHP FPM, PHP PPM, Nginx Unit, React PHP et RoadRunner



Les tests ont été effectués à l'aide de Yandex Tank.
Symfony 4 et PHP 7.2 ont été utilisés comme application.
L'objectif était de comparer les caractéristiques des services à différentes charges et de trouver la meilleure option.
Pour plus de commodité, tout est collecté dans des conteneurs Docker et est élevé à l'aide de Docker-compose.
Sous un chat, il y a beaucoup de tableaux et de graphiques.

Le code source est ici .
Tous les exemples de commandes décrits dans l'article doivent être exécutés à partir du répertoire du projet.


App


L'application fonctionne sur Symfony 4 et PHP 7.2.


Répond à un seul itinéraire et retourne:


  • nombre aléatoire;
  • l'environnement
  • pid du processus;
  • nom du service avec lequel il travaille;
  • Variables php.ini.

Exemple de réponse:


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 configuré dans chaque conteneur:


  • OPcache est activé;
  • cache d'amorçage configuré à l'aide de composer;
  • Les paramètres php.ini sont conformes aux meilleures pratiques Symfony .

Les journaux sont écrits dans stderr:
/config/packages/prod/monolog.yaml


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

Le cache est écrit dans / 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; } } } ... 

Chaque docker-compose lance trois conteneurs principaux:


  • Nginx - serveur proxy inverse;
  • App - code d'application préparé avec toutes les dépendances;
  • PHP FPM \ Nginx Unit \ Road Runner \ React PHP - serveur d'applications.

Le traitement des demandes est limité à deux instances d'application (par le nombre de cœurs de processeur).


Les services


PHP FPM


Gestionnaire de processus PHP. Écrit en C.


Avantages:


  • pas besoin de garder une trace de la mémoire;
  • pas besoin de changer quoi que ce soit dans l'application.

Inconvénients:


  • PHP doit initialiser les variables pour chaque requête.

Commande pour lancer l'application avec docker-compose:


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

PHP PPM


Gestionnaire de processus PHP. Il est écrit en PHP.


Avantages:


  • initialise les variables une fois puis les utilise;
  • pas besoin de changer quoi que ce soit dans l'application (il existe des modules prêts à l'emploi pour Symfony / Laravel, Zend, CakePHP).

Inconvénients:


  • besoin de suivre la mémoire.

Commande pour lancer l'application avec docker-compose:


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

Unité Nginx


Serveur d'applications de l'équipe Nginx. Écrit en C.


Avantages:


  • Vous pouvez modifier la configuration à l'aide de l'API HTTP;
  • Vous pouvez exécuter plusieurs instances d'une même application simultanément avec différentes configurations et versions linguistiques;
  • pas besoin de garder une trace de la mémoire;
  • pas besoin de changer quoi que ce soit dans l'application.

Inconvénients:


  • PHP doit initialiser les variables pour chaque requête.

Pour transmettre les variables d'environnement à partir du fichier de configuration de l'unité nginx, vous devez corriger php.ini:


 ; Nginx Unit variables_order=E 

Commande pour lancer l'application avec docker-compose:


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

React PHP


Bibliothèque pour la programmation d'événements. Il est écrit en PHP.


Avantages:


  • en utilisant la bibliothèque, vous pouvez écrire un serveur qui initialisera les variables une seule fois et continuera à travailler avec elles.

Inconvénients:


  • vous devez écrire du code pour le serveur;
  • besoin de garder une trace de la mémoire.

Si vous utilisez l' indicateur --reboot-kernel-after-request pour le travailleur, Symfony Kernel sera réinitialisé pour chaque demande. Avec cette approche, vous n'avez pas besoin de surveiller la mémoire.


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

Commande pour lancer l'application avec docker-compose:


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

Coureur de route


Serveur Web et gestionnaire de processus PHP. Écrit à Golang.


Avantages:


  • vous pouvez écrire un travailleur qui initialisera les variables une seule fois et continuera à travailler avec elles.

Inconvénients:


  • vous devez écrire le code du travailleur;
  • besoin de garder une trace de la mémoire.

Si vous utilisez l' indicateur --reboot-kernel-after-request pour le travailleur, Symfony Kernel sera réinitialisé pour chaque demande. Avec cette approche, vous n'avez pas besoin de surveiller la mémoire.


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

Commande pour lancer l'application avec docker-compose:


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

Test


Les tests ont été effectués à l'aide de Yandex Tank.
L'application et Yandex Tank étaient sur différents serveurs virtuels.


Caractéristiques du serveur virtuel avec l'application:
Virtualisation : KVM
CPU : 2 cœurs
Mémoire RAM : 4096 Mo
SSD : 50 Go
Connexion : 100 Mo
Système d' exploitation : CentOS 7 (64x)


Services testés:


  • php-fpm
  • php-ppm
  • unité nginx
  • coureur
  • road-runner-reboot (avec le drapeau --reboot-kernel-after-request )
  • react-php
  • react-php-reboot (avec le drapeau --reboot-kernel-after-request )

Pour les tests 1000/10000 rps, ajout du service php-fpm-80
La configuration php-fpm a été utilisée pour cela:


 pm = dynamic pm.max_children = 80 

Yandex Tank détermine à l'avance le nombre de fois qu'il a besoin de tirer sur la cible et ne s'arrête que lorsque les cartouches sont épuisées. Selon la vitesse de réponse du service, le temps de test peut être plus long que celui spécifié dans la configuration de test. Pour cette raison, les graphiques de différents services peuvent avoir des longueurs différentes. Plus le service répond lentement, plus son calendrier est long.


Pour chaque service et configuration de Yandex Tank, un seul essai a été effectué. Pour cette raison, les chiffres peuvent être inexacts. Il était important d'évaluer les caractéristiques des services les uns par rapport aux autres.


100 rps


Configuration du réservoir Phantom Yandex


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

Liens vers les rapports détaillés



Centiles de temps de réponse


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (nombre)
php-fpm9,96.34,353,5910057030
php-ppm9.463,883.1610057030
unité nginx116,64.433,6910057030
coureur8.15.13,532,9210057030
road-runner-reboot128.65.33,8510057030
react-php8.54,913.292,7410057030
react-php-reboot138.55.53,9510057030

Suivi


CPU médiane (%)cpu max (%)médiane de mémoire (Mo)mémoire max (Mo)
php-fpm9.1512,58880,32907,97
php-ppm7.0813,68901,72913.80
unité nginx9,5612,54923.02943,90
coureur5,578.61992,711001,46
road-runner-reboot9.1812,67848,43870.26
react-php4,536,581 004,681 009,91
react-php-reboot9,6112,67885,92892,52

Graphiques



Graphique 1.1 Temps de réponse moyen par seconde



Graphique 1.2 Charge moyenne du processeur par seconde



Graphique 1.3 Consommation moyenne de mémoire par seconde


500 rps


Configuration du réservoir Phantom Yandex


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

Liens vers les rapports détaillés



Centiles de temps de réponse


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (nombre)
php-fpm138.45.33,69100285030
php-ppm1594.723.24100285030
unité nginx1285.53,93100285030
coureur9,663,712,83100285030
road-runner-reboot14117.14,45100285030
react-php9.35.83,572,68100285030
react-php-reboot15127.24.21100285030

Suivi


CPU médiane (%)cpu max (%)médiane de mémoire (Mo)mémoire max (Mo)
php-fpm41,6848,331 006,061 015,09
php-ppm33,9048,901.046,321.055,00
unité nginx42.1347,921 006,671 015,73
coureur24/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

Graphiques



Graphique 2.1 Temps de réponse moyen par seconde



Graphique 2.2 Charge moyenne du processeur par seconde



Graphique 2.3 Consommation moyenne de mémoire par seconde


1000 rps


Configuration du réservoir Phantom Yandex


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

Liens vers les rapports détaillés



Centiles de temps de réponse


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (nombre)
php-fpm1105011050904019580,6772627
php-fpm-8031501375116515299,8589895
php-ppm278527402685254510090030
unité nginx9880602110090030
coureur27157.13.2110090030
road-runner-reboot111011001085106010090030
react-php23135.62,8610090030
react-php-reboot2824191110090030

Suivi


CPU médiane (%)cpu max (%)médiane de mémoire (Mo)mémoire max (Mo)
php-fpm12,6678,25990.161 006,56
php-fpm-8083,7891,28746.01937,24
php-ppm66,1691,201.088,741102,92
unité nginx78.1188,771 010,151 062,01
coureur42,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

Graphiques



Graphique 3.1 Temps de réponse moyen par seconde



Graphique 3.2 Temps de réponse moyen par seconde (sans php-fpm, php-ppm, road-runner-reboot)



Graphique 3.3 Charge moyenne du processeur par seconde



Graphique 3.4 Consommation moyenne de mémoire par seconde


10000 rps


Configuration du réservoir Phantom Yandex


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

Liens vers les rapports détaillés



Centiles de temps de réponse


95% (ms)90% (ms)80% (ms)50% (ms)HTTP OK (%)HTTP OK (nombre)
php-fpm110501105011050188070,466317107
php-fpm-80326031401360114599,619448301
php-ppm2755273026952605100450015
unité nginx102010101000980100450015
coureur640630615580100450015
road-runner-reboot1130112011101085100450015
react-php1890109010455899,9964,49996
react-php-reboot3480307012559199,72448753

Suivi


CPU médiane (%)cpu max (%)médiane de mémoire (Mo)mémoire max (Mo)
php-fpm5,5779,35984,47998,78
php-fpm-8085.0592.19936,64943,93
php-ppm66,8682,411.089,311.097,41
unité nginx86.1493,941067,711.069,52
coureur73,4182,721129,481.134,00
road-runner-reboot80,3286,29982,69984.80
react-php73,7682.181101,711 105,06
react-php-reboot85,7791,92975,85978,42


Graphique 4.1 Temps de réponse moyen par seconde



Graphique 4.2 Temps de réponse moyen par seconde (sans php-fpm, php-ppm)



Graphique 4.3 Charge moyenne du processeur par seconde



Graphique 4.4 Consommation moyenne de mémoire par seconde


Résumé


Voici des graphiques montrant l'évolution des caractéristiques des services en fonction de la charge. Lors de la visualisation des graphiques, il convient de noter que tous les services n'ont pas répondu à 100% des demandes.



Graphique 5.1 95% de centile du temps de réponse



Graphique 5.2 Centile à 95% du temps de réponse (sans php-fpm)



Graphique 5.3 Charge CPU maximale



Graphique 5.4 Consommation maximale de mémoire


La solution optimale (sans changer le code), à ​​mon avis, est le gestionnaire de processus Nginx Unit. Il montre de bons résultats en termes de vitesse de réponse et bénéficie du soutien de l'entreprise.


Dans tous les cas, l'approche et les outils de développement doivent être sélectionnés individuellement, en fonction de vos charges de travail, des ressources du serveur et des capacités des développeurs.


UPD
Pour les tests 1000/10000 rps, ajout du service php-fpm-80
La configuration php-fpm a été utilisée pour cela:


 pm = dynamic pm.max_children = 80 

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


All Articles