Overførsel af PHP-backend til Redis-streams-bussen og valg af et framework-uafhængigt bibliotek

Overførsel af PHP-backend til Redis-streams-bussen og valg af et framework-uafhængigt bibliotek

Forord

Min hjemmeside, som jeg driver som en hobby, er designet til at være vært for interessante hjemmesider og personlige sider. Dette emne begyndte at interessere mig helt i begyndelsen af ​​min programmeringsrejse; i det øjeblik var jeg fascineret af at finde fantastiske fagfolk, der skriver om sig selv, deres hobbyer og projekter. Vanen med at opdage dem for mig selv forbliver den dag i dag: På næsten alle kommercielle og ikke særlig kommercielle websteder fortsætter jeg med at kigge i sidefoden på jagt efter links til forfatterne.

Implementering af idéen

Den første version var blot en html-side på min personlige hjemmeside, hvor jeg lagde links med signaturer ind i en ul-liste. Efter at have skrevet 20 sider over en periode begyndte jeg at tro, at dette ikke var særlig effektivt, og besluttede at forsøge at automatisere processen. På stackoverflow bemærkede jeg, at mange mennesker angiver websteder i deres profiler, så jeg skrev en parser i php, som simpelthen gik gennem profilerne, begyndende med den første (adresser på SO den dag i dag er som denne: `/users/1` ), udtrak links fra det ønskede tag og tilføjede det i SQLite.

Dette kan kaldes den anden version: en samling af titusindvis af URL'er i en SQLite-tabel, som erstattede den statiske liste i HTML. Jeg lavede en simpel søgning på denne liste. Fordi der var kun URL'er, så var søgningen blot baseret på dem.

På dette tidspunkt opgav jeg projektet og vendte tilbage til det efter lang tid. På dette tidspunkt var min erhvervserfaring allerede mere end tre år, og jeg følte, at jeg kunne gøre noget mere seriøst. Derudover var der et stort ønske om at mestre relativt nye teknologier.

Moderne version

Projekt installeret i Docker, blev databasen overført til mongoDb, og for nylig blev der tilføjet radise, som først kun var til caching. Et af PHP-mikrorammerne er brugt som grundlag.

problem

Nye websteder tilføjes af en konsolkommando, der synkront gør følgende:

  • Downloader indhold efter URL
  • Indstiller et flag, der angiver, om HTTPS var tilgængeligt
  • Bevarer essensen af ​​hjemmesiden
  • Kilde-HTML og overskrifter gemmes i "indekserings"-historikken
  • Parser indhold, udtrækker titel og beskrivelse
  • Gemmer data til en separat samling

Dette var nok til blot at gemme websteder og vise dem på en liste:

Overførsel af PHP-backend til Redis-streams-bussen og valg af et framework-uafhængigt bibliotek

Men ideen om automatisk at indeksere, kategorisere og rangere alt, holde alt opdateret, passede ikke godt ind i dette paradigme. Selv blot tilføjelse af en webmetode til at tilføje sider krævede kodeduplikering og blokering for at undgå potentiel DDoS.

Generelt kan alt selvfølgelig gøres synkront, og i webmetoden kan du blot gemme URL'en, så den monstrøse dæmon udfører alle opgaver for URL'erne fra listen. Men alligevel, selv her antyder ordet "kø" sig selv. Og hvis en kø er implementeret, så kan alle opgaver opdeles og udføres i det mindste asynkront.

beslutning

Implementer køer og lav et hændelsesdrevet system til behandling af alle opgaver. Og jeg har længe ønsket at prøve Redis Streams.

Brug af Redis-streams i PHP

Fordi Da mit framework ikke er en af ​​de tre giganter Symfony, Laravel, Yii, vil jeg gerne finde et uafhængigt bibliotek. Men som det viste sig (ved første undersøgelse), er det umuligt at finde individuelle seriøse biblioteker. Alt relateret til køer er enten et projekt fra 3 commits for fem år siden, eller er bundet til rammerne.

Jeg har hørt meget om Symfony som leverandør af individuelle brugbare komponenter, og jeg bruger allerede nogle af dem. Og også nogle ting fra Laravel kan også bruges, for eksempel deres ORM, uden tilstedeværelsen af ​​selve rammen.

symfoni/messenger

Den første kandidat virkede umiddelbart ideel og uden tvivl installerede jeg den. Men det viste sig at være sværere at google eksempler på brug uden for Symfony. Hvordan samles man fra en masse klasser med universelle, meningsløse navne, en bus til at sende beskeder og endda på Redis?

