Prenos zaledja PHP na vodilo tokov Redis in izbira knjižnice, neodvisne od ogrodja

Prenos zaledja PHP na vodilo tokov Redis in izbira knjižnice, neodvisne od ogrodja

Predgovor

Moje spletno mesto, ki ga vodim kot hobi, je zasnovano tako, da gosti zanimive domače strani in osebna spletna mesta. Ta tema me je začela zanimati že na začetku moje programerske poti, takrat me je fasciniralo iskanje odličnih strokovnjakov, ki pišejo o sebi, svojih hobijih in projektih. Navada, da jih odkrivam zase, ostaja do danes: na skoraj vsakem komercialnem in ne zelo komercialnem spletnem mestu še naprej gledam v nogo v iskanju povezav do avtorjev.

Izvedba ideje

Prva različica je bila le html stran na mojem osebnem spletnem mestu, kjer sem povezave s podpisi dal na seznam ul. Ko sem v določenem časovnem obdobju natipkal 20 strani, sem začel razmišljati, da to ni zelo učinkovito, in sem se odločil poskusiti postopek avtomatizirati. Na stackoverflowu sem opazil, da veliko ljudi označuje mesta v svojih profilih, zato sem napisal razčlenjevalec v php, ki je preprosto šel skozi profile, začenši s prvim (naslovi na SO do danes so takšni: `/users/1` ), ekstrahirali povezave iz želene oznake in jo dodali v SQLite.

To lahko imenujemo druga različica: zbirka več deset tisoč URL-jev v tabeli SQLite, ki je nadomestila statični seznam v html. Preprosto sem iskal na tem seznamu. Ker obstajali so samo URL-ji, potem je iskanje preprosto temeljilo na njih.

Na tej stopnji sem projekt opustil in se po dolgem času vrnil k njemu. V tej fazi so bile moje delovne izkušnje že več kot tri leta in začutila sem, da bi lahko delala kaj resnejšega. Poleg tega je obstajala velika želja po obvladovanju relativno novih tehnologij.

Moderna različica

Projekt nameščen v Dockerju, baza podatkov je bila prenesena v mongoDb, pred kratkim pa je bil dodan radish, ki je bil sprva samo za predpomnjenje. Za osnovo je uporabljeno eno od mikroogrodij PHP.

problem

Nova spletna mesta se dodajo z ukazom konzole, ki sinhrono naredi naslednje:

  • Prenaša vsebino po URL-ju
  • Nastavi zastavico, ki označuje, ali je bil HTTPS na voljo
  • Ohranja bistvo spletne strani
  • Izvorni HTML in glave se shranijo v zgodovino »indeksiranja«.
  • Razčleni vsebino, izvleče naslov in opis
  • Podatke shrani v ločeno zbirko

To je bilo dovolj za preprosto shranjevanje spletnih mest in njihov prikaz na seznamu:

Prenos zaledja PHP na vodilo tokov Redis in izbira knjižnice, neodvisne od ogrodja

Toda zamisel o samodejnem indeksiranju, kategoriziranju in razvrščanju vsega, ohranjanju vsega posodobljenega, se ni dobro ujemala s to paradigmo. Tudi preprosto dodajanje spletne metode za dodajanje strani je zahtevalo podvajanje kode in blokiranje, da bi se izognili morebitnemu DDoS-u.

Na splošno je seveda vse mogoče narediti sinhrono, v spletni metodi pa lahko preprosto shranite URL, tako da pošastni demon izvaja vse naloge za URL-je s seznama. A vseeno, tudi tukaj beseda "čakalna vrsta" nakazuje sama od sebe. In če je implementirana čakalna vrsta, je mogoče vse naloge razdeliti in izvajati vsaj asinhrono.

odločitev

Izvedite čakalne vrste in ustvarite sistem, ki temelji na dogodkih za obdelavo vseh nalog. Že dolgo sem si želel preizkusiti Redis Streams.

Uporaba tokov Redis v PHP

Ker Ker moj okvir ni eden od treh velikanov Symfony, Laravel, Yii, bi rad našel neodvisno knjižnico. Toda, kot se je izkazalo (pri prvem pregledu), je nemogoče najti posamezne resne knjižnice. Vse, kar je povezano s čakalnimi vrstami, je bodisi projekt iz 3 commitov pred petimi leti ali pa je vezano na okvir.

O Symfonyju kot dobavitelju posameznih uporabnih komponent sem že veliko slišal in nekatere od njih že uporabljam. In tudi nekatere stvari iz Laravela je mogoče uporabiti, na primer njihov ORM, brez prisotnosti samega ogrodja.

symfony/messenger

Prvi kandidat se je takoj zdel idealen in brez dvoma sem ga namestil. Izkazalo pa se je, da je težje poguglati primere uporabe zunaj Symfonyja. Kako sestaviti vodilo za posredovanje sporočil iz kopice razredov z univerzalnimi, nesmiselnimi imeni in še to na Redisu?

