
Bonjour, Habr ! Nous travaillons activement chez Badoo , car nous avons un assez grand système dans ce langage, et la question de la performance est une question d'économie d'argent. Il y a plus de dix ans, nous avons créé PHP-FPM, qui était d'abord un ensemble de correctifs pour PHP, mais qui a ensuite été intégré dans la version officielle.
Au cours des dernières années, PHP a beaucoup progressé : le ramasse-miettes s'est amélioré, la stabilité a augmenté — aujourd'hui, il est possible d'écrire des démons et des scripts de longue durée en PHP sans trop de problèmes. Cela a permis à Spiral Scout d'aller plus loin : RoadRunner, contrairement à PHP-FPM, ne nettoie pas la mémoire entre les requêtes, ce qui permet un gain de performance supplémentaire (bien que cette approche complique le processus de développement). Nous expérimentons actuellement avec cet outil, mais nous n'avons pas encore de résultats à partager. Pour rendre l'attente plus agréable, nous publions une traduction de l'annonce de RoadRunner de Spiral Scout.
L'approche de l'article nous est familière : dans nos tâches, nous utilisons également le duo PHP et Go, tirant parti des avantages des deux langages sans renoncer à l'un au profit de l'autre.
Profitez-en !
Au cours des dix dernières années, nous avons créé des applications pour des entreprises de la liste , ainsi que pour des entreprises ayant une audience de moins de 500 utilisateurs. Pendant tout ce temps, nos ingénieurs ont principalement développé le backend en PHP. Mais il y a deux ans, quelque chose a eu un fort impact non seulement sur la performance de nos produits, mais aussi sur leur évolutivité — nous avons intégré Golang (Go) dans notre pile technologique.
Nous avons immédiatement découvert que Go nous permet de créer des applications plus grandes avec des augmentations de performance allant jusqu'à 40 fois. Grâce à lui, nous avons pu étendre des produits existants, écrits en PHP, en les améliorant grâce à la combinaison des avantages des deux langages.
Nous allons vous expliquer comment la combinaison Go et PHP aide à résoudre des tâches de développement réelles et comment elle est devenue, pour nous, un outil capable de résoudre certains des problèmes liés à .
Votre environnement quotidien de développement PHP
Avant de parler de la façon dont Go peut revitaliser le modèle de « déclin » de PHP, examinons votre environnement standard de développement PHP.
Dans la plupart des cas, vous lancez l'application à l'aide d'une combinaison de serveur web nginx et de serveur PHP-FPM. Le premier sert des fichiers statiques et redirige les requêtes spécifiques vers PHP-FPM, qui exécute le code PHP. Il est possible que vous utilisiez une combinaison moins populaire d'Apache et de mod_php. Mais bien qu'elle fonctionne un peu différemment, les principes restent les mêmes.
Examinons comment PHP-FPM exécute le code de l'application. Lorsque la requête arrive, PHP-FPM initialise un processus PHP enfant et transmet les détails de la requête comme partie de son état (_GET, _POST, _SERVER, etc.).
L'état ne peut pas changer pendant l'exécution du script PHP, c'est pourquoi il n'y a qu'une seule façon d'obtenir un nouvel ensemble de données d'entrée : en nettoyant 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éé, ce qui n'affectera pas les autres processus. Mais cette approche a également des inconvénients, qui se manifestent lors de la tentative de mise à l'échelle de l'application.
Inconvénients et inefficacité de l'environnement PHP classique
Si vous êtes un développeur PHP professionnel, vous savez par où commencer un nouveau projet : par le choix d'un framework. Il représente des bibliothèques pour l'injection de dépendances, des ORM, des traductions et des modèles. Et, bien sûr, toutes les données d'entrée des utilisateurs peuvent être facilement regroupées en un seul objet (Symfony/HttpFoundation ou PSR-7). Les frameworks, c'est génial !
Mais tout a un prix. Dans tout framework de niveau entreprise, pour traiter une simple requête utilisateur ou une interaction avec la base de données, vous devrez charger au moins des dizaines de fichiers, créer de nombreuses classes et analyser plusieurs configurations. Mais le pire, c'est qu'après chaque tâche, vous devrez tout réinitialiser et recommencer : tout le code que vous avez récemment initialisé devient inutile, et vous ne pourrez pas traiter une autre requête avec. Dites cela à n'importe quel programmeur qui écrit dans un autre langage, et vous verrez de la perplexité sur son visage.
Les ingénieurs PHP cherchaient depuis des années des moyens de résoudre ce problème, utilisant des méthodologies réfléchies de « chargement paresseux », des micro-frameworks, des bibliothèques optimisées, du cache, etc. Mais au final, ils doivent toujours tout redémarrer et recommencer encore et encore. (Note du traducteur : une partie de ce problème sera résolue avec l'apparition de dans PHP 7.4)
PHP peut-il survivre à plus d'une requête avec Go ?
Il est possible d'écrire des scripts PHP qui vivent plus de quelques minutes (jusqu'à des heures ou des jours) : par exemple, des tâches cron, des parseurs CSV, des décomposeurs de files d'attente. Tous fonctionnent selon un même scénario : extraire une tâche, l'exécuter, attendre la suivante. Le code reste constamment en mémoire, économisant des millisecondes précieuses, car le chargement du framework et de l'application nécessite de nombreuses étapes supplémentaires.
Cependant, il n'est pas si simple de développer des scripts de long terme. La moindre erreur tue complètement le processus, le diagnostic des fuites de mémoire est frustrant, et il n'est plus possible d'utiliser le débogage par F5.
La situation s'est améliorée avec la sortie de PHP 7 : un ramasse-miettes fiable a été introduit, le traitement des erreurs est devenu plus facile et les extensions du noyau sont désormais protégées contre les fuites. Cependant, les ingénieurs doivent toujours faire attention à la mémoire et garder à l'esprit les problèmes d'état dans le code (y a-t-il une langue dans laquelle on peut ignorer ces choses ?). Mais malgré cela, PHP 7 réserve moins de surprises.
Peut-on prendre le modèle de fonctionnement des scripts PHP de longue durée, l'adapter à des tâches plus triviales comme le traitement des requêtes HTTP et ainsi éliminer la nécessité de tout recharger à chaque requête ?
Pour résoudre ce problème, il fallait d'abord implémenter une application serveur capable de recevoir des requêtes HTTP et de les rediriger une par une vers le travailleur PHP, sans le tuer à chaque fois.
Nous savions que nous pouvions écrire un serveur web en pur PHP (PHP-PM) ou en utilisant une extension C (Swoole). Bien que chaque méthode ait ses avantages, aucune des deux options ne nous convenait — nous recherchions quelque chose de plus. Nous avions besoin non seulement d'un serveur web, mais d'une solution capable de nous libérer des problèmes liés au « démarrage lent » en PHP, tout en étant facilement adaptable et extensible aux applications spécifiques. En d'autres termes, nous avions besoin d'un serveur d'applications.
Go peut-il aider dans ce domaine ? Nous savions que oui, car ce langage compile des applications en fichiers binaires uniques ; il est multiplateforme ; utilise son propre modèle de gestion de la concurrence très élégant et une bibliothèque pour travailler avec HTTP ; enfin, des milliers de bibliothèques open-source et d'intégrations seront à notre disposition.
Difficultés de l'intégration de deux langages de programmation
Tout d'abord, il fallait définir comment deux applications (ou plus) pourraient communiquer entre elles.
Par exemple, en utilisant d'Alex Paleystras, il était possible de mettre en œuvre le partage de mémoire entre les processus PHP et Go (similaire à mod_php dans Apache). Mais cette bibliothèque présente des particularités qui limitent son utilisation pour résoudre notre problème.
Nous avons décidé d'utiliser une autre approche, plus répandue : établir l'interaction entre les processus via des sockets/pipes. Cette méthode a prouvé sa robustesse au cours des dernières décennies 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 le traitement des erreurs de transmission. Dans sa forme la plus simple, un protocole de ce type ressemble à un avec (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.
Du côté PHP, nous avons utilisé , et du côté Go — la bibliothèque.
Un protocole ne nous semblait pas suffisant — nous avons donc ajouté la possibilité d'appeler. Cela nous a beaucoup aidés durant 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 un autre de nos produits open-source.
Répartition des tâches entre plusieurs workers PHP
Après la mise en œuvre du mécanisme d'interaction, nous avons commencé à réfléchir à la manière de transmettre le plus efficacement possible les tâches aux processus PHP. Lorsque une tâche arrive, le serveur d'applications doit choisir un worker libre pour l'exécuter. Si un worker/processus termine son travail avec une erreur ou "meurt", nous nous en séparons et en créons un nouveau à la place. En revanche, si le worker/processus a réussi son travail, nous le ramenons dans le pool de workers disponibles pour exécuter des tâches.

Pour stocker le pool de workers actifs, nous avons utilisé, pour éliminer du pool les workers "morts" de manière inattendue, nous avons ajouté un mécanisme de suivi des erreurs et des états des workers.
Résultat, nous avons obtenu un serveur PHP opérationnel, capable de traiter toutes les demandes présentées sous forme binaire.
Pour que notre application fonctionne comme un serveur web, il a fallu choisir une norme PHP fiable pour représenter toutes les requêtes HTTP entrantes. Dans notre cas, nous avons simplement la requête net/http de Go en format, afin qu'elle soit compatible avec la plupart des frameworks PHP disponibles aujourd'hui.
Étant donné que PSR-7 est considéré comme immuable (certains diront que techniquement cela n'est pas le cas), les développeurs doivent écrire des applications qui ne traitent en principe pas la requête comme une entité globale. Cela s'harmonise parfaitement avec le concept de processus PHP de longue durée. Notre implémentation finale, qui n'a pas encore reçu de nom, était la suivante :

Voici RoadRunner —
Notre première tâche de test a été le backend API, où des pics de requêtes survenaient de manière imprévisible (de manière beaucoup plus fréquente que d'habitude). Bien que dans la plupart des cas les capacités de nginx étaient suffisantes, nous rencontrions régulièrement l'erreur 502, car nous ne pouvions pas ajuster le système assez rapidement pour faire face à l'augmentation prévue de la charge.
Pour remplacer cette solution, au début de l'année 2018, nous avons déployé notre premier serveur d'applications PHP/Go. Et nous avons immédiatement obtenu un effet incroyable ! Non seulement nous nous sommes complètement débarrassés de l'erreur 502, mais nous avons également pu réduire le nombre de serveurs de deux tiers, économisant une tonne d'argent et de comprimés contre les maux de tête pour les ingénieurs et les chefs de produits.
À mi-parcours de l'année, nous avons perfectionné notre solution, l'avons publiée sur GitHub sous la licence MIT et l'avons nommée , soulignant ainsi sa vitesse et son efficacité incroyables.
Comment RoadRunner peut améliorer votre pile de développement
Application nous a permis d'utiliser Middleware net/http côté Go pour effectuer la vérification JWT avant que la requête n'atteigne PHP, ainsi que pour gérer les WebSockets et agréger globalement les états dans Prometheus.
Grâce à l'RPC intégré, il est possible d'ouvrir l'API de n'importe quelle bibliothèque Go pour PHP sans écrire d'extensions wrappers. Plus important encore, avec RoadRunner, il est possible de déployer de nouveaux serveurs différents de HTTP. Parmi les exemples, on peut citer le lancement de gestionnaires en PHP, la création de parseurs de files d'attente fiables et même l'ajout à nos applications.
Avec les communautés PHP et Go, nous avons augmenté la stabilité de la solution, dans certains tests, la performance des applications a été multipliée par 40, nous avons amélioré les outils de débogage, réalisé l'intégration avec le framework Symfony et ajouté le support HTTPS, HTTP/2, des plugins et PSR-17.
Conclusion
Certains sont encore prisonniers de la vision obsolète de PHP comme d'un langage lent et encombrant, adapté uniquement à la création de plugins pour WordPress. Ces personnes pourraient même dire que PHP a une telle limitation : lorsque l'application devient suffisamment grande, il faut choisir un langage plus « mature » et réécrire la base de code accumulée depuis de nombreuses années.
À tout cela, nous voulons répondre : pensez-y à nouveau. Nous croyons que seul vous imposiez certaines limites à PHP. Vous pouvez passer toute votre vie à passer d'un langage à l'autre, essayant de trouver la combinaison parfaite pour vos besoins, ou vous pouvez commencer à percevoir les langages comme des outils. Les prétendus défauts d'un langage comme PHP peuvent en réalité être la clé de son succès. Et si vous l'associez à un autre langage comme Go, vous créerez des produits beaucoup plus puissants que si vous vous limitiez à un seul langage.
Après avoir travaillé avec l'association Go et PHP, nous pouvons affirmer que nous sommes tombés amoureux d'eux. Nous ne prévoyons pas de sacrifier l'un au profit de l'autre — au contraire, nous chercherons des moyens de tirer encore plus de bénéfices de cette double pile.
Mise à jour : nous saluons le créateur de RoadRunner et co-auteur de l'article original —
Source : habr.com
