Transférer un backend PHP vers le bus de flux Redis et choisir une bibliothèque indépendante des frameworks



Préface


Mon site, que je fais comme passe-temps, est conçu pour stocker des pages d'accueil et des sites personnels intéressants. Ce sujet a commencé à m'intéresser au tout début de mon parcours en programmation, à ce moment-là j'étais ravi de trouver de grands professionnels qui écrivent sur eux-mêmes, leurs hobbies et projets. L'habitude de les découvrir est restée pour l'instant: sur quasiment tous les sites commerciaux et peu nombreux, je continue de fouiller le pied de page à la recherche de liens vers les auteurs.

Mise en œuvre de l'idée


La première version était juste une page html sur mon site personnel, où je mettais des liens avec des signatures dans une liste ul. Ayant tapé 20 pages pendant un certain temps, j'ai commencé à penser que ce n'était pas très efficace et j'ai décidé d'essayer d'automatiser le processus. Sur stackoverflow, j'ai remarqué que beaucoup indiquent des sites dans leurs profils, j'ai donc écrit un analyseur php qui vient de parcourir les profils, en commençant par le premier (adresse sur SO et jusqu'à ce jour comme ceci: `/ users / 1`), extrait les liens à partir de la balise souhaitée et empilés dans SQLite.

Cela peut être appelé la deuxième version: une collection de dizaines de milliers d'URL dans une plaque SQLite qui a remplacé la liste statique en html. J'ai fait une recherche simple sur cette liste. Parce que il n'y avait que des URL, alors la recherche était juste pour eux.

À ce stade, j'ai abandonné le projet et y suis retourné, après une longue période. À ce stade, mon expérience de travail dépassait déjà trois ans et je sentais que je pouvais faire quelque chose de plus sérieux. De plus, il y avait un grand désir de maîtriser par eux-mêmes des technologies relativement nouvelles.

Version moderne


Le projet a été déployé dans docker, la base de données a été transférée vers mongoDb et, relativement récemment, des radis ont été ajoutés, ce qui était au départ juste pour la mise en cache. Comme base, une des microframes PHP est utilisée.

Le problème


De nouveaux sites sont ajoutés par une commande de console qui effectue de manière synchrone ce qui suit:

  • Télécharger du contenu par URL
  • Indique si HTTPS était disponible
  • Préserve l'essence du site Web
  • HTML source et en-têtes enregistrés dans l'historique d'indexation
  • Analyse le contenu, récupère le titre et la description
  • Enregistre les données dans une collection distincte.

C'était suffisant pour simplement stocker des sites et les afficher dans une liste:



Mais l'idée d'indexer, de catégoriser et de classer automatiquement tout, de tout garder à jour, convient mal à ce paradigme. Même l'ajout d'une méthode Web pour ajouter des pages a nécessité la duplication et le blocage de code pour éviter les DDoS potentiels.

En général, bien sûr, tout peut être fait de manière synchrone, et dans la méthode Web, enregistrez simplement l'URL pour vous assurer que le démon monstrueux effectue toutes les tâches pour les URL de la liste. Mais tout de même, même ici, le mot «tour» supplie. Et si la file d'attente est implémentée, toutes les tâches peuvent être divisées et exécutées au moins de manière asynchrone.

Solution


Introduisez des files d'attente et créez un système de traitement basé sur les événements pour toutes les tâches. Et pendant longtemps, j'ai voulu essayer Redis Streams.

Utilisation des flux Redis en PHP


Parce que Je n'ai pas de framework des trois géants Symfony, Laravel, Yii, et j'aimerais trouver une bibliothèque indépendante. Mais, comme il s'est avéré (au premier examen), il est impossible de trouver des bibliothèques sérieuses individuelles. Tout ce qui est associé aux files d'attente est soit une projection de 3 commits d'il y a cinq ans, soit lié à un cadre.

J'ai entendu parler de Symfony en tant que fournisseur de certains composants utiles, et j'en utilise déjà certains. Et aussi depuis Laravel, quelque chose peut aussi être utilisé, par exemple, leur ORM, sans la présence du framework lui-même.

symfony / messenger


Le premier candidat me parut immédiatement idéal, et sans aucun doute je l'ai installé. Mais il était plus difficile de rechercher sur Google des exemples d'utilisation en dehors de Symfony. Comment collecter à partir d'un tas de classes avec des noms universels, sans parler, un bus pour envoyer des messages, et même sur Redis?



La documentation sur le site officiel était assez détaillée, mais l'initialisation n'a été décrite que pour Symfony en utilisant leur YML préféré et d'autres méthodes magiques pour un non-symphoniste. Je n'avais aucun intérêt pour le processus d'installation, surtout pendant les vacances du Nouvel An. Mais j'ai dû le faire pendant une durée inattendue.

Essayer de comprendre comment instancier un système à l'aide de sources Symfony n'est pas non plus la tâche la plus triviale pour des délais serrés:



En fouillant dans cela et en essayant de faire quelque chose avec mes mains, je suis arrivé à la conclusion que je faisais une sorte de béquilles et j'ai décidé d'essayer autre chose.

illuminer / faire la queue


Il s'est avéré que cette bibliothèque était étroitement liée à l'infrastructure Laravel et à un tas d'autres dépendances, donc je n'y ai pas passé beaucoup de temps: installé, regardé, vu les dépendances et supprimé.

file d'attente yiisoft / yii2


Eh bien, ici, il a été immédiatement supposé du nom, encore une fois une liaison étroite à Yii2. J'ai dû utiliser cette bibliothèque et ce n'était pas mal, mais je ne pensais pas que cela dépendait complètement de Yii2.

Le reste


Tout le reste que j'ai trouvé sur le github était des projections obsolètes et abandonnées non fiables sans étoiles, fourches et un grand nombre de commits.

Retour à symfony / messenger, détails techniques


J'ai dû m'occuper de cette bibliothèque et après avoir passé un peu plus de temps, j'ai pu. Il s'est avéré que tout est assez concis et simple. Pour l'instanciation du bus, j'ai fait une petite usine, car J'ai eu plusieurs pneus avec différents gestionnaires.



Quelques étapes:

  • Créez des gestionnaires de messages qui devraient simplement être appelables
  • Enveloppez-les dans HandlerDescriptor (classe de la bibliothèque)
  • Nous encapsulons ces «Descripteurs» dans l'instance HandlersLocator.
  • Ajouter HandlersLocator à l'instance MessageBus
  • Nous passons à SendersLocator un ensemble de `SenderInterface`, dans mon cas, les instances des classes` RedisTransport`, qui sont configurées de manière évidente
  • Ajouter SendersLocator à l'instance MessageBus

MessageBus a une méthode `-> dispatch ()`, qui recherche les gestionnaires appropriés dans HandlersLocator et leur transmet le message, en utilisant le `SenderInterface` correspondant pour envoyer via le bus (flux Redis).

Dans la configuration du conteneur (dans ce cas php-di), tout ce groupe peut être configuré comme suit:

CONTAINER_REDIS_TRANSPORT_SECRET => function (ContainerInterface $c) { return new RedisTransport( $c->get(CONTAINER_REDIS_STREAM_CONNECTION_SECRET), $c->get(CONTAINER_SERIALIZER)) ; }, CONTAINER_REDIS_TRANSPORT_LOG => function (ContainerInterface $c) { return new RedisTransport( $c->get(CONTAINER_REDIS_STREAM_CONNECTION_LOG), $c->get(CONTAINER_SERIALIZER)) ; }, CONTAINER_REDIS_STREAM_RECEIVER_SECRET => function (ContainerInterface $c) { return new RedisReceiver( $c->get(CONTAINER_REDIS_STREAM_CONNECTION_SECRET), $c->get(CONTAINER_SERIALIZER) ); }, CONTAINER_REDIS_STREAM_RECEIVER_LOG => function (ContainerInterface $c) { return new RedisReceiver( $c->get(CONTAINER_REDIS_STREAM_CONNECTION_LOG), $c->get(CONTAINER_SERIALIZER) ); }, CONTAINER_REDIS_STREAM_BUS => function (ContainerInterface $c) { $sendersLocator = new SendersLocator([ \App\Messages\SecretJsonMessages::class => [CONTAINER_REDIS_TRANSPORT_SECRET], \App\Messages\DaemonLogMessage::class => [CONTAINER_REDIS_TRANSPORT_LOG], ], $c); $middleware[] = new SendMessageMiddleware($sendersLocator); return new MessageBus($middleware); }, CONTAINER_REDIS_STREAM_CONNECTION_SECRET => function (ContainerInterface $c) { $host = 'bu-02-redis'; $port = 6379; $dsn = "redis://$host:$port"; $options = [ 'stream' => 'secret', 'group' => 'default', 'consumer' => 'default', ]; return Connection::fromDsn($dsn, $options); }, CONTAINER_REDIS_STREAM_CONNECTION_LOG => function (ContainerInterface $c) { $host = 'bu-02-redis'; $port = 6379; $dsn = "redis://$host:$port"; $options = [ 'stream' => 'log', 'group' => 'default', 'consumer' => 'default', ]; return Connection::fromDsn($dsn, $options); }, 

On peut voir que dans SendersLocator, nous avons attribué un «transport» différent pour deux messages différents, chacun ayant sa propre connexion aux flux correspondants.

J'ai fait un projet de démonstration séparé démontrant une application de trois démons communiquant entre eux à l'aide d'un tel bus: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus .

Mais je vais vous montrer comment organiser un consommateur:

 use App\Messages\DaemonLogMessage; use Symfony\Component\Messenger\Handler\HandlerDescriptor; use Symfony\Component\Messenger\Handler\HandlersLocator; use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware; use Symfony\Component\Messenger\Middleware\SendMessageMiddleware; use Symfony\Component\Messenger\Transport\Sender\SendersLocator; require_once __DIR__ . '/../vendor/autoload.php'; /** @var \Psr\Container\ContainerInterface $container */ $container = require_once('config/container.php'); $handlers = [ DaemonLogMessage::class => [ new HandlerDescriptor( function (DaemonLogMessage $m) { \error_log('DaemonLogHandler: message handled: / ' . $m->getMessage()); }, ['from_transport' => CONTAINER_REDIS_TRANSPORT_LOG] ) ], ]; $middleware = []; $middleware[] = new HandleMessageMiddleware(new HandlersLocator($handlers)); $sendersLocator = new SendersLocator(['*' => [CONTAINER_REDIS_TRANSPORT_LOG]], $container); $middleware[] = new SendMessageMiddleware($sendersLocator); $bus = new MessageBus($middleware); $receivers = [ CONTAINER_REDIS_TRANSPORT_LOG => $container->get(CONTAINER_REDIS_STREAM_RECEIVER_LOG), ]; $w = new \Symfony\Component\Messenger\Worker($receivers, $bus, $container->get(CONTAINER_EVENT_DISPATCHER)); $w->run(); 

Utilisation de cette infrastructure dans une application


Après avoir implémenté le bus dans mon backend, j'ai sélectionné des étapes individuelles de l'ancienne équipe synchrone et créé des gestionnaires distincts, chacun étant engagé dans sa propre entreprise.

Le pipeline pour ajouter un nouveau site à la base de données est le suivant:



Et juste après cela, il est devenu beaucoup plus facile pour moi d'ajouter de nouvelles fonctionnalités, par exemple, l'extraction et l'analyse de Rss. Parce que Étant donné que ce processus nécessite également du contenu source, le gestionnaire-extracteur du lien rss, ainsi que WebsiteIndexHistoryPersistor, s'abonne au message "Content / HtmlContent", le traite et transmet le message souhaité sur son pipeline.



En fin de compte, il s'est avéré que plusieurs démons, chacun ne détenant des connexions qu'aux ressources nécessaires. Par exemple, le démon des robots d'indexation contient tous les gestionnaires qui nécessitent d'aller sur Internet pour le contenu, et le démon persistant conserve une connexion à la base de données.

Maintenant, au lieu de sélectionner dans la base de données, l'ID requis après avoir été inséré par la persistance est simplement transmis via le bus à tous les gestionnaires intéressés.

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


All Articles