Übertragen des PHP-Backends auf den Redis-Streams-Bus und Auswahl einer Framework-unabhängigen Bibliothek

Übertragen des PHP-Backends auf den Redis-Streams-Bus und Auswahl einer Framework-unabhängigen Bibliothek

Vorwort

Meine Website, die ich als Hobby betreibe, soll interessante Homepages und persönliche Websites hosten. Dieses Thema begann mich gleich zu Beginn meiner Programmierreise zu interessieren; in diesem Moment war ich fasziniert davon, großartige Profis zu finden, die über sich selbst, ihre Hobbys und Projekte schreiben. Die Angewohnheit, sie selbst zu entdecken, ist bis heute geblieben: Auf fast jeder kommerziellen und nicht sehr kommerziellen Seite suche ich weiterhin in der Fußzeile nach Links zu den Autoren.

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 über einen längeren Zeitraum 20 Seiten getippt hatte, begann ich zu denken, dass dies nicht sehr effektiv sei, und beschloss, den Prozess zu automatisieren. Bei Stackoverflow ist mir aufgefallen, dass viele Leute Websites in ihren Profilen angeben, also habe ich einen Parser in PHP geschrieben, der einfach die Profile durchging, beginnend mit dem ersten (Adressen auf SO lauten bis heute so: `/users/1`). ), extrahierte Links aus dem gewünschten Tag und fügte sie in SQLite hinzu.

Dies kann als zweite Version bezeichnet werden: eine Sammlung von Zehntausenden URLs in einer SQLite-Tabelle, die die statische Liste in HTML ersetzt. Ich habe eine einfache Suche in dieser Liste durchgeführt. Weil gab es nur URLs, dann basierte die Suche einfach darauf.

Zu diesem Zeitpunkt habe ich das Projekt aufgegeben und bin nach langer Zeit wieder darauf zurückgekehrt. Zu diesem Zeitpunkt betrug 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 zu beherrschen.

Moderne Version

Projekt Die in Docker bereitgestellte Datenbank wurde nach MongoDb übertragen und in jüngerer Zeit wurde Radish hinzugefügt, das zunächst nur dem Caching diente. Als Basis dient eines der PHP-Microframeworks.

Problem

Neue Sites werden durch einen Konsolenbefehl hinzugefügt, der synchron Folgendes ausführt:

  • Lädt Inhalte per URL herunter
  • Setzt ein Flag, das angibt, ob HTTPS verfügbar war
  • Bewahrt die Essenz der Website
  • Der Quell-HTML-Code und die Header werden im „Indexierungs“-Verlauf gespeichert
  • Analysiert den Inhalt und extrahiert Titel und Beschreibung
  • Speichert Daten in einer separaten Sammlung

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

Übertragen des PHP-Backends auf den Redis-Streams-Bus und Auswahl einer Framework-unabhängigen Bibliothek

Aber die Idee, alles automatisch zu indizieren, zu kategorisieren und zu ordnen und alles auf dem neuesten Stand zu halten, passte nicht gut in dieses Paradigma. Selbst das einfache Hinzufügen einer Webmethode zum Hinzufügen von Seiten erforderte eine Duplizierung und Blockierung des Codes, um potenzielle DDoS-Angriffe zu verhindern.

Im Allgemeinen kann natürlich alles synchron erledigt werden, und in der Web-Methode kann man die URL einfach speichern, sodass der monströse Daemon alle Aufgaben für die URLs aus der Liste ausführt. Aber auch hier drängt sich das Wort „Warteschlange“ auf. Und wenn eine Warteschlange implementiert ist, können alle Aufgaben aufgeteilt und zumindest asynchron ausgeführt werden.

Lösung

Implementieren Sie Warteschlangen und erstellen Sie ein ereignisgesteuertes System zur Verarbeitung aller Aufgaben. Und ich wollte Redis Streams schon lange ausprobieren.

Verwendung von Redis-Streams in PHP

Weil Da mein Framework nicht zu den drei Giganten Symfony, Laravel, Yii gehört, würde ich gerne eine unabhängige Bibliothek finden. Aber wie sich (bei der ersten Prüfung) herausstellte, ist es unmöglich, einzelne seriöse Bibliotheken zu finden. Alles, was mit Warteschlangen zu tun hat, ist entweder ein Projekt aus drei Commits von vor fünf Jahren oder ist an das Framework gebunden.

Ich habe viel über Symfony als Anbieter einzelner nützlicher Komponenten gehört und verwende einige davon bereits. Und auch einige Dinge von Laravel können verwendet werden, beispielsweise deren ORM, ohne dass das Framework selbst vorhanden ist.

Symfony/Messenger

Der erste Kandidat erschien mir sofort ideal und ich habe ihn ohne jeden Zweifel installiert. Es stellte sich jedoch als schwieriger heraus, Anwendungsbeispiele außerhalb von Symfony zu googeln. Wie kann man aus einer Reihe von Klassen mit universellen, bedeutungslosen Namen einen Bus zum Weiterleiten von Nachrichten und sogar auf Redis zusammenstellen?

Übertragen des PHP-Backends auf den Redis-Streams-Bus und Auswahl einer Framework-unabhängigen Bibliothek

Die Dokumentation auf der offiziellen Website war recht detailliert, aber die Initialisierung wurde nur für Symfony unter Verwendung ihres bevorzugten YML und anderer magischer Methoden für Nicht-Symphoniker beschrieben. Der Installationsprozess selbst interessierte mich nicht, insbesondere während der Neujahrsferien. Aber ich musste das unerwartet lange tun.

Der Versuch, herauszufinden, wie man ein System mithilfe von Symfony-Quellen instanziiert, ist angesichts einer engen Frist auch nicht die trivialste Aufgabe:

Übertragen des PHP-Backends auf den Redis-Streams-Bus und Auswahl einer Framework-unabhängigen Bibliothek

Nachdem ich mich mit all dem befasst und versucht hatte, etwas mit meinen Händen zu machen, kam ich zu dem Schluss, dass ich eine Art Krücken machte, und beschloss, etwas anderes auszuprobieren.

beleuchtet/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 damit verbrachte: Ich habe sie installiert, angeschaut, die Abhängigkeiten gesehen und sie gelöscht.

yiisoft/yii2-queue

Nun, hier wurde vom Namen sofort wieder eine enge Verbindung zu Yii2 vermutet. Ich musste diese Bibliothek verwenden und sie war nicht schlecht, aber ich habe nicht darüber nachgedacht, dass sie vollständig von Yii2 abhängt.

Andere

Alles andere, was ich auf GitHub gefunden habe, waren unzuverlässige, veraltete und aufgegebene Projekte ohne Sterne, Forks und eine große Anzahl von Commits.

Zurück zu Symfony/Messenger, technische Details

Ich musste diese Bibliothek herausfinden und nachdem ich etwas mehr Zeit investiert hatte, gelang es mir auch. Es stellte sich heraus, dass alles recht prägnant und einfach war. Um den Bus zu instanziieren, habe ich eine kleine Fabrik erstellt, weil ... Ich sollte mehrere Reifen und unterschiedliche Fahrer haben.

Übertragen des PHP-Backends auf den Redis-Streams-Bus und Auswahl einer Framework-unabhängigen Bibliothek

Nur ein paar Schritte:

  • Wir erstellen Message-Handler, die einfach aufrufbar sein sollen
  • Wir verpacken sie in HandlerDescriptor (Klasse aus der Bibliothek)
  • Wir packen diese „Deskriptoren“ in eine HandlersLocator-Instanz
  • Hinzufügen von HandlersLocator zur MessageBus-Instanz
  • Wir übergeben eine Reihe von „SenderInterface“ an SendersLocator, in meinem Fall Instanzen von „RedisTransport“-Klassen, die auf offensichtliche Weise konfiguriert sind
  • Hinzufügen von SendersLocator zur MessageBus-Instanz

MessageBus verfügt über eine „->dispatch()“-Methode, die die entsprechenden Handler im HandlersLocator sucht und die Nachricht an diese weiterleitet, wobei das entsprechende „SenderInterface“ zum Senden über den Bus (Redis-Streams) verwendet wird.

In der Containerkonfiguration (in diesem Fall php-di) kann dieses gesamte Bundle 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([
                AppMessagesSecretJsonMessages::class => [CONTAINER_REDIS_TRANSPORT_SECRET],
                AppMessagesDaemonLogMessage::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);
        },

Hier sehen Sie, dass wir im SendersLocator zwei verschiedenen Nachrichten unterschiedliche „Transporte“ zugewiesen haben, von denen jede eine eigene Verbindung zu den entsprechenden Streams hat.

Ich habe ein separates Demoprojekt erstellt, das eine Anwendung von drei Daemons demonstriert, die über den folgenden Bus miteinander kommunizieren: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

Aber ich zeige Ihnen, wie ein Verbraucher strukturiert sein kann:

use AppMessagesDaemonLogMessage;
use SymfonyComponentMessengerHandlerHandlerDescriptor;
use SymfonyComponentMessengerHandlerHandlersLocator;
use SymfonyComponentMessengerMessageBus;
use SymfonyComponentMessengerMiddlewareHandleMessageMiddleware;
use SymfonyComponentMessengerMiddlewareSendMessageMiddleware;
use SymfonyComponentMessengerTransportSenderSendersLocator;

require_once __DIR__ . '/../vendor/autoload.php';
/** @var PsrContainerContainerInterface $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 SymfonyComponentMessengerWorker($receivers, $bus, $container->get(CONTAINER_EVENT_DISPATCHER));
$w->run();

Verwendung dieser Infrastruktur in einer Anwendung

Nachdem ich den Bus in meinem Backend implementiert hatte, trennte ich einzelne Stufen vom alten synchronen Befehl und erstellte separate Handler, von denen jeder sein eigenes Ding macht.

Die Pipeline zum Hinzufügen einer neuen Site zur Datenbank sah folgendermaßen aus:

Übertragen des PHP-Backends auf den Redis-Streams-Bus und Auswahl einer Framework-unabhängigen Bibliothek

Und sofort danach wurde es für mich viel einfacher, neue Funktionen hinzuzufügen, zum Beispiel das Extrahieren und Parsen von Rss. Weil Dieser Prozess erfordert auch den Originalinhalt, dann abonniert der RSS-Link-Extraktor-Handler, wie WebsiteIndexHistoryPersistor, die „Content/HtmlContent“-Nachricht, verarbeitet sie und leitet die gewünschte Nachricht entlang seiner Pipeline weiter.

Übertragen des PHP-Backends auf den Redis-Streams-Bus und Auswahl einer Framework-unabhängigen Bibliothek

Am Ende hatten wir mehrere Daemons, von denen jeder nur Verbindungen zu den notwendigen Ressourcen aufrechterhält. Zum Beispiel ein Dämon Crawler enthält alle Handler, die für den Inhalt ins Internet gehen müssen, sowie den Daemon hartnäckig hält eine Verbindung zur Datenbank.

Anstatt nun aus der Datenbank auszuwählen, werden die benötigten IDs nach dem Einfügen durch den Persister einfach über den Bus an alle interessierten Handler übermittelt.

Source: habr.com

Kommentar hinzufügen