Développement d'applications hybrides PHP / Go à l'aide de RoadRunner

L'application PHP classique est un thread unique, un chargement lourd (sauf si vous écrivez bien sûr sur des microframes) et la mort inévitable du processus après chaque requête ... Une telle application est lourde et lente, mais nous pouvons lui donner une seconde vie par hybridation. Pour accélérer - nous diabolisons et optimisons les fuites de mémoire pour obtenir de meilleures performances - nous présenterons notre propre serveur d'applications PHP Golang RoadRunner pour ajouter de la flexibilité - simplifier le code PHP, étendre la pile et partager la responsabilité entre le serveur et l'application. Essentiellement, nous ferons fonctionner notre application comme si nous l'écrivions en Java ou dans un autre langage.

Grâce à l'hybridation, une application auparavant lente a cessé de souffrir de 502 erreurs sous charge, le temps de réponse moyen aux demandes a diminué, la productivité a augmenté et le déploiement et l'assemblage sont devenus plus faciles en raison de l'unification de l'application et de l'élimination des liaisons inutiles sous la forme nginx + php-fpm.


Anton Titov ( Lachezis ) est CTO et co-fondateur de SpiralScout LLC avec 12 ans d'expérience active en développement commercial en PHP. Au cours des dernières années, il a activement mis en œuvre Golang sur la pile de développement de l'entreprise. Anton a parlé d'un exemple à PHP Russie 2019 .

Cycle de vie des applications PHP


Schématiquement, un dispositif d'application abstrait avec un certain cadre ressemble à ceci.



Lorsque nous envoyons une demande Ă  un processus, cela se produit:

  • initialisation du projet;
  • chargement de bibliothèques, de cadres et d'ORM partagĂ©s;
  • chargement des bibliothèques requises pour un projet spĂ©cifique;
  • routage;
  • acheminer la demande vers un contrĂ´leur spĂ©cifique;
  • gĂ©nĂ©ration de rĂ©ponse.

C'est le principe de fonctionnement d'une application monothread classique avec un seul point d'entrée, qui après chaque exécution est complètement détruite ou efface son état. Tout le code est déchargé de la mémoire, le travailleur est effacé ou réinitialise simplement son état.

Chargement paresseux


La manière standard et simple d'accélérer est l'implémentation du système de chargement différé ou des bibliothèques de chargement à la demande.



Avec Lazy-loading, nous demandons uniquement le code nécessaire.

Lors de l'accès à un contrôleur spécifique, seules les bibliothèques nécessaires seront chargées en mémoire, traitées, puis déchargées. Cela vous permet de réduire le temps de réponse moyen du projet et de faciliter considérablement le processus de travail sur le serveur. Dans tous les frameworks que nous utilisons actuellement, le principe du chargement paresseux est implémenté.

Cachez les calculs fréquents


La méthode est plus compliquée et activement utilisée, par exemple, dans le framework Symfony, les moteurs de modèle, les schémas ORM et le routage. Ce n'est pas la mise en cache comme memcached ou Redis pour les données utilisateur. Ce système réchauffe à l'avance certaines parties du code . A la première demande, le système génère un code ou un fichier cache, et aux demandes suivantes, ces calculs, nécessaires par exemple pour compiler un modèle, ne seront plus effectués.



La mise en cache accélère considérablement l'application , mais en même temps la complique . Par exemple, il y a des problèmes pour invalider le cache et mettre à jour l'application. Ne confondez pas le cache utilisateur avec le cache d'application - dans l'un, les données changent au fil du temps, dans l'autre uniquement lorsque le code est mis à jour.

Traitement des demandes


Lorsqu'une demande est reçue d'un serveur PHP-FPM externe, le point d'entrée de la demande et l'initialisation correspondent.

Il s’avère que la demande du client est l’état de notre processus.

La seule façon de changer cet état est de détruire complètement le travailleur et de recommencer avec une nouvelle demande.



Il s'agit d'un modèle classique à filetage unique avec ses avantages.

  • Tous les travailleurs Ă  la fin de la demande meurent.
  • Les fuites de mĂ©moire, la condition de concurrence, les blocages ne sont pas inhĂ©rents Ă  PHP. Vous ne pouvez pas vous en soucier.
  • Le code est simple: nous Ă©crivons, traitons la demande, mourons et passons.

En revanche, pour chaque requête, nous chargeons complètement le framework, toutes les librairies, effectuons quelques calculs, recompilons les templates. Avec chaque demande dans un cercle, nous produisons beaucoup de manipulations et de travail inutile.

Comment ça marche sur le serveur


Très probablement, un tas de nginx et PHP fonctionnera. Nginx fonctionnera comme un proxy inverse: fournissez aux utilisateurs une partie de la statique et déléguez une partie des requêtes au gestionnaire de processus PHP PHP-FPM ci-dessous. Le gestionnaire élève déjà un travailleur distinct pour la demande et la traite. Après cela, le travailleur est détruit ou autorisé. Ensuite, un nouveau travailleur est créé pour la prochaine demande.



Un tel modèle fonctionne de manière stable - l'application est presque impossible à tuer. Mais sous de lourdes charges, la quantité de travail pour l'initialisation et la destruction des travailleurs affecte les performances du système, car même pour une simple demande GET, nous devons souvent tirer un tas de dépendances et relancer la connexion à la base de données.

Accélérer l'application


Comment accélérer l'application classique après l'introduction du cache et du chargement différé? Quelles sont les autres options?

Tournez-vous vers la langue elle-mĂŞme .

  • Utilisez OPCache. Je pense que personne n'exĂ©cute PHP en production sans OPCache activĂ©?
  • Attendez RFC: prĂ©chargement . Il vous permet de prĂ©charger un ensemble de fichiers dans une machine virtuelle.
  • JIT - accĂ©lère sĂ©rieusement l'application sur les tâches liĂ©es au processeur. Malheureusement, avec les tâches liĂ©es aux bases de donnĂ©es, cela n'aidera pas beaucoup.

Utilisez des alternatives . Par exemple, la machine virtuelle HHVM de Facebook. Il exécute du code dans un environnement plus optimisé. Malheureusement, HHVM n'est pas entièrement compatible avec la syntaxe PHP. Comme alternative, les compilateurs kPHP de VK ou PeachPie, qui convertit complètement le code en .NET C #, sont une alternative.

Réécrire entièrement dans une autre langue. C'est une option radicale - débarrassez-vous complètement du chargement de code entre les requêtes.

Vous pouvez stocker complètement l'état de l'application dans la mémoire , utiliser activement cette mémoire pour le travail, oublier le concept d'un travailleur mourant et effacer complètement l'application entre les demandes.

Pour y parvenir, nous déplaçons le point d'entrée, qui était auparavant avec le point d'initialisation, profondément dans l'application.

Transfert du point d'entrée - diabolisation


Cela crée une boucle infinie dans l'application: demande entrante - exécutez-la dans le cadre - générez une réponse à l'utilisateur. Il s'agit d'une économie sérieuse - tous les bootstrap, toutes les initialisations du framework ne sont effectuées qu'une seule fois, puis plusieurs requêtes sont traitées par l'application.



Nous adaptons l'application


Fait intéressant, nous pouvons nous concentrer sur l'optimisation uniquement de la partie de l'application qui s'exécutera au moment de l'exécution : contrôleurs, logique métier. Dans ce cas, vous pouvez abandonner le modèle de chargement différé. Nous prendrons une partie du projet d'amorçage au début - au moment de l'initialisation. Calculs préliminaires: routage, modèles, paramètres, schémas ORM gonfleront l'initialisation, mais à l'avenir, ils gagneront du temps de traitement pour une demande spécifique.



Je ne recommande pas de compiler des modèles lors du téléchargement d'un travailleur, mais le téléchargement, par exemple, de toutes les configurations est utile.

Comparez les modèles


Comparez les modèles diabolisés (à gauche) et classiques.



Le modèle diabolisé prend plus de temps à partir du moment où le processus a été créé jusqu'au moment où la réponse est retournée à l'utilisateur. L'application classique est optimisée pour une création, un traitement et une destruction rapides.

Cependant, toutes les demandes ultérieures après avoir réchauffé le code sont beaucoup plus rapides. Le framework, l'application, le conteneur est déjà en mémoire et prêt à accepter les requêtes et à répondre rapidement.

Problèmes du modèle à longue durée de vie


Malgré les avantages, le modèle présente un ensemble de limitations.

Fuites de mémoire. L'application se trouve dans la mémoire pendant très longtemps, et si vous utilisez les "courbes" de la bibliothèque, les mauvaises dépendances ou les états globaux - la mémoire commencera à fuir. À un moment donné, une erreur fatale apparaîtra qui interrompra la demande de l'utilisateur.

Le problème est résolu de deux manières.

  • Écrivez du code prĂ©cis, utilisez des bibliothèques Ă©prouvĂ©es.
  • Surveiller activement les travailleurs. Si vous pensez que la mĂ©moire fuit Ă  l'intĂ©rieur du processus, remplacez-la de manière proactive par un analogue avec une limite infĂ©rieure, c'est-Ă -dire simplement par une nouvelle copie qui n'a pas encore rĂ©ussi Ă  accumuler de la mĂ©moire non nettoyĂ©e.

