Overføre PHP-backend til Redis-strømbussen og velge et rammeuavhengig bibliotek

Overføre PHP-backend til Redis-strømbussen og velge et rammeuavhengig bibliotek

Forord

Nettstedet mitt, som jeg driver som hobby, er designet for å være vert for interessante hjemmesider og personlige sider. Dette emnet begynte å interessere meg helt i begynnelsen av min programmeringsreise; i det øyeblikket ble jeg fascinert av å finne flotte fagfolk som skriver om seg selv, hobbyene og prosjektene deres. Vanen med å oppdage dem selv forblir den dag i dag: på nesten alle kommersielle og lite kommersielle nettsteder fortsetter jeg å se i bunnteksten på jakt etter lenker til forfatterne.

Implementering av ideen

Den første versjonen var bare en html-side på min personlige nettside, hvor jeg la inn lenker med signaturer i en ul-liste. Etter å ha skrevet 20 sider over en periode begynte jeg å tenke at dette ikke var særlig effektivt og bestemte meg for å prøve å automatisere prosessen. På stackoverflow la jeg merke til at mange mennesker angir nettsteder i profilene sine, så jeg skrev en parser i php, som ganske enkelt gikk gjennom profilene, og startet med den første (adresser på SO frem til i dag er som dette: `/users/1` ), hentet koblinger fra ønsket tag og la den til i SQLite.

Dette kan kalles den andre versjonen: en samling av titusenvis av URL-er i en SQLite-tabell, som erstattet den statiske listen i html. Jeg gjorde et enkelt søk på denne listen. Fordi det var bare nettadresser, så ble søket ganske enkelt basert på dem.

På dette stadiet forlot jeg prosjektet og kom tilbake til det etter lang tid. På dette stadiet var arbeidserfaringen min allerede mer enn tre år, og jeg følte at jeg kunne gjøre noe mer seriøst. I tillegg var det et stort ønske om å mestre relativt nye teknologier.

Moderne versjon

Prosjekt distribuert i Docker, ble databasen overført til mongoDb, og nylig ble reddik lagt til, som først bare var for caching. Et av PHP-mikrorammene brukes som grunnlag.

problem

Nye nettsteder legges til av en konsollkommando som synkront gjør følgende:

  • Laster ned innhold etter URL
  • Setter et flagg som indikerer om HTTPS var tilgjengelig
  • Bevarer essensen av nettstedet
  • Kilde-HTML og overskrifter lagres i "indekserings"-loggen
  • Analyser innhold, trekker ut tittel og beskrivelse
  • Lagrer data til en egen samling

Dette var nok til å bare lagre nettsteder og vise dem i en liste:

Overføre PHP-backend til Redis-strømbussen og velge et rammeuavhengig bibliotek

Men ideen om automatisk å indeksere, kategorisere og rangere alt, holde alt oppdatert, passet ikke godt inn i dette paradigmet. Selv ganske enkelt å legge til en nettmetode for å legge til sider krevde kodeduplisering og blokkering for å unngå potensiell DDoS.

Generelt kan selvfølgelig alt gjøres synkront, og i webmetoden kan du ganske enkelt lagre URL-en slik at den monstrøse daemonen utfører alle oppgavene for URL-ene fra listen. Men likevel, selv her antyder ordet "kø" seg selv. Og hvis en kø er implementert, kan alle oppgaver deles og utføres minst asynkront.

beslutning

Implementere køer og lage et hendelsesdrevet system for behandling av alle oppgaver. Og jeg har hatt lyst til å prøve Redis Streams i lang tid.

Bruk av Redis-strømmer i PHP

Fordi Siden rammeverket mitt ikke er en av de tre gigantene Symfony, Laravel, Yii, vil jeg gjerne finne et uavhengig bibliotek. Men, som det viste seg (ved første undersøkelse), er det umulig å finne individuelle seriøse biblioteker. Alt knyttet til kø er enten et prosjekt fra 3 forplikter for fem år siden, eller er bundet til rammeverket.

Jeg har hørt mye om Symfony som leverandør av individuelle nyttige komponenter, og jeg bruker allerede noen av dem. Og også noen ting fra Laravel kan også brukes, for eksempel deres ORM, uten tilstedeværelse av selve rammeverket.

symfoni/messenger

Den første kandidaten virket umiddelbart ideell, og uten tvil installerte jeg den. Men det viste seg å være vanskeligere å google eksempler på bruk utenfor Symfony. Hvordan sette sammen en buss for å sende meldinger fra en haug med klasser med universelle, meningsløse navn, og til og med på Redis?

Overføre PHP-backend til Redis-strømbussen og velge et rammeuavhengig bibliotek

Dokumentasjonen på det offisielle nettstedet var ganske detaljert, men initialiseringen ble kun beskrevet for Symfony ved å bruke deres favoritt YML og andre magiske metoder for ikke-symfonisten. Jeg var ikke interessert i selve installasjonsprosessen, spesielt i nyttårsferien. Men jeg måtte gjøre dette i uventet lang tid.

Å prøve å finne ut hvordan man instansierer et system ved å bruke Symfony-kilder er heller ikke den mest trivielle oppgaven for en stram tidsfrist:

Overføre PHP-backend til Redis-strømbussen og velge et rammeuavhengig bibliotek

Etter å ha fordypet meg i alt dette og prøvd å gjøre noe med hendene, kom jeg til den konklusjonen at jeg holdt på med en slags krykker og bestemte meg for å prøve noe annet.

opplyst/kø

Det viste seg at dette biblioteket var tett knyttet til Laravel-infrastrukturen og en haug med andre avhengigheter, så jeg brukte ikke mye tid på det: Jeg installerte det, så på det, så avhengighetene og slettet det.

yiisoft/yii2-kø

Vel, her ble det umiddelbart antatt fra navnet, igjen, en streng forbindelse til Yii2. Jeg måtte bruke dette biblioteket, og det var ikke dårlig, men jeg tenkte ikke på det faktum at det helt avhenger av Yii2.

Resten

Alt annet jeg fant på GitHub var upålitelige, utdaterte og forlatte prosjekter uten stjerner, gafler og et stort antall forpliktelser.

Gå tilbake til symfoni/messenger, tekniske detaljer

Jeg måtte finne ut av dette biblioteket, og etter å ha brukt litt mer tid klarte jeg det. Det viste seg at alt var ganske kortfattet og enkelt. For å instansiere bussen laget jeg en liten fabrikk, fordi... Jeg skulle ha flere dekk og med forskjellige førere.

Overføre PHP-backend til Redis-strømbussen og velge et rammeuavhengig bibliotek

Bare noen få trinn:

  • Vi lager meldingsbehandlere som enkelt skal kunne ringes opp
  • Vi pakker dem inn i HandlerDescriptor (klasse fra biblioteket)
  • Vi pakker disse "Descriptors" i en HandlersLocator-forekomst
  • Legger til HandlersLocator til MessageBus-forekomsten
  • Vi sender et sett med 'SenderInterface' til SendersLocator, i mitt tilfelle tilfeller av 'RedisTransport'-klasser, som er konfigurert på en åpenbar måte
  • Legger til SendersLocator til MessageBus-forekomsten

MessageBus har en `->dispatch()`-metode som slår opp de riktige behandlerne i HandlersLocator og sender meldingen til dem ved å bruke den tilsvarende `SenderInterface` for å sende via bussen (Redis-strømmer).

I containerkonfigurasjonen (i dette tilfellet php-di), kan hele denne pakken konfigureres slik:

        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 forskjellige "transporter" for to forskjellige meldinger, som hver har sin egen tilkobling til de tilsvarende strømmene.

Jeg laget et separat demoprosjekt som demonstrerte en applikasjon av tre demoner som kommuniserer med hverandre ved hjelp av følgende buss: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

Men jeg skal vise deg hvordan en forbruker 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();

Bruk av denne infrastrukturen i en applikasjon

Etter å ha implementert bussen i min backend, skilte jeg individuelle trinn fra den gamle synkrone kommandoen og laget separate behandlere, som hver gjør sine egne ting.

Rørledningen for å legge til et nytt nettsted i databasen så slik ut:

Overføre PHP-backend til Redis-strømbussen og velge et rammeuavhengig bibliotek

Og umiddelbart etter det ble det mye lettere for meg å legge til ny funksjonalitet, for eksempel å trekke ut og analysere Rss. Fordi denne prosessen krever også det originale innholdet, så abonnerer RSS-lenkeekstraktoren, som WebsiteIndexHistoryPersistor, på "Content/HtmlContent"-meldingen, behandler den og sender den ønskede meldingen videre langs sin pipeline.

Overføre PHP-backend til Redis-strømbussen og velge et rammeuavhengig bibliotek

Til slutt endte vi opp med flere demoner, som hver opprettholder forbindelser kun til de nødvendige ressursene. For eksempel en demon crawlere inneholder alle behandlere som krever å gå til Internett for innhold, og daemonen fortsette har en tilkobling til databasen.

Nå, i stedet for å velge fra databasen, blir de nødvendige ID-ene etter innsetting av persister ganske enkelt overført via bussen til alle interesserte behandlere.

Kilde: www.habr.com

Legg til en kommentar