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

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

Hé Habr ! Nous sommes actifs chez Badoo travailler sur les performances de PHP, puisque nous avons un système assez important dans ce langage et que le problème des performances est un problème d'économie d'argent. Il y a plus de dix ans, nous avons créé PHP-FPM pour cela, qui était au départ un ensemble de correctifs pour PHP, puis est entré dans la distribution officielle.

Ces dernières années, PHP a fait de grands progrès : le ramasse-miettes s'est amélioré, le niveau de stabilité a augmenté - aujourd'hui, vous pouvez écrire des démons et des scripts de longue durée en PHP sans aucun problème. 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 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 à partager. Pour rendre leur attente plus amusante, nous publions la traduction de l'annonce RoadRunner de Spiral Scout.

L'approche de l'article est proche de nous : lors de la résolution de nos problèmes, nous utilisons aussi le plus souvent un tas de PHP et de Go, en profitant des avantages des deux langages et en n'abandonnant pas l'un au profit de l'autre.

Chin!

Au cours des dix dernières années, nous avons créé des applications pour les entreprises de la liste Fortune 500, et pour les entreprises dont l'audience ne dépasse 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 eu un impact important non seulement sur les performances de nos produits, mais également sur leur évolutivité : nous avons introduit Golang (Go) dans notre pile technologique.

Presque immédiatement, nous avons découvert que Go nous permettait de créer des applications plus volumineuses avec des améliorations de performances jusqu'à 40 fois supérieures. Avec lui, nous avons pu étendre nos produits PHP existants, en les améliorant en combinant les avantages des deux langages.

Nous vous dirons comment la combinaison de Go et PHP aide à résoudre de vrais problèmes de développement et comment il s'est transformé pour nous en un outil qui peut nous débarrasser de certains des problèmes liés à Modèle de mort PHP.

Votre environnement de développement PHP quotidien

Avant de parler de la façon dont vous pouvez utiliser Go pour faire revivre le modèle PHP mourant, examinons votre environnement de développement PHP par défaut.

Dans la plupart des cas, vous exécutez votre application en utilisant 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, tandis que PHP-FPM lui-même exécute du code PHP. Vous utilisez peut-être la combinaison moins populaire d'Apache et de mod_php. Mais bien que cela fonctionne un peu différemment, les principes sont les mêmes.

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

L'état ne peut pas changer pendant l'exécution d'un script PHP, donc la seule façon d'obtenir un nouvel ensemble de données d'entrée est d'effacer la mémoire du processus et de l'initialiser à nouveau.

Ce modèle d'exécution présente de nombreux avantages. Vous n'avez pas trop à 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 le reste des processus. Mais cette approche présente également des inconvénients qui apparaissent lorsque l'on essaie de faire évoluer l'application.

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

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

Mais tout a son prix. Dans n'importe quel cadre d'entreprise, pour traiter une simple demande d'utilisateur ou accéder à une 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 avoir terminé chaque tâche, vous devrez tout réinitialiser et recommencer : tout le code que vous venez d'initier devient inutile, avec son aide vous ne traiterez plus une autre requête. Dites ceci à n'importe quel programmeur qui écrit dans une autre langue, et vous verrez la perplexité sur son visage.

Les ingénieurs PHP ont cherché des moyens de résoudre ce problème pendant des années, en utilisant des techniques intelligentes de chargement paresseux, des microframeworks, des bibliothèques optimisées, un cache, etc. Mais au final, vous devez toujours réinitialiser l'application entière et recommencer, encore et encore. (Note du traducteur : ce problème sera partiellement résolu avec l'avènement de pré-charge en PHP 7.4)

PHP avec Go peut-il survivre à plus d'une requête ?

Il est possible d'écrire des scripts PHP qui durent plus de quelques minutes (jusqu'à des heures ou des jours) : par exemple, des tâches cron, des parseurs CSV, des coupe-files. Ils fonctionnent tous selon le même scénario : ils récupèrent une tâche, l'exécutent et attendent la suivante. Le code réside en mémoire en permanence, ce qui permet d'économiser de précieuses millisecondes car de nombreuses étapes supplémentaires sont nécessaires pour charger le framework et l'application.

Mais développer des scripts durables n'est pas facile. Toute erreur tue complètement le processus, diagnostiquer les fuites de mémoire est exaspérant et le débogage F5 n'est plus possible.

La situation s'est améliorée avec la sortie de PHP 7 : un ramasse-miettes fiable est apparu, il est devenu plus facile de gérer les erreurs et les extensions du noyau sont désormais étanches. Certes, les ingénieurs doivent toujours faire attention à la mémoire et être conscients des problèmes d'état dans le code (existe-t-il un langage qui peut ignorer ces choses ?). Pourtant, PHP 7 nous réserve moins de surprises.

Est-il possible de prendre le modèle de travail avec des scripts PHP de longue durée, 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 charger à partir de zéro à chaque requête ?

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

Nous savions que nous pouvions écrire un serveur web en PHP pur (PHP-PM) ou en utilisant une extension C (Swoole). Et bien que chaque méthode ait ses propres mérites, les deux options ne nous convenaient pas - nous voulions quelque chose de plus. Nous avions besoin de plus qu'un simple serveur Web - nous nous attendions à obtenir une solution qui pourrait nous éviter les problèmes associés à un "démarrage difficile" en PHP, qui en même temps pourrait être facilement adapté et étendu pour des applications spécifiques. Autrement dit, nous avions besoin d'un serveur d'applications.

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

Les difficultés de combiner deux langages de programmation

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

Par exemple, en utilisant excellente bibliothèque Alex Palaestras, il était possible de partager de la mémoire entre 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, plus courante : construire une interaction entre les processus via des sockets / pipelines. Cette approche s'est avérée fiable 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 échanger des données entre processus et gérer les erreurs de transmission. Dans sa forme la plus simple, ce type de protocole s'apparente à chaîne réseau с 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.

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

Il nous a semblé qu'un protocole n'était pas suffisant - et nous avons ajouté la possibilité d'appeler services net/rpc go directement depuis PHP. Plus tard, cela nous a beaucoup aidé dans le développement, car nous pouvions facilement intégrer des bibliothèques Go dans des 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 workers PHP

Après avoir implémenté le mécanisme d'interaction, nous avons commencé à réfléchir au moyen le plus efficace de transférer des tâches vers des processus PHP. Lorsqu'une tâche arrive, le serveur d'application doit choisir un worker libre pour l'exécuter. Si un travailleur/processus se termine avec une erreur ou « meurt », nous nous en débarrassons et en créons un nouveau pour le remplacer. Et si le travailleur/processus s'est terminé avec succès, nous le renvoyons au pool de travailleurs disponibles pour effectuer les tâches.

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

Pour stocker le pool de travailleurs actifs, nous avons utilisé canal tamponné, pour supprimer les nœuds de calcul « morts » de manière inattendue du pool, nous avons ajouté un mécanisme de suivi des erreurs et des états des nœuds de calcul.

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

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

Parce que PSR-7 est considéré comme immuable (certains diraient que techniquement ce n'est pas le cas), les développeurs doivent écrire des applications qui ne traitent pas la demande comme une entité globale en principe. Cela correspond bien au concept de processus PHP à longue durée de vie. Notre implémentation finale, qui n'a pas encore été nommée, ressemblait à ceci :

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

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

Notre première tâche de test était un backend API, qui éclate périodiquement de manière imprévisible (beaucoup plus souvent que d'habitude). Bien que nginx ait suffi dans la plupart des cas, nous rencontrions régulièrement des erreurs 502 car nous ne pouvions pas équilibrer le système assez rapidement pour l'augmentation de charge attendue.

Pour remplacer cette solution, nous avons déployé notre premier serveur d'application PHP/Go début 2018. Et a immédiatement obtenu un effet incroyable! Non seulement nous nous sommes complètement débarrassés de l'erreur 502, mais nous avons pu réduire le nombre de serveurs de deux tiers, ce qui a permis d'économiser beaucoup d'argent et de soulager les maux de tête des ingénieurs et des chefs de produit.

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

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

application RoadRunner 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 de gérer WebSockets et l'état agrégé globalement dans Prometheus.

Grâce au RPC intégré, vous pouvez ouvrir l'API de n'importe quelle bibliothèque Go pour PHP sans écrire de wrappers d'extension. Plus important encore, avec RoadRunner, vous pouvez déployer de nouveaux serveurs non HTTP. Les exemples incluent l'exécution de gestionnaires en PHP AWS Lambda, en créant des disjoncteurs de file d'attente fiables et même en ajoutant gRPC à nos candidatures.

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

Conclusion

Certaines personnes sont encore prises dans la notion dépassée de PHP en tant que langage lent et peu maniable uniquement bon pour écrire des plugins pour WordPress. Ces gens pourraient même dire que PHP a une telle limitation : lorsque l'application devient assez grosse, il faut choisir un langage plus « mature » et réécrire la base de code accumulée depuis de nombreuses années.

A tout cela je veux répondre : détrompez-vous. Nous pensons que vous êtes le seul à définir des restrictions pour PHP. Vous pouvez passer toute votre vie à passer d'une langue à une autre, à essayer de trouver la réponse parfaite à vos besoins, ou vous pouvez commencer à considérer les langues comme des outils. Les défauts supposés d'un langage comme PHP peuvent en fait être la raison de son succès. Et si vous le combinez avec un autre langage comme Go, vous créerez des produits beaucoup plus puissants que si vous étiez limité à l'utilisation d'un seul langage.

Ayant travaillé avec un tas de Go et PHP, nous pouvons dire que nous les aimons. Nous ne prévoyons pas de sacrifier l'un pour l'autre - au contraire, nous chercherons des moyens d'obtenir encore plus de valeur de cette double pile.

UPD : nous souhaitons la bienvenue au créateur de RoadRunner et au co-auteur de l'article original - Lachésis

Source: habr.com

Ajouter un commentaire