Fuites de données . Par exemple, si lors d'une demande entrante, nous enregistrons l'utilisateur actuel du système dans une variable globale et oublions de réinitialiser cette variable après la demande, il y a une chance que le prochain utilisateur du système accède accidentellement à des données qu'il ne devrait pas voir.

Le problème est résolu au niveau de l'architecture de l'application.

  • Ne stockez pas un utilisateur actif dans un contexte global. Toutes les donnĂ©es spĂ©cifiques au contexte de la demande sont supprimĂ©es et effacĂ©es avant la prochaine demande.
  • Manipulez les donnĂ©es de session avec soin. Sessions en PHP - avec l'approche classique, c'est un objet global. Enveloppez-le correctement afin qu'il soit rĂ©initialisĂ© Ă  la demande suivante.

Gestion des ressources .

  • Surveillez les connexions Ă  la base de donnĂ©es. Si l'application se bloque en mĂ©moire pendant un mois ou deux, la connexion ouverte se fermera très probablement dans ce dĂ©lai: la base de donnĂ©es sera rĂ©installĂ©e, redĂ©marrĂ©e ou le pare-feu rĂ©initialisera la connexion. Au niveau du code, pensez Ă  vous reconnecter ou, après chaque demande, rĂ©initialisez la connexion et relancez-la Ă  la demande suivante.
  • Évitez le verrouillage de fichiers de longue durĂ©e. Si votre collaborateur Ă©crit des informations dans un fichier, il n'y a aucun problème. Mais si ce fichier est ouvert et comporte un verrou, aucun autre processus de votre système n'y aura accès tant que le verrou ne sera pas libĂ©rĂ©.


Explorez le modèle à longue durée de vie


Envisagez un modèle de travail à long terme - diaboliser une application - et explorez les moyens de le mettre en œuvre.

Approche non bloquante


Nous utilisons PHP asynchrone - nous chargeons l'application une fois en mémoire et traitons les requêtes HTTP entrantes à l'intérieur de l'application. Maintenant, l' application et le serveur sont un processus . Lorsque la demande arrive, nous créons une coroutine distincte ou dans la boucle d'événements, nous donnons une promesse, la traitons et la donnons à l'utilisateur.



L'avantage indéniable de l'approche est une performance maximale. Il est également possible d'utiliser des outils intéressants, par exemple, configurer WebSocket directement sur votre application .

Cependant, l'approche augmente considérablement la complexité du développement . Il est nécessaire d'installer ELDO, n'oubliez pas que tous les pilotes de base de données ne seront pas pris en charge et que la bibliothèque PDO est exclue.

Pour résoudre les problèmes en cas de diabolisation avec une approche non bloquante, vous pouvez utiliser des outils bien connus: ReactPHP , amphp et Swoole - un développement intéressant sous la forme d'une extension C. Ces outils fonctionnent rapidement, ils ont une bonne communauté et une bonne documentation.

Approche de blocage


Nous n'élevons pas de coroutines à l'intérieur de l'application, mais le faisons de l'extérieur.



Nous prenons juste quelques processus d'application , comme le ferait PHP-FPM. Au lieu de transmettre ces requêtes sous forme d'état de processus, nous les délivrons de l'extérieur sous forme de protocole ou de messagerie.

Nous écrivons le même code monothread que nous connaissons, nous utilisons toutes les mêmes bibliothèques et le même PDO. Tout le travail acharné de travailler avec des sockets, HTTP et d'autres outils se fait en dehors de l'application PHP .

Parmi les inconvénients: nous devons surveiller la mémoire et nous rappeler que la communication entre deux processus différents n'est pas gratuite , mais nous devons transférer des données. Cela créera une légère surcharge.

Pour résoudre le problème, il existe déjà un outil PHP-RM écrit en PHP. Sur la bibliothèque ReactPHP, il est intégré à plusieurs frameworks . Cependant, PHP-PM est très lent, il fuit de la mémoire au niveau du serveur et sous charge, il ne montre pas autant de croissance que PHP-FRM.

Nous écrivons notre serveur d'applications


Nous avons écrit notre serveur d'applications , qui est similaire à PHP-RM, mais il y a plus de fonctionnalités. Que voulions-nous du serveur?

Combinez avec les cadres existants. Nous souhaitons une intégration flexible avec presque tous les frameworks du marché. Je n'ai pas envie d'écrire un outil qui ne fonctionne que dans un cas particulier.

Différents processus pour le serveur et l'application . Possibilité d'un redémarrage à chaud, de sorte que lors du développement local, appuyez sur F5 et voyez le nouveau code mis à jour, ainsi que de pouvoir les développer individuellement.

Grande vitesse et stabilité . Pourtant, nous écrivons un serveur HTTP.

Extensibilité facile . Nous voulons utiliser le serveur non seulement comme serveur HTTP, mais également pour des scénarios individuels comme un serveur de file d'attente ou un serveur gRPC.

Travaillez dès que possible: Windows, Linux, ARM CPU.

Possibilité d'écrire des extensions multi-threads très rapides spécifiques à notre application.

Comme vous l'avez déjà compris, nous écrirons à Golang.

Serveur RoadRunner


Pour créer un serveur PHP, vous devez résoudre 4 problèmes principaux:

  • Établissez la communication entre Golang et les processus PHP.
  • Gestion des processus: crĂ©ation, destruction, suivi des travailleurs.
  • Équilibrer les tâches - distribution efficace des tâches aux travailleurs. Étant donnĂ© que nous mettons en Ĺ“uvre un système qui bloque un travailleur individuel pour une tâche entrante spĂ©cifique particulière, il est important de crĂ©er un système qui dirait rapidement que le processus a terminĂ© le travail et est prĂŞt Ă  accepter la tâche suivante.
  • Pile HTTP - envoi des donnĂ©es de requĂŞte HTTP au travailleur. C'est une tâche simple d'Ă©crire un point entrant auquel l'utilisateur envoie une demande, qui est transmise Ă  PHP et renvoyĂ©e.

Variantes d'interaction entre les processus


Tout d'abord, résolvons le problème de communication entre Golang et les processus PHP. Nous avons plusieurs façons.

Intégration: incorporer un interpréteur PHP directement dans Golang. Ceci est possible, mais nécessite un assemblage PHP personnalisé, une configuration complexe et un processus commun pour le serveur et PHP. Comme dans go-php , par exemple, où l'interpréteur PHP est intégré à Golang.

Mémoire partagée - Utilisation de l'espace de mémoire partagée, où les processus partagent cet espace . Cela demande un travail minutieux. Lors de l'échange de données, vous devrez synchroniser l'état manuellement et le nombre d'erreurs qui peuvent se produire est assez important. La mémoire partagée dépend également du système d'exploitation.

Rédaction de votre protocole de transport - Goridge


Nous avons suivi un chemin simple qui est utilisé dans presque toutes les solutions sur les systèmes Linux - nous avons utilisé le protocole de transport. Il est écrit au-dessus des PIPES et des PRISES UNIX / TCP standard .

Il a la capacité de transférer des données dans les deux sens, de détecter les erreurs, de baliser les demandes et de mettre des en-têtes dessus. Une nuance importante pour nous est la possibilité de mettre en œuvre le protocole sans dépendances à la fois du côté de PHP et de Golang - sans extensions C dans un langage pur.

Comme pour tout protocole, la base est un paquet de données. Dans notre cas, le paquet a un en-tête fixe de 17 octets.



Le premier octet est alloué pour déterminer le type de paquet. Il peut s'agir d'un flux ou d'un indicateur qui indique le type de sérialisation des données. Ensuite, nous emballons deux fois la taille des données dans Little Endian et Big Endian. Nous utilisons cet héritage pour détecter les erreurs de transmission. Si nous constatons que la taille des données compressées dans deux commandes différentes ne correspond pas, une erreur de transfert de données s'est probablement produite. Ensuite, les données sont transmises.



Dans la troisième version du package, nous nous débarrasserons d'un tel héritage, introduirons une approche plus classique avec une somme de contrôle et ajouterons également la possibilité d'utiliser ce protocole avec des processus PHP asynchrones.

Pour implémenter le protocole dans Golang et PHP, nous avons utilisé des outils standard.

Sur Golang: encodage / bibliothèques binaires et io et bibliothèques net pour travailler avec des canaux standard et des sockets UNIX / TCP.

En PHP: la fonction familière pour travailler avec les paquets de données binaires / décompresser et les flux d'extensions et les sockets pour les pipes et sockets.

Un effet secondaire intéressant est apparu lors de la mise en œuvre. Nous l'avons intégré à la bibliothèque standard Golang net / rpc, ce qui nous permet d'appeler le code de service depuis Golang directement dans l'application.

Nous écrivons un service:

//  sample type  struct{} // Hi returns greeting message. func (a *App) Hi(name string, r *string) error { *r = fmt.Sprintf("ll, %s!", name) return nil } 

Avec une petite quantité de code, nous l'appelons depuis l'application:

 <?php use Spiral\Goridge; require "vendor/autoload.php"; $rpc = new Goridge\RPC( new Goridge\SocketRelay("127.0.0.1", 6001) ); echo $rpc->call("App.Hi", "Antony"); 

Gestionnaire de processus PHP


La prochaine partie du serveur est la gestion des travailleurs PHP.


Worker est un processus PHP que nous observons constamment depuis Golang. Nous collectons le journal de ses erreurs dans le fichier STDERR, communiquons avec le travailleur via le protocole de transport Goridge et collectons des statistiques sur la consommation de mémoire, l'exécution des tâches et le blocage.

L'implémentation est simple - c'est la fonctionnalité standard de os / exec, runtime, sync, atomic. Pour créer des travailleurs, nous utilisons Worker Factory .


Pourquoi Worker Factory? Parce que nous voulons communiquer à la fois sur des tuyaux standard et sur des prises. Dans ce cas, le processus d'initialisation est légèrement différent. Lors de la création d'un travailleur qui communique par pipe, nous pouvons le créer immédiatement et envoyer des données directement. Dans le cas des sockets, vous devez créer un travailleur, attendre qu'il atteigne le système, effectuer une négociation PID, puis continuer à travailler.

Équilibreur de tâches


La troisième partie du serveur est la plus importante pour les performances.

Pour la mise en œuvre, nous utilisons la fonctionnalité standard de Golang - un canal tamponné . En particulier, nous créons plusieurs travailleurs et les mettons dans ce canal en tant que pile LIFO.

À la réception des tâches de l'utilisateur, nous envoyons une demande à la pile LIFO et demandons l'émission du premier travailleur gratuit. Si le travailleur ne peut pas être alloué pendant un certain temps, alors l'utilisateur reçoit une erreur du type "Timeout Error". Si le travailleur est alloué - il obtient de la pile, est bloqué, après quoi il reçoit la tâche de l'utilisateur.

Une fois la tâche traitée, la réponse est renvoyée à l'utilisateur et le travailleur se trouve à la fin de la pile. Il est prêt à effectuer à nouveau la tâche suivante.

Si une erreur se produit, l'utilisateur recevra une erreur, car le travailleur sera détruit. Nous demandons à Worker Pool et Worker Factory de créer un processus identique et de le remplacer sur la pile. Cela permet au système de fonctionner même en cas d'erreurs fatales en recréant simplement les travailleurs par analogie avec PHP-FPM.


En conséquence, il s'est avéré implémenter un petit système qui fonctionne très rapidement - 200 ns pour l'allocation des travailleurs . Il est capable de fonctionner même en cas d'erreurs fatales. Chaque travailleur à un moment donné ne traite qu'une seule tâche, ce qui nous permet d'utiliser l' approche de blocage classique .

Surveillance proactive


Une partie distincte du gestionnaire de processus et de l'équilibreur de tâches est le système de surveillance proactif.


Il s'agit d'un système qui, une fois par seconde, interroge les travailleurs et surveille les indicateurs: il examine la quantité de mémoire qu'ils consomment, la quantité dans laquelle ils se trouvent, s'ils sont inactifs. En plus du suivi, le système surveille les fuites de mémoire. Si le travailleur dépasse une certaine limite, nous le verrons et le retirerons soigneusement du système avant qu'une fuite fatale ne se produise.

Pile HTTP


La dernière et simple partie.

Comment est-il mis en œuvre:

  • soulève un point HTTP du cĂ´tĂ© de Golang;
  • nous recevons une demande;
  • convertir au format PSR-7;
  • envoyer la demande au premier travailleur libre;
  • DĂ©compressez la demande dans un objet PSR-7;
  • nous traitons;
  • nous gĂ©nĂ©rons la rĂ©ponse.

Pour l'implémentation, nous avons utilisé la bibliothèque Golang NET / HTTP standard. Il s'agit d'une bibliothèque célèbre avec de nombreuses extensions. Capable de travailler à la fois sur HTTPS et sur le protocole HTTP / 2.

Côté PHP, nous avons utilisé la norme PSR-7 . Il s'agit d'un framework indépendant avec de nombreuses extensions et middlewares. Le PSR-7 est immuable dans sa conception , ce qui correspond bien au concept des applications à longue durée de vie et évite les erreurs de requête globales.

Les deux structures dans Golang et PSR-7 sont similaires, ce qui a considérablement gagné du temps pour mapper une demande d'une langue à une autre.

Pour démarrer le serveur nécessite une liaison minimale :

 http: address: 0.0.0.0:8080 workers: command: "php psr-worker.php" pool: numWorkers: 4 

De plus, à partir de la version 1.3.0, la dernière partie de la configuration peut être omise.

Téléchargez le fichier binaire du serveur, placez-le dans le conteneur Docker ou dans le dossier du projet. Alternativement, globalement, nous écrivons un petit fichier de configuration qui décrit quel pod nous allons écouter, quel travailleur est le point d'entrée et combien sont nécessaires.

Côté PHP, nous écrivons une boucle primaire qui reçoit une requête PSR-7, la traite et renvoie une réponse ou une erreur au serveur.

 while ($req = $psr7->acceptRequest()) { try { $resp = new \Zend\Diactoros\Response(); $resp->getBody()->write("hello world"); $psr7->respond($resp); } catch (\Throwable $e) { $psr7->getWorker()->error((string)$e); } } 

Assemblage Pour implémenter le serveur, nous avons choisi une architecture avec une approche composante. Cela permet d'assembler le serveur pour les besoins du projet, en ajoutant ou en supprimant des pièces individuelles en fonction des exigences de l'application.

 func main() { rr.Container.Register(env.ID, &env.Service{}) rr.Container.Register(rpc.ID, &rpc.Service{}) rr.Container.Register(http.ID, &http.Service{}) rr.Container.Register(static.ID, &static.Service{}) rr.Container.Register(limit.ID, &limit.Service{} // you can register additional commands using cmd.CLI rr.Execute() } 

Cas d'utilisation


Considérez les options d'utilisation du serveur et de modification de la structure. Pour commencer, considérez le pipeline classique - le travail du serveur avec les requêtes.

Modularité


Le serveur reçoit la demande vers un point HTTP et la transmet à travers un ensemble de middleware, qui sont écrits en Golang. Une demande entrante est convertie en une tâche que le travailleur comprend. Le serveur confie la tâche au travailleur et la renvoie.



Dans le même temps, le travailleur, utilisant le protocole Goridge, communique avec le serveur, surveille son état et lui transfère des données.

Middleware sur Golang: Autorisation


C'est la première chose à faire. Dans notre application, nous avons écrit Middleware pour autoriser l'utilisateur par jeton JWT . Le middleware est écrit de la même manière pour tout autre type d'autorisation. Une implémentation très banale et simple consiste à écrire Rate-Limiter ou Circuit-Breaker.



L'autorisation est rapide . Si la demande n'est pas valide - il suffit de ne pas l'envoyer à l'application PHP et de ne pas gaspiller de ressources pour traiter des tâches inutiles.

Suivi


Le deuxième cas d'utilisation. Nous pouvons intégrer le système de surveillance directement dans Golang Middleware. Par exemple, Prométhée, pour collecter des statistiques sur la vitesse des points de réponse, le nombre d'erreurs.



Vous pouvez également combiner la surveillance avec des mesures spécifiques à l'application (disponibles en standard avec 1.4.5). Par exemple, nous pouvons envoyer le nombre de demandes à la base de données ou le nombre de demandes spécifiques traitées au serveur Golang, puis à Prométhée.

Suivi et journalisation distribués


Nous écrivons Middleware avec un gestionnaire de processus. En particulier, nous pouvons nous connecter au système en temps réel pour surveiller les journaux et collecter tous les journaux dans une base de données centrale , ce qui est utile lors de l'écriture d'applications distribuées.



Nous pouvons également étiqueter les demandes , leur donner un ID spécifique et transmettre cet ID à tous les services en aval ou systèmes de communication entre eux. Par conséquent, nous pouvons créer une trace distribuée et voir comment vont les journaux d'application.

Enregistrez l'historique de vos requĂŞtes


Il s'agit d'un petit module qui enregistre toutes les demandes entrantes et les stocke dans une base de données externe. Le module vous permet de rejouer les requêtes dans le projet et d'implémenter un système de test automatique, un système de test de charge, ou simplement de vérifier l'API.



Comment avons-nous implémenté le module?

Nous traitons une partie des demandes de Golang . Nous écrivons Middleware à Golang et nous pouvons envoyer une partie des demandes au gestionnaire, qui est également écrite en Golang. Si un point de l'application est inquiétant en termes de performances, nous le réécrivons sur Golang et faisons glisser la pile d'une langue à l'autre.



Nous écrivons un serveur WebSocket . L'implémentation d'un serveur WebSocket ou d'un serveur de notifications push devient une tâche triviale.

  • Service Golang au niveau du serveur.
  • Pour la communication, nous utilisons Goridge.
  • Couche de service mince en PHP.
  • Nous implĂ©mentons le serveur de notification.

Nous recevons une demande et établissons une connexion WebSocket. Si l'application doit envoyer une sorte de notification à l'utilisateur, elle lance ce message via le protocole RPC au serveur WebSocket.



Gérez votre environnement PHP. Lors de la création d'un pool de travailleurs, RoadRunner a un contrôle total sur l'état des variables d'environnement et vous permet de les modifier à votre guise. Si nous écrivons une grande application distribuée, nous pouvons utiliser une seule source de données de configuration et la connecter en tant que système pour configurer l'environnement. Si nous élevons un ensemble de services, tous ces services se heurteront à un seul système, se configureront et fonctionneront. Cela peut grandement simplifier le déploiement et éliminer les fichiers .env.



Fait intéressant, les variables env qui sont disponibles à l'intérieur du travailleur ne sont pas globales dans le système. Cela améliore légèrement la sécurité des conteneurs.

Intégration de la bibliothèque Golang en PHP


Nous avons utilisé cette option sur le site officiel de RoadRunner . Il s'agit d'une intégration d'une base de données à part entière avec la recherche en texte intégral BleveSearch à l'intérieur du serveur.



Nous avons indexé les pages de documentation: nous les avons placées dans Bolt DB, après quoi nous avons effectué une recherche en texte intégral sans véritable base de données comme MySQL, et sans cluster de recherche comme Elasticsearch. Le résultat a été un petit projet dont certaines fonctionnalités sont en PHP, mais la recherche est en Golang.

Implémentation des fonctions Lambda


Vous pouvez aller plus loin et vous débarrasser complètement de la couche HTTP. Dans ce cas, l'implémentation, par exemple, des fonctions Lambda est une tâche simple.



Pour l'implémentation, nous utilisons le runtime standard AWS pour la fonction Lambda. Nous écrivons une petite liaison, coupons complètement les serveurs HTTP et envoyons les données au format binaire aux travailleurs. Nous avons également accès aux paramètres d'environnement, ce qui nous permet d'écrire des fonctions qui sont configurées directement à partir du panneau d'administration Amazon.

Les travailleurs sont en mémoire pendant toute la durée de vie du processus et la fonction Lambda après la demande initiale reste en mémoire pendant 15 minutes. Pour le moment, le code ne se charge pas et répond rapidement. Dans les tests synthétiques, nous avons reçu jusqu'à 0,5 ms pour une demande entrante .

gRPC pour PHP


L'option la plus difficile consiste Ă  remplacer la couche HTTP par la couche gRPC. Ce package est disponible sur GitHub .


Nous pouvons complètement proxy toutes les demandes de Protobuf entrantes vers une application PHP subordonnée, où elles peuvent être décompressées, traitées et répondues. Nous pouvons écrire du code à la fois en PHP et en Golang, en combinant et en transférant des fonctionnalités d'une pile à une autre. Le service prend en charge le middleware. Les deux applications autonomes et en conjonction avec HTTP peuvent fonctionner.

Serveur de file d'attente


La dernière option et la plus intéressante est l'implémentation du serveur de file d'attente .


Du côté de PHP, tout ce que nous faisons est d'obtenir une charge utile binaire, de la décompresser, de faire le travail et d'informer le serveur du succès. Du côté de Golang, nous sommes pleinement engagés dans la gestion des relations avec les courtiers. Il peut s'agir de RabbitMQ, Amazon SQS ou Beanstalk.

Du côté de Golang, nous mettons en œuvre la « fermeture progressive» des travailleurs. Nous pouvons admirablement attendre la mise en œuvre de la «connexion durable» - si la connexion avec le courtier est perdue, le serveur attend un certain temps en utilisant la «stratégie de secours», il lève la connexion et l'application ne le remarque même pas.

Nous pouvons traiter ces demandes à la fois en PHP et en Golang, et les mettre en file d'attente des deux côtés:

  • de PHP via le protocole Goridge Goridge RPC;
  • de Golang - communiquer avec la bibliothèque du SDK.

Si la charge utile tombe, alors pas le consommateur entier ne tombe, mais un seul processus distinct. Le système la soulève immédiatement, la tâche est envoyée au travailleur suivant. Cela vous permet d'effectuer des tâches non-stop.

Nous avons implémenté l'un des courtiers directement dans la mémoire du serveur et utilisé la fonctionnalité Golang. Cela nous permet d'écrire une application à l'aide de files d'attente avant de choisir la pile finale. Nous levons l'application localement, la démarrons et nous avons des files d'attente qui fonctionnent en mémoire et se comportent de la même manière qu'elles se comporteraient sur RabbitMQ, Amazon SQS ou Beanstalk.

Lorsque vous utilisez deux langues dans un tel ensemble hybride, il convient de se rappeler comment les séparer.

Domaines de domaine séparés


Golang est un langage multithread et rapide qui convient à l'écriture de la logique d'infrastructure et de la logique de surveillance et d'autorisation des utilisateurs.

Il est également utile pour implémenter des pilotes personnalisés pour accéder aux sources de données - ce sont des files d'attente, par exemple, Kafka, Cassandra.

PHP est un excellent langage pour écrire la logique métier.

C'est un bon système pour le rendu HTML, ORM et pour travailler avec la base de données.

Comparaison d'outils


Il y a plusieurs mois, Habré a comparé PHP-FPM, PHP-PM, React-PHP, Roadrunner et d'autres outils. Le benchmark a eu lieu sur un projet avec du vrai Symfony 4.

RoadRunner sous charge affiche de bons résultats et devance tous les serveurs. Par rapport à PHP-FPM, les performances sont 6 à 8 fois supérieures.


Dans le même benchmark, RoadRunner n'a pas perdu une seule demande, tout a été 100% élaboré. Malheureusement, React-PHP a perdu 8 à 9 requêtes sous charges - c'est inacceptable. Nous souhaitons que le serveur ne plante pas et fonctionne de manière stable.


Depuis la publication de RoadRunner en accès public sur GitHub, nous avons reçu plus de 30 000 installations. La communauté nous a aidés à écrire un ensemble spécifique d'extensions, d'améliorations et de croire que la solution a le droit à la vie.

RoadRunner est bon si vous souhaitez accélérer considérablement l'application, mais que vous n'êtes pas encore prêt à passer en PHP asynchrone . Il s'agit d'un compromis qui nécessitera un certain effort, mais pas aussi important qu'une réécriture complète de la base de code.

Prenez RoadRunner si vous voulez plus de contrôle sur le cycle de vie de PHP , s'il n'y a pas assez de capacités PHP, par exemple, pour le système de file d'attente ou Kafka, et quand votre bibliothèque Golang populaire résout votre problème, qui n'est pas en PHP, et l'écriture prend du temps, ce que vous n'avez pas non plus.

Résumé


Ce que nous avons obtenu en écrivant ce serveur et en l'utilisant dans notre infrastructure de production.

  • Ils ont augmentĂ© la vitesse de rĂ©action des points d'application de 4 fois par rapport Ă  PHP-FPM.
  • Complètement dĂ©barrassĂ© de 502 erreurs sous charges . Aux pics de charge, le serveur attend juste un peu plus longtemps et rĂ©pond comme s'il n'y avait pas de charges.
  • Après avoir optimisĂ© les fuites de mĂ©moire, les employĂ©s restent en mĂ©moire jusqu'Ă  2 mois . Cela aide lors de l'Ă©criture d'applications distribuĂ©es, car toutes les demandes entre les services sont dĂ©jĂ  mises en cache au niveau du socket.
  • Nous utilisons Keep-Alive. Cela accĂ©lère considĂ©rablement la communication entre un système distribuĂ©.
  • Ă€ l'intĂ©rieur de la vĂ©ritable infrastructure, nous avons tout mis dans l'Alpine Docker Ă  Kubernetes . Le système de dĂ©ploiement et de construction du projet est dĂ©sormais plus facile. Tout ce qui est nĂ©cessaire est de crĂ©er une construction RoadRunner personnalisĂ©e pour le projet, de la placer dans le projet Docker, de remplir l'image Docker, puis de tĂ©lĂ©charger calmement notre pod sur Kubernetes.
  • Selon le calendrier rĂ©el de l'un des projets vers des points individuels qui n'ont pas accès Ă  la base de donnĂ©es, le temps de rĂ©ponse moyen est de 0,33 ms .

La prochaine conférence professionnelle pour les développeurs PHP PHP Russie seulement l'année prochaine. Pour l'instant, nous vous proposons:

  • Faites attention Ă  GolangConf si vous ĂŞtes intĂ©ressĂ© par la partie Go et souhaitez en savoir plus de dĂ©tails ou entendre des arguments en faveur du passage Ă  cette langue. Si vous ĂŞtes prĂŞt Ă  partager votre expĂ©rience, veuillez envoyer des rĂ©sumĂ©s .
  • Participez Ă  HighLoad ++ Ă  Moscou, si tout ce qui est important pour vous est liĂ© Ă  la haute performance, soumettez un rapport avant le 7 septembre ou rĂ©servez un billet.
  • Abonnez-vous Ă  la newsletter et Ă  la chaĂ®ne tĂ©lĂ©gramme afin de recevoir une invitation Ă  PHP Russie 2020 plus tĂ´t que les autres.

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


All Articles