Oversættelse af PHP-backend til Redis streams og valg af en uafhængig biblioteksbibliotek fra rammerne.

Oversættelse af PHP-backend til Redis streams og valg af en uafhængig biblioteksbibliotek fra rammerne.

Forord

Min hjemmeside, som jeg driver som en hobby, er designet til at gemme interessante hjemmesider og personlige sites. Dette emne begyndte at interessere mig i starten af min rejse inden for programmering, på det tidspunkt blev jeg fascineret af at finde store fagfolk, der skriver om sig selv, deres passioner og projekter. Jeg har stadig vanen med at opdage dem: næsten på hver kommerciel og ikke så kommerciel hjemmeside kigger jeg fortsat i bunden efter links til forfatterne.

Implementeringen af idéen

Den første version var blot en html-side på min personlige hjemmeside, hvor jeg samlede links med beskrivelser i en ul-liste. Efter et stykke tid og omkring 20 sider begyndte jeg at tænke, at det ikke var særlig effektivt, og besluttede mig for at prøve at automatisere processen. På StackOverflow bemærkede jeg, at mange angiver hjemmesider i deres profiler, så jeg skrev en parser i PHP, der simpelthen gik gennem profilerne, startende fra den første (linkene til SO til denne dag er af denne type: `\/users\/1`), udtræk links fra den ønskede tag og gemte dem i SQLite.

Dette kan kaldes den anden version: en samling af ti tusind URLs i en SQLite-tabel, der erstattede den statiske liste i html. Baseret på denne liste lavede jeg en simpel søgning. Da der kun var URLs, var søgningen også kun baseret på dem.

På dette tidspunkt lagde jeg projektet på hylden og vendte tilbage til det efter lang tid. På dette tidspunkt havde jeg allerede mere end tre års erfaring, og jeg følte, at jeg kunne lave noget mere seriøst. Desuden var der en stor trang til at lære relativt nye teknologier for mig.

Den moderne version

Projekt er implementeret i Docker, databasen er overført til MongoDB, og for relativt nylig blev Redis tilføjet, som først blev brugt til caching. Som basis anvendes en af PHP's mikro-frameworks.

Problemet

Nye hjemmesider tilføjes med en kommandolinjekommando, der synkront gør følgende:

  • Downloader indholdet fra URL
  • Sætter en flag om, hvorvidt HTTPS var tilgængelig
  • Gemmer entiteten af hjemmesiden
  • Gemmer den originale HTML og overskrifter i historien 'indeksering'
  • Parser indhold, udtrækker Title og Description
  • Data gemmes i en separat samling

Dette var tilstrækkeligt til blot at gemme hjemmesider og vise dem i en liste:

Oversættelse af PHP-backend til Redis streams og valg af en uafhængig biblioteksbibliotek fra rammerne.

Men ideen om at indeksere, kategorisere og rangere alt automatisk, mens man holder det opdateret, passede dårligt ind i denne paradigme. Selv det at tilføje en webmetode til at tilføje sider krævede duplikering af kode og låsning for at undgå potentiel DDoS.

Generelt kan man selvfølgelig også gøre alt synkront og blot i webmetoden gemme URL'en, så det monstrøse daemon kan udføre alle opgaver for URL'erne fra listen. Men selv her er ordet „kø“ nærliggende. Og hvis man implementerer en kø, kan alle opgaver opdeles og udføres i det mindste asynkront.

Løsning

Implementer køer og lav et event-drevet system til behandling af alle opgaver. Jeg har længe ønsket at prøve Redis Streams.

Brug af Redis streams i PHP

Da mit framework ikke er blandt de tre kæmper Symfony, Laravel, Yii, ville jeg gerne finde et uafhængigt bibliotek. Men som det viste sig (ved første gennemgang) er det umuligt at finde separate seriøse biblioteker. Alt relateret til køer er enten et lille projekt med 3 commits fra fem år siden eller bundet til et framework.

Jeg har hørt om Symfony som en leverandør af separate nyttige komponenter, og nogle af dem bruger jeg allerede. Derudover kan man også bruge nogle ting fra Laravel, f.eks. deres ORM, uden at have hele frameworket.

symfony/messenger

Den første kandidat virkede straks perfekt, og uden tvivl installerede jeg den. Men det viste sig at finde eksempler på brug uden Symfony var sværere. Hvordan man samler en bus til at sende meddelelser fra en bunke klasser med universelle, ikke sigende navne, og samtidig på Redis?

Oversættelse af PHP-backend til Redis streams og valg af en uafhængig biblioteksbibliotek fra rammerne.

Dokumentationen på den officielle hjemmeside var ret detaljeret, men initialisering blev kun beskrevet for Symfony ved hjælp af deres foretrukne YML og andre magiske metoder for ikke-Symfonist. Jeg havde ikke meget interesse i selve installationsprocessen, især ikke i nytårsferien. Men jeg måtte gribe ind og uventet tage lang tid.

At forsøge at forstå instansiering af systemet baseret på Symfony-kilderne er heller ikke en triviel opgave under pressede deadlines:

Oversættelse af PHP-backend til Redis streams og valg af en uafhængig biblioteksbibliotek fra rammerne.

Efter at have rodede rundt med det hele og forsøgt at gøre noget selv, kom jeg frem til, at jeg stod med nogle løsninger, og besluttede at prøve noget andet.

illuminate/queue

Det viste sig, at dette bibliotek er nært knyttet til Laravel-infrastrukturen og en masse andre afhængigheder, så jeg brugte ikke meget tid på det: jeg installerede det, kiggede på det, så afhængighederne og fjernede det.

yiisoft/yii2-queue

Her antog man straks fra navnet, at det igen var stærkt bundet til Yii2. Jeg har brugt dette bibliotek, og det var OK, men jeg havde ikke tænkt over, at det er helt afhængigt af Yii2.

Andre

Alt andet, jeg fandt på GitHub, var usikre, forældede og forladte projekter uden stjerner, forks og mange commits.

Tilbage til symfony/messenger, tekniske detaljer

Jeg måtte forstå dette bibliotek, og efter at have brugt lidt mere tid kunne jeg. Det viste sig, at alt er ret klart og enkelt. For at instantiere bussen lavede jeg en lille fabrik, da jeg havde planer om at have flere busser med forskellige behandler.

Oversættelse af PHP-backend til Redis streams og valg af en uafhængig biblioteksbibliotek fra rammerne.

Kun få skridt:

  • Vi opretter beskedbehandlere, som blot skal være callable
  • Vi pakker dem ind i HandlerDescriptor (en klasse fra biblioteket)
  • Disse 'deskriptorer' pakker vi ind i en instans af HandlersLocator
  • Vi tilføjer HandlersLocator til en instans af MessageBus
  • Vi sender et sæt af `SenderInterface` til SendersLocator, i mit tilfælde instanser af klasserne `RedisTransport`, som konfigureres på åbenlys måde
  • Vi tilføjer SendersLocator til en instans af MessageBus

MessageBus har en metode `->dispatch()`, der søger de relevante behandlere i HandlersLocator og sender beskeden til dem ved at bruge den relevante `SenderInterface` til at sende via bussen (Redis streams).

I konfigurationen af containeren (i dette tilfælde php-di) kan hele denne kobling konfigureres således:

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

Her kan vi se, at vi i SendersLocator har tildelt en forskellig "transport" til to forskellige beskeder, hver med sin egen forbindelse til de tilsvarende streams.

Jeg har lavet et separat demo-projekt, der demonstrerer en applikation bestående af tre dæmoner, der kommunikerer med hinanden via et sådant bus-system: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

Men jeg vil vise, hvordan en consumer kan være konstrueret:

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

Brug af denne infrastruktur i applikationen

Når jeg har implementeret bus-systemet i min backend, har jeg isoleret separate trin fra den gamle synkrone kommando og lavet separate håndterere, hvor hver enkelt klarer sine egne opgaver.

Pipeline til tilføjelse af et nyt websted i databasen ser således ud:

Oversættelse af PHP-backend til Redis streams og valg af en uafhængig biblioteksbibliotek fra rammerne.

Og straks herefter blev det meget lettere for mig at tilføje ny funktionalitet, såsom at udtrække og analysere RSS. Da denne proces også kræver det oprindelige indhold, tilmelder RSS-linkudtrækkeren sig lige som WebsiteIndexHistoryPersistor på beskeden "Content/HtmlContent", behandler den denne og videresender den nødvendige besked i sin pipeline.

Oversættelse af PHP-backend til Redis streams og valg af en uafhængig biblioteksbibliotek fra rammerne.

I sidste ende fik jeg flere daemoner, hvor hver enkelt har forbindelser kun til de nødvendige ressourcer. For eksempel daemonen crawlers indeholder alle håndterere, der kræver internetadgang for at hente indhold, mens daemonen persister holder forbindelsen til databasen.

Nu, i stedet for at lave select-forespørgsler fra databasen, sendes de nødvendige id'er efter indsat midler hurtigt gennem bus-systemet til alle interesserede håndterere.

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster