Übertragen eines PHP-Backends auf den Redis-Streams-Bus und Auswählen einer von den Frameworks unabhängigen Bibliothek



Vorwort


Auf meiner Website, die ich als Hobby verwende, werden interessante Homepages und persönliche Websites gespeichert. Dieses Thema begann mich zu Beginn meines Programmierens zu interessieren. In diesem Moment freute ich mich, großartige Fachleute zu finden, die über sich selbst, ihre Hobbys und Projekte schreiben. Die Angewohnheit, sie zu entdecken, ist fürs Erste geblieben: Auf fast jeder kommerziellen und nicht sehr Website schaue ich weiterhin in die Fußzeile, um nach Links zu Autoren zu suchen.

Umsetzung der Idee


Die erste Version war nur eine HTML-Seite auf meiner persönlichen Website, auf der ich Links mit Signaturen in eine UL-Liste einfügte. Nachdem ich einige Zeit 20 Seiten getippt hatte, kam ich zu dem Schluss, dass dies nicht sehr effektiv war, und entschied mich, den Prozess zu automatisieren. Beim Stackoverflow ist mir aufgefallen, dass viele Websites in ihren Profilen angeben, und so habe ich einen PHP-Parser geschrieben, der die Profile ab dem ersten (Adresse auf SO und bis heute diese Art: `/ users / 1`) und extrahierten Links durchging vom gewünschten Tag und in SQLite gestapelt.

Dies kann als zweite Version bezeichnet werden: eine Sammlung von Zehntausenden von URLs in einer SQLite-Platte, die die statische Liste in HTML ersetzt hat. Ich habe in dieser Liste eine einfache Suche durchgeführt. Weil es gab nur urls, dann war die suche nur für sie.

In diesem Stadium habe ich das Projekt aufgegeben und bin nach langer Zeit wieder dorthin zurückgekehrt. Zu diesem Zeitpunkt war meine Berufserfahrung bereits mehr als drei Jahre und ich hatte das Gefühl, dass ich etwas Ernsthafteres tun könnte. Darüber hinaus bestand ein großer Wunsch, relativ neue Technologien für sich zu beherrschen.

Moderne Version


Das Projekt wurde in Docker implementiert, die Datenbank wurde auf MongoDb übertragen und vor relativ kurzer Zeit wurden Radieschen hinzugefügt, die zunächst nur zum Zwischenspeichern gedacht waren. Als Basis wird einer der PHP-Microframes verwendet.

Das problem


Neue Sites werden mit einem Konsolenbefehl hinzugefügt, der synchron Folgendes ausführt:

  • Herunterladen von Inhalten per URL
  • Gibt an, ob HTTPS verfügbar war
  • Bewahrt die Essenz der Website
  • Quell-HTML und Header werden im Indexverlauf gespeichert
  • Analysiert Inhalte, ruft Titel und Beschreibung ab
  • Speichert Daten in einer separaten Sammlung.

Dies reichte aus, um Websites zu speichern und in einer Liste anzuzeigen:



Aber die Idee, alles automatisch zu indizieren, zu kategorisieren und zu ordnen, alles auf dem neuesten Stand zu halten, passt nur schwach zu diesem Paradigma. Schon das Hinzufügen einer Webmethode zum Hinzufügen von Seiten erforderte eine Codeduplizierung und Blockierung, um potenzielle DDoS zu vermeiden.

Im Allgemeinen kann natürlich alles synchron ausgeführt werden. Speichern Sie in der Webmethode einfach die URL, um sicherzustellen, dass der monströse Dämon alle Aufgaben für URLs aus der Liste ausführt. Aber trotzdem bittet auch hier das Wort "dran". Wenn die Warteschlange implementiert ist, können alle Aufgaben zumindest asynchron aufgeteilt und ausgeführt werden.

Lösung


Stellen Sie Warteschlangen ein und erstellen Sie ein ereignisgesteuertes Verarbeitungssystem für alle Aufgaben. Und nur für eine lange Zeit wollte ich Redis Streams ausprobieren.

Redis-Streams in PHP verwenden


Weil Ich habe kein Framework der drei Giganten Symfony, Laravel, Yii und möchte eine unabhängige Bibliothek finden. Wie sich jedoch herausstellte (bei der ersten Untersuchung), ist es unmöglich, einzelne seriöse Bibliotheken zu finden. Alles, was mit Warteschlangen zusammenhängt, ist entweder eine Projektion von drei Commits vor fünf Jahren oder an ein Framework gebunden.

Ich habe von Symfony als Anbieter einiger nützlicher Komponenten gehört und benutze bereits einige. Und auch von Laravel kann etwas verwendet werden, zum Beispiel ihr ORM, ohne dass das Framework selbst vorhanden ist.

symfony / messenger


Der erste Kandidat schien sofort ideal, und ohne Zweifel habe ich ihn installiert. Es war jedoch schwieriger, Anwendungsbeispiele außerhalb von Symfony zu googeln. Wie man aus einem Haufen von Klassen mit universellen, nichts sagenden Namen, einem Bus zum Versenden von Nachrichten und sogar auf Redis sammelt?



Die Dokumentation auf der offiziellen Website war recht ausführlich, aber die Initialisierung wurde nur für Symfony beschrieben, wobei deren Lieblings-YML und andere magische Methoden für einen Nicht-Symphoniker verwendet wurden. Ich hatte kein Interesse an der Installation, besonders in den Neujahrsferien. Aber ich musste das für eine unerwartet lange Zeit tun.

Der Versuch, herauszufinden, wie ein System mithilfe von Symfony-Quellen instanziiert werden kann, ist auch bei engen Fristen nicht die einfachste Aufgabe:



Ich stocherte darin herum und versuchte, etwas mit meinen Händen zu tun. Ich kam zu dem Schluss, dass ich eine Art Krücken machte und beschloss, etwas anderes zu versuchen.

beleuchten / Warteschlange


Es stellte sich heraus, dass diese Bibliothek eng mit der Laravel-Infrastruktur und einer Reihe anderer Abhängigkeiten verbunden war, sodass ich nicht viel Zeit darauf verwendet habe: Abhängigkeiten installiert, gesucht, gesehen und gelöscht.

yiisoft / yii2-queue


Naja, hier wurde sofort vom Namen ausgegangen, wieder eine enge Bindung an Yii2. Ich musste diese Bibliothek benutzen und es war nicht schlecht, aber ich dachte nicht, dass es vollständig von Yii2 abhängt.

Der rest


Alles andere, was ich auf dem Github fand, war unzuverlässig veraltete und verlassene Projektionen ohne Sterne, Gabeln und eine große Anzahl von Verpflichtungen.

Zurück zu Symfony / Messenger, technische Details


Ich musste mich mit dieser Bibliothek auseinandersetzen und nachdem ich etwas mehr Zeit verbracht hatte, konnte ich es. Es stellte sich heraus, dass alles sehr kurz und einfach ist. Für die Instantiierung des Busses habe ich eine kleine Fabrik gebaut, weil Ich hatte mehrere Reifen mit unterschiedlichen Handlern.



Nur ein paar Schritte:

  • Erstellen Sie Message-Handler, die nur aufrufbar sein sollen
  • Wrap sie in HandlerDescriptor (Klasse aus der Bibliothek)
  • Wir binden diese "Deskriptoren" in die HandlersLocator-Instanz ein.
  • Fügen Sie der MessageBus-Instanz HandlersLocator hinzu
  • Wir übergeben dem SendersLocator eine Reihe von "SenderInterface", in meinem Fall Instanzen der "RedisTransport" -Klassen, die auf offensichtliche Weise konfiguriert sind
  • Fügen Sie SendersLocator zur MessageBus-Instanz hinzu

MessageBus hat eine Methode `-> dispatch ()`, die im HandlersLocator nach den entsprechenden Handlern sucht und ihnen die Nachricht mit dem entsprechenden `SenderInterface` zum Senden über den Bus übergibt (Redis-Streams).

In der Containerkonfiguration (in diesem Fall php-di) kann diese ganze Gruppe wie folgt konfiguriert werden:

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); }, 

Es ist zu sehen, dass wir in SendersLocator zwei verschiedenen Nachrichten einen unterschiedlichen „Transport“ zugewiesen haben, von denen jede eine eigene Verbindung zu den entsprechenden Streams hat.

Ich habe ein separates Demo-Projekt erstellt, in dem drei Dämonen demonstriert wurden, die über einen solchen Bus miteinander kommunizieren: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus .

Aber ich zeige Ihnen, wie ein Verbraucher vermittelt werden kann:

 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(); 

Verwenden dieser Infrastruktur in einer Anwendung


Nachdem ich den Bus in meinem Backend implementiert hatte, wählte ich einzelne Schritte aus dem alten Synchron-Team aus und stellte separate Handler her, von denen jeder mit seinem eigenen Geschäft beschäftigt ist.

Die Pipeline zum Hinzufügen einer neuen Site zur Datenbank sieht wie folgt aus:



Und gleich danach wurde es für mich viel einfacher, neue Funktionen hinzuzufügen, zum Beispiel das Extrahieren und Parsen von RSS. Weil Da für diesen Prozess auch Quellinhalte erforderlich sind, abonniert der Handler-Extraktor des RSS-Links sowie WebsiteIndexHistoryPersistor die Nachricht "Content / HtmlContent", verarbeitet sie und leitet die gewünschte Nachricht in der Pipeline weiter.



Am Ende stellte sich heraus, dass es mehrere Dämonen gibt, von denen jede nur Verbindungen zu den erforderlichen Ressourcen enthält. Der Crawler- Daemon enthält beispielsweise alle Handler, die eine Verbindung zum Internet für Inhalte benötigen, und der Persister- Daemon stellt eine Verbindung zur Datenbank her.

Anstatt nun aus der Datenbank auszuwählen, wird die erforderliche ID nach dem Einfügen durch den Persister einfach über den Bus an alle interessierten Handler weitergeleitet.

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


All Articles