Prenos zaledja PHP na vodilo tokov Redis in izbira knjižnice, neodvisne od ogrodja

Dokumentacija na uradni strani je bila precej podrobna, vendar je bila inicializacija opisana le za Symfony z uporabo njihovega najljubšega YML in drugih čarobnih metod za ne-simfonike. Sam postopek namestitve me ni zanimal, sploh med novoletnimi prazniki. Toda to sem moral početi nepričakovano dolgo.

Poskus ugotoviti, kako instancirati sistem z uporabo virov Symfony, tudi ni najbolj trivialna naloga za kratek rok:

Prenos zaledja PHP na vodilo tokov Redis in izbira knjižnice, neodvisne od ogrodja

Ko sem se poglobil v vse to in poskušal narediti nekaj z rokami, sem prišel do zaključka, da delam neke vrste bergle in se odločil poskusiti nekaj drugega.

osvetljen/čakalna vrsta

Izkazalo se je, da je bila ta knjižnica tesno povezana z infrastrukturo Laravel in kupom drugih odvisnosti, zato ji nisem namenil veliko časa: namestil sem jo, pogledal, videl odvisnosti in izbrisal.

yiisoft/yii2-čakalna vrsta

No, tukaj je bilo takoj domnevano iz imena, spet, stroga povezava z Yii2. Moral sem uporabiti to knjižnico in ni bilo slabo, vendar nisem pomislil na dejstvo, da je popolnoma odvisna od Yii2.

Ostalo

Vse ostalo, kar sem našel na GitHubu, so nezanesljivi, zastareli in opuščeni projekti brez zvezdic, forkov in velikega števila komitov.

Nazaj na symfony/messenger, tehnične podrobnosti

Moral sem ugotoviti to knjižnico in po nekaj več časa mi je uspelo. Izkazalo se je, da je vse precej jedrnato in preprosto. Da bi ustvaril primerek avtobusa, sem naredil majhno tovarno, ker ... Imel naj bi več gum in z različnimi ročaji.

Prenos zaledja PHP na vodilo tokov Redis in izbira knjižnice, neodvisne od ogrodja

Samo nekaj korakov:

  • Ustvarjamo upravljalnike sporočil, ki jih je treba preprosto priklicati
  • Zavijemo jih v HandlerDescriptor (razred iz knjižnice)
  • Te »deskriptorje« zavijemo v primerek HandlersLocator
  • Dodajanje HandlersLocator v primerek MessageBus
  • SendersLocatorju posredujemo niz `SenderInterface`, v mojem primeru primerke razredov `RedisTransport`, ki so konfigurirani na očiten način
  • Dodajanje SendersLocator v primerek MessageBus

MessageBus ima metodo `->dispatch()`, ki poišče ustrezne obdelovalce v HandlersLocator in jim posreduje sporočilo z uporabo ustreznega `SenderInterface` za pošiljanje prek vodila (pretoki Redis).

V konfiguraciji vsebnika (v tem primeru php-di) lahko celoten sveženj konfigurirate takole:

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

Tukaj lahko vidite, da smo v SendersLocatorju dodelili različne »transporte« za dve različni sporočili, od katerih ima vsako svojo povezavo z ustreznimi tokovi.

Naredil sem ločen demo projekt, ki prikazuje aplikacijo treh demonov, ki komunicirajo med seboj z uporabo naslednjega vodila: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

Vendar vam bom pokazal, kako je lahko strukturiran potrošnik:

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

Ko sem vodilo implementiral v svoj backend, sem iz starega sinhronskega ukaza ločil posamezne stopnje in naredil ločene handlerje, od katerih vsak dela svoje.

Cevovod za dodajanje novega mesta v bazo podatkov je bil videti takole:

Prenos zaledja PHP na vodilo tokov Redis in izbira knjižnice, neodvisne od ogrodja

In takoj za tem mi je postalo veliko lažje dodati nove funkcije, na primer ekstrahiranje in razčlenjevanje Rss. Ker ta postopek zahteva tudi izvirno vsebino, nato pa se upravljalnik za ekstrakcijo povezav RSS, kot je WebsiteIndexHistoryPersistor, naroči na sporočilo »Content/HtmlContent«, ga obdela in posreduje želeno sporočilo naprej po svojem cevovodu.

Prenos zaledja PHP na vodilo tokov Redis in izbira knjižnice, neodvisne od ogrodja

Na koncu smo imeli več demonov, od katerih vsak vzdržuje povezave samo s potrebnimi viri. Na primer demon pajki vsebuje vse upravljalnike, ki zahtevajo dostop do interneta za vsebino, in demon vztrajati ima povezavo z bazo podatkov.

Zdaj, namesto da bi izbirali iz baze podatkov, se zahtevani ID-ji po vstavitvi s strani vztrajnika preprosto prenesejo prek vodila vsem zainteresiranim upravljavcem.

Vir: www.habr.com

Dodaj komentar