RoadRunner: PHP n'est pas fait pour mourir, ou Golang à la rescousse



Bonjour, Habr! Chez Badoo, nous travaillons activement sur les performances PHP , car nous avons un système assez volumineux dans ce langage et la question des performances est une question d'économie d'argent. Il y a plus de dix ans, nous avons créé pour ce PHP-FPM, qui était d'abord un ensemble de correctifs pour PHP, et qui est ensuite entré en livraison officielle.

Ces dernières années, PHP a fait de grands progrès: le garbage collector s'est amélioré, le niveau de stabilité s'est amélioré - aujourd'hui, en PHP, vous pouvez écrire des démons et des scripts de longue durée sans aucun problème particulier. Cela a permis à Spiral Scout d'aller plus loin: RoadRunner, contrairement à PHP-FPM, n'efface pas la mémoire entre les requêtes, ce qui donne un gain de performance supplémentaire (bien que cette approche complique le processus de développement). Nous expérimentons actuellement cet outil, mais nous n'avons pas encore de résultats qui pourraient être partagés. Les attendre était plus amusant, nous publions la traduction de l'annonce du RoadRunner de Spiral Scout.

L'approche de l'article est proche de nous: pour résoudre nos problèmes, nous utilisons aussi le plus souvent un tas de PHP et Go, tirant parti des deux langages et n'abandonnant pas l'un au profit de l'autre.

Profitez-en!



Au cours des dix dernières années, nous avons créé des applications pour les sociétés Fortune 500 et pour les entreprises avec un public ne dépassant pas 500 utilisateurs. Pendant tout ce temps, nos ingénieurs ont développé le backend principalement en PHP. Mais il y a deux ans, quelque chose a grandement influencé non seulement les performances de nos produits, mais aussi leur évolutivité - nous avons introduit Golang (Go) dans notre pile technologique.

Presque immédiatement, nous avons constaté que Go nous permet de créer de plus grandes applications avec des performances jusqu'à 40 fois supérieures. Avec cela, nous avons pu étendre les produits existants écrits en PHP, en les améliorant grâce à une combinaison des avantages des deux langages.

Nous allons vous expliquer comment la combinaison Go et PHP aide à résoudre de vrais problèmes de développement et comment elle est devenue pour nous un outil qui peut soulager une partie des problèmes associés au modèle PHP «mourant» .

Votre environnement de développement PHP au quotidien


Avant de parler de la façon dont Go peut animer le modèle PHP «mourant», examinons votre environnement de développement PHP standard.

Dans la plupart des cas, vous lancez l'application à l'aide d'une combinaison du serveur Web nginx et du serveur PHP-FPM. Le premier sert des fichiers statiques et redirige des requêtes spécifiques vers PHP-FPM, et PHP-FPM lui-même exécute le code PHP. Vous utilisez peut-être un bundle moins populaire d'Apache et de mod_php. Mais bien que cela fonctionne un peu différemment, les principes sont les mêmes.

Considérez comment PHP-FPM exécute le code d'application. Lorsqu'une demande arrive, PHP-FPM initialise un processus PHP enfant et transmet les détails de la demande dans le cadre de son état (_GET, _POST, _SERVER, etc.).

L'état ne peut pas changer pendant l'exécution du script PHP, vous pouvez donc obtenir un nouvel ensemble de données d'entrée d'une seule manière: en effaçant la mémoire du processus et en l'initialisant à nouveau.

Ce modèle d'exécution présente de nombreux avantages. Vous n'avez pas à vous soucier de la consommation de mémoire, tous les processus sont complètement isolés et si l'un d'eux meurt, il sera automatiquement recréé et cela n'affectera pas les autres processus. Mais cette approche présente également des inconvénients qui apparaissent lors de la mise à l'échelle de l'application.

Inconvénients et inefficacités d'un environnement PHP standard


Si vous êtes engagé dans le développement professionnel en PHP, vous savez par où commencer un nouveau projet, avec le choix d'un framework. Il s'agit d'une bibliothèque pour l'injection de dépendances, les ORM, les traductions et les modèles. Et, bien sûr, toutes les entrées utilisateur peuvent être commodément placées dans un seul objet (Symfony / HttpFoundation ou PSR-7). Les cadres sont cool!

Mais tout a un prix. Dans n'importe quel cadre d'entreprise, pour traiter une simple demande d'utilisateur ou accéder à la base de données, vous devrez télécharger au moins des dizaines de fichiers, créer de nombreuses classes et analyser plusieurs configurations. Mais le pire, c'est qu'après avoir terminé chaque tâche, vous devrez tout réinitialiser et recommencer: tout le code que vous venez d'initier devient inutile, avec lui, vous ne traiterez plus une autre demande. Parlez-en à n'importe quel programmeur qui écrit dans une autre langue et vous verrez la perplexité sur son visage.

Pendant des années, les ingénieurs PHP ont cherché des moyens de résoudre ce problème, en utilisant des méthodes bien pensées de chargement paresseux, de microframes, de bibliothèques optimisées, de cache, etc. Mais au final, vous devez toujours réinitialiser toute l'application et recommencer, encore et encore. (Note du traducteur: ce problème sera partiellement résolu avec l'avènement de la précharge en PHP 7.4)

PHP peut-il utiliser Go pour survivre à plusieurs requêtes?


Vous pouvez écrire des scripts PHP qui dureront plus de quelques minutes (jusqu'à des heures ou des jours): par exemple, des tâches cron, des analyseurs CSV, des disjoncteurs de file d'attente. Ils travaillent tous selon un scénario: ils extraient la tâche, la complètent, attendent la suivante. Le code est constamment en mémoire, ce qui permet d'économiser de précieuses millisecondes, car de nombreuses étapes supplémentaires sont nécessaires pour télécharger le cadre et l'application.

Mais développer des scripts à longue durée de vie n'est pas si simple. Toute erreur tue complètement le processus, le diagnostic des fuites de mémoire est exaspérant et le débogage à l'aide de F5 n'est plus possible.

La situation s'est améliorée avec la sortie de PHP 7: un garbage collector fiable est apparu, il est devenu plus facile de gérer les erreurs et les extensions du noyau sont désormais protégées contre les fuites. Certes, les ingénieurs doivent toujours gérer soigneusement la mémoire et se souvenir des problèmes d'état dans le code (existe-t-il un langage dans lequel vous pouvez ignorer ces choses?). Et pourtant, en PHP 7, il y a moins de surprises.

Est-il possible de prendre un modèle pour travailler avec des scripts PHP à longue durée de vie, de l'adapter à des tâches plus triviales comme le traitement des requêtes HTTP et ainsi de se débarrasser de la nécessité de tout télécharger à partir de zéro à chaque requête?

Pour résoudre ce problème, il a d'abord fallu implémenter une application serveur capable d'accepter les requêtes HTTP et de les rediriger une à une vers le travailleur PHP, sans le tuer à chaque fois.

Nous savions que nous pouvions écrire un serveur Web en PHP pur (PHP-PM) ou en utilisant l'extension C (Swoole). Et bien que chaque méthode ait ses propres avantages, les deux options ne nous convenaient pas - je voulais quelque chose de plus. Ce n'était pas seulement un serveur Web qui était nécessaire - nous nous attendions à obtenir une solution qui pourrait nous sauver des problèmes associés à un «démarrage difficile» en PHP, qui en même temps peut être facilement adapté et étendu pour des applications spécifiques. Autrement dit, nous avions besoin d'un serveur d'applications.

Est-ce que Go peut aider avec ça? Nous savions que c'était possible, car ce langage compile les applications dans des fichiers binaires uniques; c'est multiplateforme; utilise son propre modèle de concurrence très élégant et une bibliothèque pour travailler avec HTTP; et enfin, des milliers de bibliothèques open source et d'intégrations seront à notre disposition.

Difficultés à combiner deux langages de programmation


Tout d'abord, il était nécessaire de déterminer comment deux applications ou plus communiqueraient entre elles.

Par exemple, avec l'aide de l' excellente bibliothèque d' Alex Palaestras, il a été possible d'implémenter le partage de mémoire par les processus PHP et Go (similaire à mod_php dans Apache). Mais cette bibliothèque a des fonctionnalités qui limitent son utilisation pour résoudre notre problème.

Nous avons décidé d'utiliser une approche différente et plus courante: pour créer une interaction entre les processus via des sockets / pipelines. Au cours des dernières décennies, cette approche s'est avérée fiable et a été bien optimisée au niveau du système d'exploitation.

Pour commencer, nous avons créé un protocole binaire simple pour l'échange de données entre les processus et la gestion des erreurs de transmission. Dans sa forme la plus simple, un protocole de ce type est similaire à netstring avec un en-tête de paquet de taille fixe (dans notre cas, 17 octets), qui contient des informations sur le type de paquet, sa taille et un masque binaire pour vérifier l'intégrité des données.

Côté PHP, nous avons utilisé la fonction pack , et côté Go, la bibliothèque d' encodage / binaire .

Un protocole ne nous suffisait pas - et nous avons ajouté la possibilité d'appeler les services Go net / rpc directement depuis PHP . Plus tard, cela nous a beaucoup aidé dans le développement, car nous pouvions facilement intégrer les bibliothèques Go dans les applications PHP. Le résultat de ce travail peut être vu, par exemple, dans notre autre produit open-source Goridge .

Répartition des tâches entre plusieurs travailleurs PHP


Après avoir implémenté le mécanisme d'interaction, nous avons commencé à réfléchir à la meilleure façon de transférer des tâches vers des processus PHP. Lorsqu'une tâche arrive, le serveur d'applications doit choisir un travailleur libre pour la terminer. Si le travailleur / processus s'est terminé avec une erreur ou "est mort", nous nous en débarrassons et en créons un nouveau en retour. Et si le travailleur / processus a fonctionné avec succès, nous le renvoyons au pool de travailleurs disponibles pour effectuer les tâches.



Nous avons utilisé un canal tamponné pour stocker le pool de travailleurs actifs; pour supprimer de manière inattendue des «morts» des travailleurs du pool, nous avons ajouté un mécanisme de suivi des erreurs et de l'état des travailleurs.

En conséquence, nous avons obtenu un serveur PHP fonctionnel capable de traiter toutes les demandes présentées sous forme binaire.

Pour que notre application commence à fonctionner en tant que serveur Web, j'ai dû choisir un standard PHP fiable pour présenter toutes les requêtes HTTP entrantes. Dans notre cas, nous convertissons simplement la requête net / http de Go au format PSR-7 afin qu'elle soit compatible avec la plupart des frameworks PHP disponibles aujourd'hui.

Étant donné que le PSR-7 est considéré comme immuable (quelqu'un dira que ce n'est pas le cas techniquement), les développeurs doivent écrire des applications qui, en principe, ne traitent pas la demande en tant qu'entité globale. Cela va bien avec le concept de processus PHP à longue durée de vie. Notre implémentation finale, qui n'a pas encore reçu de nom, ressemblait à ceci:



Présentation de RoadRunner - un serveur d'applications PHP hautes performances


Notre première tâche de test était un backend API, qui provoquait périodiquement des rafales de requêtes imprévisibles (beaucoup plus souvent que d'habitude). Bien que dans la plupart des cas, il y avait suffisamment de fonctionnalités nginx, nous avons régulièrement rencontré une erreur 502, car nous ne pouvions pas équilibrer le système assez rapidement pour l'augmentation prévue de la charge.

Pour remplacer cette solution, début 2018, nous avons déployé notre premier serveur d'applications PHP / Go. Et immédiatement obtenu un effet incroyable! Nous avons non seulement complètement éliminé l'erreur 502, mais nous avons également pu réduire le nombre de serveurs des deux tiers, économisant ainsi une tonne d'argent et des pilules contre les maux de tête pour les ingénieurs et les chefs de produit.

Au milieu de l'année, nous avons amélioré notre solution, l'avons publiée sur GitHub sous la licence MIT et l'avons nommée RoadRunner , en soulignant sa vitesse et son efficacité incroyables.

Comment RoadRunner peut améliorer votre pile de développement


L'utilisation de RoadRunner nous a permis d'utiliser Middleware net / http côté Go pour effectuer la vérification JWT avant que la demande ne passe en PHP, ainsi que pour traiter les WebSockets et les états d'agrégation globale dans Prometheus.

Grâce au RPC intégré, vous pouvez ouvrir l'API de toutes les bibliothèques Go pour PHP sans écrire de wrappers d'extension. Plus important encore, RoadRunner peut déployer de nouveaux serveurs autres que HTTP. Les exemples incluent l'exécution de gestionnaires AWS Lambda en PHP, la création de résolveurs de files d'attente robustes et même l'ajout de gRPC à nos applications.

Avec l'aide des communautés PHP et Go, nous avons augmenté la stabilité de la solution, dans certains tests, nous avons augmenté les performances des applications jusqu'à 40 fois, amélioré les outils de débogage, implémenté l'intégration avec le framework Symfony et ajouté la prise en charge de HTTPS, HTTP / 2, des plug-ins et du PSR-17.

Conclusion


Certains sont toujours captivés par la notion obsolète de PHP en tant que langage lent et lourd, adapté uniquement à l'écriture de plugins pour WordPress. Ces gens peuvent même dire que PHP a une telle limitation: lorsque l'application devient suffisamment grande, vous devez choisir un langage plus «mature» et réécrire la base de code qui s'est accumulée pendant de nombreuses années.

Je voudrais répondre à tout cela: détrompez-vous. Nous pensons que vous seul définissez des restrictions pour PHP. Vous pouvez passer toute votre vie à passer d'une langue à une autre, à essayer de trouver la combinaison parfaite avec vos besoins, ou vous pouvez commencer à percevoir les langues comme des outils. Les failles apparentes d'un langage comme PHP peuvent en fait être les raisons de son succès. Et si vous le combinez avec une autre langue comme Go, vous créerez des produits beaucoup plus puissants que si vous étiez limité à utiliser une seule langue.

Après avoir travaillé avec un tas de Go et PHP, nous pouvons dire que nous les aimons. Nous ne prévoyons pas de sacrifier l'un au profit de l'autre - au contraire, nous chercherons des moyens d'extraire encore plus de bénéfices de cette double pile.

UPD: Bienvenue chez RoadRunner créateur et co-auteur de l'article original - Lachezis

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


All Articles