Prevajanje PHP backend-a na Redis streams in izbira neodvisne od ogrodij knjižnice

Prevajanje PHP backend-a na Redis streams in izbira neodvisne od ogrodij knjižnice

Predgovor

Moja spletna stran, ki jo vodim kot hobi, je namenjena shranjevanju zanimivih domačih strani in osebnih spletnih mest. Ta tema me je začela zanimati na začetku moje poti v programiranju, ko me je navduševala iskanje velikih profesionalcev, ki pišejo o sebi, svojih zanimanjih in projektih. Navada, da jih odkrijem, je ostala tudi zdaj: skoraj na vsakem komercialnem in manj komercialnem spletnem mestu še vedno preverjam nogo strani v iskanju povezav do avtorjev.

Izvedba ideje

Prva različica je bila preprosta html-stran na mojem osebnem spletnem mestu, kjer sem shranjeval povezave s podpisi v ul-seznam. Po nabiranju približno 20 strani sem začel razmišljati, da to ni najbolj učinkovito, zato sem se odločil poskusiti avtomatizirati proces. Na stackoverflow sem opazil, da mnogi v svojih profilih navajajo spletna mesta, zato sem napisal parser v PHP, ki je preprosto šel skozi profile, začenši s prvim (naslovi na SO so še vedno takšnega videza: `\/users\/1`), izvlekel povezave iz potrebenega taga in jih shranil v SQLite.

To lahko imenujemo druga različica: zbirka več deset tisoč URL-jev v SQLite tabeli, ki je nadomestila statični seznam v html. Na podlagi tega seznama sem naredil preprost iskalnik. Ker so bile samo URL povezave, je bilo iskanje enostavno po njih.

V tej fazi sem projekt opustil in se mu vrnil po dolgem času. V tej fazi je imela moja delovna izkušnja že več kot tri leta in čutil sem, da lahko naredim nekaj resnejšega. Poleg tega sem imel veliko željo usvojiti razmeroma nove tehnologije.

Sodobna različica

Projekt je razvrščena v Dockerju, baza je prenesena na MongoDB, in relativno nedavno je bil dodan Redis, ki je najprej služil zgolj za predpomnjenje. Kot podlaga se uporablja en izmed mikro okvirjev PHP.

Težava

Novi spletni mesta se dodajajo z ukazno vrstico, ki sinhronizirano izvaja naslednje:

  • Prenese vsebino po URL-ju
  • Nastavi zastavico o tem, ali je bil HTTPS dostopen
  • Shrani entiteto spletne strani
  • Izvorni HTML in glave se shranijo v zgodovino 'indeksiranja'
  • Parsira vsebino, izvleče Title in Description
  • Podatke shrani v ločeno zbirko

To je bilo dovolj, da sem preprosto shranil spletne strani in jih prikazal v seznamu:

Prevajanje PHP backend-a na Redis streams in izbira neodvisne od ogrodij knjižnice

Ideja avtomatske indeksacije, kategorizacije in razvrščanja ter ohranjanja vsebin posodobljenih se je slabo prilegala tej paradigmi. Tudi preprosto dodajanje spletne metode za dodajanje strani je zahtevalo podvajanje kode in blokade za preprečevanje morebitnega DDoS-a.

Seveda je vse mogoče izvajati tudi sinhrono, v spletni metodi pa preprosto shraniti URL, da monster demon izvede vse naloge za URL-e s seznama. Kljub temu se tudi tukaj vsiljuje beseda „vrsta“. Če bi uvedli vrsto, bi lahko vse naloge razdelili in jih izvajali vsaj asinhrono.

Solution

Uvesti vrste in ustvariti sistem obdelave nalog, ki temelji na dogodkih. Že dolgo sem si želel preizkusiti Redis Streams.

Uporaba Redis streams v PHP

Ker moja ogrodja ni v trojici velikanov Symfony, Laravel, Yii, bi želel poiskati neodvisno knjižnico. Vendar se je izkazalo, da (ob prvem pregledu) ločenih resnih knjižnic skoraj ni mogoče najti. Vse, kar je povezano z vrstami, je ali projekt s tremi commit-ji starih pet let ali pa je vezano na ogrodje.

Slišal sem o Symfony kot o ponudniku posameznih uporabnih komponent, nekatere pa že uporabljam. Tudi iz Laravela se lahko uporabi kaj, na primer njihov ORM, brez prisotnosti samega ogrodja.

symfony/messenger

Prvi kandidat se mi je takoj zdel popoln in brez dvoma sem ga namestil. Vendar je bilo težje najti primere uporabe zunaj Symfony. Kako zgraditi iz kupa razredov z univerzalnimi, nejasnimi imeni, bus za prenos sporočil, še na Redis?

Prevajanje PHP backend-a na Redis streams in izbira neodvisne od ogrodij knjižnice