Overførsel af PHP-backend til Redis-streams-bussen og valg af et framework-uafhængigt bibliotek

Dokumentationen på det officielle websted var ret detaljeret, men initialiseringen blev kun beskrevet for Symfony ved hjælp af deres foretrukne YML og andre magiske metoder for ikke-symfonisten. Jeg var ikke interesseret i selve installationsprocessen, især i nytårsferien. Men jeg var nødt til at gøre dette i uventet lang tid.

At prøve at finde ud af, hvordan man instansierer et system ved hjælp af Symfony-kilder er heller ikke den mest trivielle opgave for en stram deadline:

Overførsel af PHP-backend til Redis-streams-bussen og valg af et framework-uafhængigt bibliotek

Efter at have dykket ned i alt dette og prøvet at gøre noget med mine hænder, kom jeg til den konklusion, at jeg lavede en slags krykker og besluttede at prøve noget andet.

oplyst/kø

Det viste sig, at dette bibliotek var tæt 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 slettede det.

yiisoft/yii2-kø

Nå, her blev det straks antaget fra navnet, igen, en streng forbindelse til Yii2. Jeg var nødt til at bruge dette bibliotek, og det var ikke dårligt, men jeg tænkte ikke over, at det helt afhænger af Yii2.

Resten

Alt andet, jeg fandt på GitHub, var upålidelige, forældede og forladte projekter uden stjerner, gafler og et stort antal commits.

Vend tilbage til symfony/messenger, tekniske detaljer

Jeg var nødt til at finde ud af dette bibliotek, og efter at have brugt noget mere tid, var jeg i stand til det. Det viste sig, at alt var ret kortfattet og enkelt. For at instantiere bussen lavede jeg en lille fabrik, fordi... Jeg skulle have flere dæk og med forskellige førere.

Overførsel af PHP-backend til Redis-streams-bussen og valg af et framework-uafhængigt bibliotek

Bare et par trin:

  • Vi opretter meddelelsesbehandlere, der skal kunne ringes op
  • Vi pakker dem ind i HandlerDescriptor (klasse fra biblioteket)
  • Vi indpakker disse "Descriptors" i en HandlersLocator-instans
  • Tilføjelse af HandlersLocator til MessageBus-instansen
  • Vi sender et sæt 'SenderInterface' til SendersLocator, i mit tilfælde tilfælde af 'RedisTransport' klasser, som er konfigureret på en indlysende måde
  • Tilføjelse af SendersLocator til MessageBus-forekomsten

MessageBus har en `->dispatch()`-metode, der slår de relevante handlere op i HandlersLocator og sender beskeden til dem ved at bruge den tilsvarende `SenderInterface` til at sende via bussen (Redis-streams).

I containerkonfigurationen (i dette tilfælde php-di), kan hele denne bundt konfigureres sådan:

        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 du se, at vi i SendersLocator har tildelt forskellige "transporter" til to forskellige beskeder, som hver har sin egen forbindelse til de tilsvarende streams.

Jeg lavede et separat demoprojekt, der demonstrerede en applikation af tre dæmoner, der kommunikerer med hinanden ved hjælp af følgende bus: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

Men jeg vil vise dig, hvordan en forbruger kan struktureres:

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 en applikation

Efter at have implementeret bussen i min backend, adskilte jeg individuelle stadier fra den gamle synkrone kommando og lavede separate handlere, som hver især gør deres egne ting.

Pipelinen til at tilføje et nyt websted til databasen så således ud:

Overførsel af PHP-backend til Redis-streams-bussen og valg af et framework-uafhængigt bibliotek

Og umiddelbart efter det blev det meget nemmere for mig at tilføje ny funktionalitet, for eksempel at udtrække og parse Rss. Fordi denne proces kræver også det originale indhold, så abonnerer RSS-linkudtrækkeren, som WebsiteIndexHistoryPersistor, på "Content/HtmlContent"-meddelelsen, behandler den og sender den ønskede besked videre langs sin pipeline.

Overførsel af PHP-backend til Redis-streams-bussen og valg af et framework-uafhængigt bibliotek

Til sidst endte vi med adskillige dæmoner, som hver kun opretholder forbindelser til de nødvendige ressourcer. For eksempel en dæmon crawlers indeholder alle de handlere, der kræver at gå til internettet efter indhold, og dæmonen vedvarer har en forbindelse til databasen.

Nu, i stedet for at vælge fra databasen, bliver de nødvendige id'er efter indsættelse af persisteren simpelthen transmitteret via bussen til alle interesserede handlere.

Kilde: www.habr.com

Tilføj en kommentar