Après la sortie de PHP7, il est devenu possible d'écrire des applications à longue durée de vie à un coût relativement faible. Pour les programmeurs, des projets tels que prooph
, broadway
, tactician
, messenger
sont devenus disponibles, dont les auteurs prennent la solution aux problèmes les plus courants. Mais que se passe-t-il si vous faites un petit pas en avant, en plongeant dans la question?
Essayons de comprendre le sort d'un autre vélo, ce qui vous permet de mettre en œuvre l'application Publier / S'abonner.
Pour commencer, nous allons essayer de passer brièvement en revue les tendances actuelles dans le monde de PHP, ainsi qu'un bref aperçu du fonctionnement asynchrone.
PHP créé pour mourir
Pendant longtemps, PHP a été principalement utilisé dans le workflow de demande / réponse. Du point de vue des développeurs, c'est assez pratique, car il n'y a pas besoin de s'inquiéter des fuites de mémoire, des connexions de moniteur.
Toutes les requêtes seront exécutées isolément les unes des autres, les ressources utilisées seront libérées et les connexions, par exemple, à la base de données seront fermées lorsque le processus sera terminé.
À titre d'exemple, vous pouvez prendre une application CRUD régulière écrite sur la base du framework Symfony. Pour lire à partir de la base de données et renvoyer JSON, il est nécessaire d'effectuer un certain nombre d'étapes (pour économiser de l'espace et du temps, exclure les étapes de génération / exécution des opcodes):
- Analyse de la configuration;
- Compilation de conteneurs;
- Acheminement des demandes
- Accomplissement;
- Rendu du résultat.
Comme dans le cas de PHP (utilisant des accélérateurs), le framework utilise activement la mise en cache (certaines tâches ne seront pas terminées à la prochaine requête), ainsi qu'une initialisation retardée. À partir de la version 7.4, une précharge sera disponible, ce qui optimisera davantage l'initialisation de l'application.
Cependant, il n'est pas possible de supprimer complètement tous les frais généraux pour l'initialisation.
Aidons PHP Ă survivre
La solution au problème semble assez simple: si vous exécutez l'application à chaque fois trop cher, vous devez l'initialiser une fois, puis lui transmettre des demandes, en contrôlant leur exécution.
Il existe des projets dans l'écosystème PHP tels que php-pm et RoadRunner . Conceptuellement, les deux font la même chose:
- Un processus parent est créé qui agit en tant que superviseur;
- Un pool de processus enfants est créé;
- Lorsqu'une demande est reçue, le maître extrait le processus du pool et lui transmet la demande. Le client est en attente à ce moment;
- Une fois la tâche terminée, le maître renvoie le résultat au client et le processus enfant est renvoyé au pool.
Si un processus enfant meurt, le superviseur le crée à nouveau et l'ajoute au pool. Nous avons créé un démon à partir de notre application avec un seul objectif: supprimer la surcharge d'initialisation, augmentant considérablement la vitesse de traitement des demandes. C'est le moyen le plus indolore d'augmenter la productivité, mais pas le seul.
Remarque:
de nombreux exemples de la série «prenez ReactPHP et accélérez Laravel N fois» marchent sur le réseau. Il est important de comprendre la différence entre diaboliser (et, par conséquent, gagner du temps lors du démarrage de l'application) et multitâche.
Lorsque vous utilisez php-pm ou roadrunner, votre code ne devient pas non bloquant. Vous gagnez simplement du temps lors de l'initialisation.
La comparaison de php-pm, roadrunner et ReactPHP / Amp / Swoole est incorrecte par définition.
PHP et E / S
L'interaction avec les E / S en PHP est exécutée par défaut en mode blocage. Cela signifie que si nous exécutons une demande de mise à jour des informations dans la table, le flux d'exécution s'arrêtera en attendant une réponse de la base de données. Plus ces appels sont en cours de traitement de la demande, plus les ressources du serveur sont inactives. En effet, dans le processus de traitement de la demande, nous devons aller plusieurs fois dans la base de données, écrire quelque chose dans le journal et rendre le résultat au client, au final - également une opération de blocage.
Imaginez que vous êtes un opérateur de centre d'appels et que vous devez appeler 50 clients en une heure.
Vous composez le premier numéro, et là , il est occupé (l'abonné discute par téléphone de la dernière série de Game of Thrones et de la suite de la série).
Et maintenant vous êtes assis et essayez de l'atteindre avant la victoire. Le temps passe, le décalage touche à sa fin. Après avoir perdu 40 minutes en essayant d'atteindre le premier abonné, vous avez raté l'occasion de contacter les autres et naturellement reçu du patron.
Mais vous pouvez faire autrement: n'attendez pas que le premier abonné soit libre et dès que vous entendez un bip, raccrochez et commencez à composer le numéro suivant. Vous pouvez revenir au premier un peu plus tard.
Avec cette approche, les chances de téléphoner au nombre maximum de personnes sont considérablement augmentées, et la vitesse de votre travail ne repose pas sur la tâche la plus lente.
Le code qui ne bloque pas le thread d'exécution (n'utilise pas le blocage des appels d'E / S, ainsi que des fonctions comme sleep()
), est appelé asynchrone.
Revenons à notre application Symfony CRUD. Il est presque impossible de le faire fonctionner en mode asynchrone en raison de l'abondance de l'utilisation des fonctions de blocage: tous fonctionnent avec des configurations, des caches, une journalisation, un rendu de la réponse, une interaction avec la base de données.
Mais ce sont toutes des conventions, essayons de lancer Symfony et d'utiliser Amp , qui fournit une implémentation d'Event Loop (y compris un certain nombre de classeurs), Promises et Coroutines, comme une cerise sur le gâteau pour résoudre notre problème.
La promesse est une façon d'organiser le code asynchrone. Par exemple, nous devons accéder à une ressource http.
Nous créons un objet de demande et le transmettons au transport, que Promise nous renvoie contenant l'état actuel. Il existe trois états possibles:
- Succès: notre demande a été traitée avec succès;
- Erreur: lors de l'exécution de la demande, un problème est survenu (par exemple, le serveur a renvoyé une réponse 500);
- En attente: le traitement de la demande n'a pas encore commencé.
Chaque Promise a une méthode (dans l'exemple, Promise est analysée par Amp ) - onResolve()
, dans laquelle une fonction de rappel avec deux arguments est passée
$promise->onResolve( static function(?/Throwable $throwable, $result): void { if(null !== $throwable) { return; } } );
Après avoir reçu Promise, la question se pose: qui surveillera son statut et nous informera du changement de statut?
Pour cela, Event Loop est utilisé.
En substance, une boucle d'événement est un planificateur qui surveille l'exécution. Dès que la tâche est terminée (peu importe comment), l'appelable que nous avons transmis à Promise sera appelé.
En ce qui concerne les nuances, je recommanderais de lire un article de Nikita Popov: Multitâche coopératif utilisant des coroutines . Cela aidera à clarifier ce qui se passe et où sont les générateurs.
Armés de nouvelles connaissances, essayons de revenir à notre tâche de rendu JSON.
Un exemple de traitement d'une requĂŞte http entrante en utilisant amphp / http-server .
Dès que nous recevons la demande, une lecture asynchrone de la base de données est effectuée (nous obtenons une promesse) et à la fin, l'utilisateur recevra le JSON convoité, formé sur la base des données reçues.
Si nous devons écouter un port de plusieurs processus, nous pouvons regarder vers amphp / cluster
La principale différence est qu'un même processus peut servir plusieurs requêtes à la fois car le thread d'exécution n'est pas bloqué. Le client recevra sa réponse lorsque la lecture de la base de données sera terminée, et s'il n'y a pas de réponse, vous pouvez commencer à traiter la demande suivante.
Le monde merveilleux du PHP asynchrone
Clause de non-responsabilité
Le PHP asynchrone est considéré dans le contexte des espèces exotiques et n'est pas considéré comme quelque chose de sain / normal. Fondamentalement, ils attendront des rires dans le style de "prendre GO / Kotlin, un fou", etc. Je ne dirais pas que ces gens ont tort, mais ...
Il existe un certain nombre de projets qui aident à écrire du code PHP non bloquant. Dans le cadre de l'article, je n'analyserai pas complètement tous les avantages et les inconvénients, mais j'essaierai simplement de les examiner superficiellement.
Un framework asynchrone écrit contrairement aux autres en C et livré comme une extension de PHP. Il possède peut-être les meilleurs indicateurs de performance pour le moment.
Il y a une implémentation de canaux, de corutine et d'autres choses savoureuses, mais il a un gros inconvénient - la documentation. Bien qu'il soit en partie en anglais, à mon avis, il n'est pas très détaillé et l'api lui-même n'est pas très évident.
Quant à la communauté, elle n'est pas non plus toute simple et sans ambiguïté. Personnellement, je ne connais pas une seule personne vivante qui utilise Swoole au combat. Je vais peut-être surmonter mes peurs et migrer vers lui, mais cela ne se produira pas dans un avenir proche.
Aux inconvénients, vous pouvez également ajouter que contribuer au projet (en utilisant la demande de tirage) avec des modifications est également difficile si vous ne connaissez pas C au niveau approprié.
S'il perd de la vitesse par rapport à son concurrent (en parlant de Swoole), alors il n'est pas très visible et la différence dans un certain nombre de scénarios peut être négligée.
Il a une intégration avec ReactPHP, qui à son tour augmente le nombre d'implémentations de problèmes d'infrastructure. Pour économiser de l'espace, je décrirai les inconvénients avec ReactPHP.
Les avantages comprennent une communauté assez importante et un grand nombre d'exemples. Les inconvénients commencent à apparaître dans le processus d'utilisation - c'est le concept de promesse.
Si vous devez effectuer plusieurs opérations asynchrones, le code se transforme alors en une corbeille sans fin d'appels (voici un exemple de connexion simple à RabbiqMQ sans créer d'échange / file d'attente et leurs liants).
Avec un certain raffinement avec un fichier (considéré comme la norme), vous pouvez obtenir une implémentation de corutine qui vous aidera à vous débarrasser de l'enfer Promise.
Sans le projet recoilphp / recoil, l' utilisation de ReactPHP, Ă mon avis, n'est pas possible dans une application saine.
De plus, outre tout le reste, on a l'impression que son développement a beaucoup ralenti. Pas assez, par exemple, un travail normal avec PostgreSQL.
Ă€ mon avis, la meilleure des options qui existent Ă l'heure actuelle.
En plus de la promesse habituelle, il existe une implémentation de Coroutine, qui facilite grandement le processus de développement et le code semble le plus familier aux programmeurs PHP.
Les développeurs complètent et améliorent constamment le projet, avec des commentaires, il n'y a pas non plus de problèmes.
Malheureusement, avec tous les avantages du framework, la communauté est relativement petite, mais en même temps il existe des implémentations, par exemple, travaillant avec PostgreSQL, ainsi que toutes les choses de base (système de fichiers, client http, DNS, etc.).
Je ne comprends toujours pas très bien le sort du projet ext-async, mais les gars le suivent. Ce qui en résultera dans la 3ème version, le temps nous le dira.
Pour commencer
Donc, nous avons un peu réglé la partie théorique, il est temps de passer à la pratique et de remplir les bosses.
Tout d'abord, nous formalisons un peu les exigences:
- Messagerie asynchrone (le concept de
message
lui-même peut être divisé en 2 types)
command
: indique la nécessité de terminer la tâche. Ne renvoie pas de résultat (au moins dans le cas d'une communication asynchrone);event
: signale tout changement d'état (par exemple, à la suite d'une commande).
- Format non bloquant pour travailler avec les E / S;
- La possibilité d'augmenter facilement le nombre de processeurs;
- Capacité à écrire des gestionnaires de messages dans n'importe quelle langue.
Tout message est intrinsèquement une structure simple et partagé uniquement par la sémantique. La dénomination des messages est extrêmement importante du point de vue de la compréhension du type et du but (bien que ce point soit ignoré dans l'exemple).
Pour une liste d'exigences, une implémentation simple du modèle de publication / abonnement est la mieux adaptée.
Pour assurer une exécution distribuée, nous utiliserons RabbitMQ comme courtier de messages.
Le prototype a été écrit en utilisant ReactPHP , Bunny et DoctrineDBAL .
Un lecteur attentif pourrait remarquer que Dbal utilise les appels de blocage pdo / mysqli en interne, mais au stade actuel, cela n'était pas particulièrement important, car il fallait comprendre ce qui devait se passer à la fin.
L'un des problèmes était le manque de bibliothèques pour travailler avec PostgreSQL. Il existe quelques brouillons, mais cela ne suffit pas pour un travail à part entière (plus de détails ci-dessous).
Après une courte recherche, ReactPHP a été retiré au profit d'Amp, car il est relativement simple et se développe très activement.
Transport RabbitMQ
Mais avec tous les avantages d'Amp, il y avait 1 problème: Amp n'a pas de pilote pour RabbitMQ ( Bunny ne supporte que ReactPHP).
En théorie, Amp vous permet d'utiliser Promise d'un concurrent. Il semblerait que tout devrait être simple, mais ReactPHP utilise Event Loop pour travailler avec les sockets de la bibliothèque.
À un moment donné, évidemment, deux boucles d'événements différentes n'ont pas pu être démarrées, donc je n'ai pas pu utiliser la fonction adapt () .
Malheureusement, la qualité du code dans bunny laissait beaucoup à désirer et il n'était pas possible de remplacer correctement une implémentation par une autre. Afin de ne pas arrêter le travail, il a été décidé de réécrire un peu la bibliothèque afin qu'elle fonctionne avec Amp et ne conduise pas à bloquer le flux d'exécution.
Cette adaptation avait l'air très effrayante, tout le temps j'en avais extrêmement honte, mais surtout, ça fonctionnait. Eh bien, puisqu'il n'y a rien de plus permanent que temporaire, l'adaptateur est resté en prévision d'une personne qui n'est pas trop paresseuse pour faire face à la mise en œuvre du pilote.
Et un tel homme a été trouvé. Le projet PHPinnacle , entre autres, fournit une implémentation d'un adaptateur adapté pour Amp.
Le nom de l'auteur est Anton Shabovta, qui parlera de php asynchrone dans le cadre de PHP Russie et du développement de pilotes pour PHP fwdays .
PostgreSQL
La deuxième caractéristique du travail est l'interaction avec la base de données. Dans les conditions du PHP «traditionnel», tout est simple: nous avons une connexion et toutes les requêtes sont exécutées séquentiellement.
Dans le cas d'une exécution asynchrone, nous devons pouvoir exécuter simultanément plusieurs requêtes (par exemple, 3 transactions). Pour ce faire, une implémentation de pool de connexions est requise.
Le mécanisme de travail est assez simple:
- nous ouvrons N connexions au démarrage (ou initialisation retardée, pas le point);
- si nécessaire, nous prenons la connexion de la piscine, en veillant à ce que personne d'autre ne puisse l'utiliser;
- Nous exécutons la demande et détruisons la connexion ou la renvoyons au pool (de préférence).
Premièrement, cela nous permet de démarrer plusieurs transactions en même temps, et deuxièmement, cela accélère le travail en raison de la présence de connexions déjà ouvertes. L'ampli a un composant amphp / postgres . Il s'occupe des connexions: surveille leur nombre, leur durée de vie, et tout cela sans bloquer le flux d'exécution.
Par ailleurs, lorsque vous utilisez, par exemple, ReactPHP, vous devrez l'implémenter vous-même si vous souhaitez travailler avec une base de données.
Mutex
Pour un fonctionnement efficace et, surtout, correct de l'application, il est nécessaire de mettre en œuvre quelque chose de similaire aux mutex. On peut distinguer 3 scénarios pour leur utilisation:
- Dans le cadre d'un processus, un mécanisme simple en mémoire convient sans excédent;
- Si nous voulons fournir le verrouillage dans plusieurs processus, nous pouvons utiliser le système de fichiers (bien sûr, en mode non bloquant);
- Si dans le contexte de plusieurs serveurs, vous devez déjà penser à quelque chose comme Zookeeper.
Des mutex sont nécessaires pour résoudre les problèmes de conditions de concurrence. Après tout, nous ne savons pas (et nous ne pouvons pas savoir) dans quel ordre nos tâches seront exécutées, mais nous devons néanmoins garantir l'intégrité des données.
Journalisation / Contextes
Pour la journalisation, Monolog est déjà devenu standard, mais avec quelques mises en garde: nous ne pouvons pas utiliser les gestionnaires intégrés, car ils entraîneront des verrous.
Pour écrire dans stdOut, vous pouvez prendre amphp / log , ou écrire un simple message envoyé à un Graylog.
Étant donné qu'à un moment donné, nous pouvons traiter de nombreuses tâches et lorsque vous enregistrez des journaux, vous devez comprendre dans quel contexte les données sont écrites. Au cours des expériences, il a été décidé de faire trace_id
( traçage distribué ). L'essentiel est que toute la chaîne d'appels doit être accompagnée d'un identifiant d'intercommunication qui peut être suivi. De plus, au moment de la réception du message, package_id
généré, ce qui indique exactement le message reçu.
Ainsi, en utilisant les deux identifiants, nous pouvons facilement suivre à quoi se réfère un enregistrement particulier. Le fait est qu'en PHP traditionnel, tous les enregistrements que nous obtenons dans le journal sont principalement dans l'ordre dans lequel ils ont été écrits. Dans le cas d'une exécution asynchrone, il n'y a pas de modèle dans l'ordre des entrées.
Résiliation
Une autre des nuances du développement asynchrone est le contrôle de l'arrêt de notre démon. Si vous venez de tuer le processus, toutes les tâches en cours ne seront pas terminées et les données seront perdues. Dans l'approche habituelle, il y a aussi un tel problème, mais ce n'est pas si grave, car une seule tâche est effectuée à la fois.
Pour terminer correctement l'exécution, nous avons besoin de:
- Se désinscrire de la file d'attente. En d'autres termes, il est impossible de recevoir de nouveaux messages;
- Terminez toutes les tâches restantes (attendez la résolution des promesses);
- Et seulement après cela, terminer le script.
Fuites, débogage
Contrairement à la croyance populaire, en PHP moderne, il n'est pas si simple de faire face à des situations dans lesquelles une fuite de mémoire se produit. Il faut faire quelque chose d'absolument mauvais.
Cependant, une fois confronté à cela, mais à cause de la négligence banale. Pendant l'implémentation de Heartbeat, un nouveau temporisateur a été ajouté toutes les 40 secondes pour interroger la connexion. Il n'est pas difficile de deviner qu'après un certain temps, l'utilisation de la mémoire a commencé à remonter et assez rapidement.
Entre autres choses, il a écrit un simple observateur qui démarrera éventuellement toutes les 10 minutes et appellera gc_collect_cycles () et gc_mem_caches () .
Mais le démarrage forcé du ramasse-miettes n'est pas quelque chose de nécessaire et de fondamental.
Afin de voir constamment l'utilisation de la mémoire, un MemoryUsageProcessor standard a été ajouté à la journalisation .
Si vous avez l'idée que Event Loop se bloque avec quelque chose, cela peut également être facilement vérifié: connectez simplement LoopBlockWatcher .
Mais vous devez vous assurer que cet observateur ne démarre pas dans l'environnement de production. Cette fonctionnalité est utilisée exclusivement pendant le développement.
Résultats
: php-service-bus , Message Based .
, :
composer create-project php-service-bus/skeleton pub-sub-example cd pub-sub-example docker-compose up --build -d
, , .
/bin/consumer
, .
/src
3 : Ping
; Pong
: ; PingService
: , .
PingService
, 2 :
public function handle(Ping $command, KernelContext $context): Promise { return $context->delivery(new Pong()); } public function whenPong(Pong $event, KernelContext $context): void { $context->logContextMessage('Pong message received'); }
handle
( 1 ). @CommandHandler
;
- Promise , RabbitMQ (
delivery()
). , RabbitMQ .
whenPong
— Pong
. . @EventListener
;
, — . , , , . php-service-bus , , .
2 : , ( ) . , , (, ).
Ping
, Pong
. .
, RabbitMQ:
tools/ping
, php-service-bus , Message based .
Ping\Pong, — , , Hello, world
.
, .
- , , , Saga pattern (Process manager) .
, symfony/messenger .
, , .