Dokumentacija na uradni spletni strani je bila dovolj podrobna, vendar je bila inicializacija opisana samo za Symfony z njihovim priljubljenim YML in drugimi magičnimi metodami za ne-symfonista. Bila sem brez interesa za sam proces namestitve, še posebej med novoletnimi počitnicami. A sem se moral s tem ukvarjati in presenetljivo dolgo.

Poskus razumeti instanciranje sistema na podlagi izvornih kod Symfony ni trivialna naloga za stisnjene roke:

Prevajanje PHP backend-a na Redis streams in izbira neodvisne od ogrodij knjižnice

Potem ko sem se tukaj malo prekopal in poskušal nekaj narediti ročno, sem prišel do ugotovitve, da delam neki improviziran pristop in sem se odločil poskusiti nekaj drugega.

illuminate/queue

Izkazalo se je, da je ta knjižnica trdno povezana z infrastrukturo Laravel in številnimi drugimi odvisnostmi, zato sem ji posvetil malo časa: namestil sem jo, pogledal, videl odvisnosti in jo odstranil.

yiisoft/yii2-queue

Tukaj je že bilo domnevno iz imena, znova trda vezava na Yii2. S to knjižnico sem se moral ukvarjati in bila je kar dobra, vendar o tem, da je popolnoma odvisna od Yii2, nisem razmišljal.

Ostale

Vse drugo, kar sem našel na GitHubu — zanesljivi zastareli in opuščeni projekti brez zvezd, forkov in velikega števila commitov.

Povratak k symfony/messenger, tehnični podrobnosti

Moral sem se poglobiti v to knjižnico in po nekaj dodatnem času mi je uspelo. Izkazalo se je, da je vse dovolj jasno in preprosto. Za instanciranje vodilne naprave sem naredil majhno tovarno, ker je bilo predvideno več vodil s različnimi obdelovalci.

Prevajanje PHP backend-a na Redis streams in izbira neodvisne od ogrodij knjižnice

Samo nekaj korakov:

  • Ustvarimo obdelovalce sporočil, ki morajo biti preprosto klicni
  • Ovijemo jih v HandlerDescriptor (razred iz knjižnice)
  • Te 'Descriptorje' ovijemo v instanco HandlersLocator
  • Dodamo HandlersLocator v instanco MessageBus
  • Predamo v SendersLocator niz `SenderInterface`, v mojem primeru instance razredov `RedisTransport`, ki se konfigurirajo na očiten način
  • Dodamo SendersLocator v instanco MessageBus

MessageBus ima metodo `->dispatch()`, ki išče ustrezne obdelovalce v HandlersLocator in sporočilo posreduje njim, pri čemer uporablja ustrezne `SenderInterface` za pošiljanje skozi vodilno napravo (Redis streams).

V konfiguraciji kontejnerja (v tem primeru php-di) se lahko celotna ta povezava konfigurira tako:

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

Tuk vidno, što v SendersLocator dlya dvukh raznykh soobshcheniy my prisoedinyli raznyy «transport», kazhdyy iz kotorikh imayet svoy konnekt na sootvetstvuyushchie strimy.

Ya sdelal otdel'nyy demo-proyekt, demonstriruyushchiy prilozhenie iz tryokh demonov, obshchayushchikhsya mezhdu soboy s pomoshch'yu takoy shy. https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

No pokazhuy kak mozhet byt' ustroeny konsyumer:

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

Uporaba te infrastrukture v aplikaciji

Po implementaciji vodila v svojem backendu sem ločil posamezne korake iz stare sinhrone komande in naredil ločene obdelovalce, od katerih vsak obravnava svojo nalogo.

Pipeline za dodajanje novega spletnega mesta v bazo podatkov je izgledal takole:

Prevajanje PHP backend-a na Redis streams in izbira neodvisne od ogrodij knjižnice

In takoj po tem mi je bilo veliko lažje dodajati novo funkcionalnost, na primer, pridobivanje in obdelavo Rss. Ker ta postopek prav tako zahteva izvorno vsebino, se obdelovalec, ki pridobiva povezave na rss, prav tako kot WebsiteIndexHistoryPersistor, naroči na sporočilo 'Content/HtmlContent', ga obdela in razpošlje ustrezno sporočilo naprej po svojem pipeline-u.

Prevajanje PHP backend-a na Redis streams in izbira neodvisne od ogrodij knjižnice

Na koncu je nastalo več demonov, od katerih vsak vzdržuje povezave le do potrebnih virov. Na primer, demon crawlers vsebuje vse obdelovalce, ki zahtevajo obisk interneta za vsebino, medtem ko demon persister vzdržuje povezavo do baze podatkov.

Zdaj namesto poizvedb iz baze podatkov potrebni id-ji po vstavljanju od persisterja preprosto preidejo preko vodila do vseh zainteresiranih obdelovalcev.

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster