Prenošenje PHP pozadine na Redis stream sabirnicu i odabir biblioteke nezavisne od okvira

Prenošenje PHP pozadine na Redis stream sabirnicu i odabir biblioteke nezavisne od okvira

Predgovor

Moja web stranica, koju vodim iz hobija, dizajnirana je za smještaj zanimljivih početnih i osobnih stranica. Ova tema me počela zanimati na samom početku mog programskog puta, u tom trenutku sam bio fasciniran pronalaskom velikih profesionalaca koji pišu o sebi, svojim hobijima i projektima. Navika da ih otkrivam za sebe ostala je do danas: na gotovo svim komercijalnim i ne baš komercijalnim stranicama, nastavljam tražiti u podnožju u potrazi za linkovima do autora.

Implementacija ideje

Prva verzija je bila samo html stranica na mojoj ličnoj web stranici, gdje sam stavio linkove sa potpisima u ul listu. Otkucavši 20 stranica tokom određenog vremenskog perioda, počeo sam da mislim da to nije baš efikasno i odlučio sam da pokušam da automatizujem proces. Na stackoverflowu sam primijetio da mnogi ljudi u svojim profilima označavaju stranice, pa sam napisao parser u php-u, koji je jednostavno prošao kroz profile, počevši od prvog (adrese na SO do danas su ovako: `/users/1` ), izdvojio linkove iz željene oznake i dodao je u SQLite.

Ovo se može nazvati drugom verzijom: zbirka od desetina hiljada URL-ova u SQLite tabeli, koja je zamijenila statičku listu u HTML-u. Uradio sam jednostavnu pretragu na ovoj listi. Jer postojali su samo URL-ovi, onda se pretraga jednostavno zasnivala na njima.

U ovoj fazi sam napustio projekat i vratio mu se nakon dužeg vremena. U ovoj fazi, moje radno iskustvo je bilo već više od tri godine i osjetio sam da mogu raditi nešto ozbiljnije. Osim toga, postojala je velika želja za savladavanjem relativno novih tehnologija.

Moderna verzija

Projekat postavljena u Docker-u, baza podataka je prebačena u mongoDb, a nedavno je dodat radish, koji je u početku bio samo za keširanje. Kao osnova se koristi jedan od PHP mikroframeworka.

problem

Nove stranice se dodaju naredbom konzole koja sinhrono radi sljedeće:

  • Preuzimanje sadržaja po URL-u
  • Postavlja oznaku koja pokazuje da li je HTTPS bio dostupan
  • Čuva suštinu web stranice
  • Izvorni HTML i zaglavlja se čuvaju u istoriji "indeksiranja".
  • Parsira sadržaj, izdvaja naslov i opis
  • Sprema podatke u posebnu kolekciju

Ovo je bilo dovoljno da jednostavno pohranite web stranice i prikažete ih na listi:

Prenošenje PHP pozadine na Redis stream sabirnicu i odabir biblioteke nezavisne od okvira

Ali ideja o automatskom indeksiranju, kategorizaciji i rangiranju svega, održavanju svega ažurnim, nije se dobro uklopila u ovu paradigmu. Čak i jednostavno dodavanje web metode za dodavanje stranica zahtijevalo je dupliciranje koda i blokiranje kako bi se izbjegao potencijalni DDoS.

Generalno, naravno, sve se može raditi sinhrono, a u web metodi možete jednostavno sačuvati URL tako da monstruozni demon obavlja sve zadatke za URL-ove sa liste. Ali ipak, i ovdje se sama po sebi sugerira riječ "red". A ako se implementira red čekanja, onda se svi zadaci mogu podijeliti i izvoditi barem asinhrono.

odluka

Implementirajte redove i kreirajte sistem vođen događajima za obradu svih zadataka. I već duže vrijeme želim isprobati Redis Streams.

Korištenje Redis tokova u PHP-u

Jer Pošto moj okvir nije jedan od tri giganta Symfony, Laravel, Yii, želio bih pronaći nezavisnu biblioteku. Ali, kako se pokazalo (na prvom pregledu), nemoguće je pronaći pojedinačne ozbiljne biblioteke. Sve što se tiče redova je ili projekat iz 3 urezivanja prije pet godina, ili je vezano za okvir.

Čuo sam dosta o Symfonyju kao dobavljaču pojedinačnih korisnih komponenti, a neke od njih već koristim. I neke stvari iz Laravel-a se također mogu koristiti, na primjer njihov ORM, bez prisustva samog okvira.

symfony/messenger

Prvi kandidat se odmah učinio idealnim i bez ikakve sumnje sam ga instalirao. Ali pokazalo se da je teže proguglati primjere korištenja izvan Symfonyja. Kako sastaviti iz gomile klasa sa univerzalnim, besmislenim imenima, sabirnicom za prosleđivanje poruka, pa čak i na Redisu?

Prenošenje PHP pozadine na Redis stream sabirnicu i odabir biblioteke nezavisne od okvira

Dokumentacija na službenoj stranici bila je prilično detaljna, ali inicijalizacija je opisana samo za Symfony koristeći njihov omiljeni YML i druge magične metode za nesimfoniste. Sam proces instalacije me nije zanimao, posebno tokom novogodišnjih praznika. Ali morao sam ovo raditi neočekivano dugo.

Pokušaj da se otkrije kako instancirati sistem koristeći Symfony izvore također nije najtrivijalniji zadatak za kratak rok:

Prenošenje PHP pozadine na Redis stream sabirnicu i odabir biblioteke nezavisne od okvira

Udubivši se u sve ovo i pokušavši nešto da uradim rukama, došao sam do zaključka da radim neke štake i odlučio sam da pokušam nešto drugo.

osvijetljeno/u redu

Ispostavilo se da je ova biblioteka čvrsto vezana za Laravel infrastrukturu i gomilu drugih zavisnosti, tako da nisam trošio mnogo vremena na nju: instalirao sam je, pogledao je, video zavisnosti i obrisao.

yiisoft/yii2-queue

Pa, ovdje se odmah pretpostavilo iz imena, opet, stroga veza sa Yii2. Morao sam da koristim ovu biblioteku i nije bilo loše, ali nisam razmišljao o tome da potpuno zavisi od Yii2.

Ostalo

Sve ostalo što sam našao na GitHubu bili su nepouzdani, zastarjeli i napušteni projekti bez zvijezda, forkova i velikog broja urezivanja.

Povratak na symfony/messenger, tehnički detalji

Morao sam shvatiti ovu biblioteku i, nakon što sam proveo još neko vrijeme, uspio sam. Ispostavilo se da je sve prilično sažeto i jednostavno. Da bih instancirao autobus, napravio sam malu fabriku, jer... Trebalo je da imam nekoliko guma i sa različitim rukovaocima.

Prenošenje PHP pozadine na Redis stream sabirnicu i odabir biblioteke nezavisne od okvira

Samo nekoliko koraka:

  • Kreiramo rukovaoce porukama koje treba jednostavno pozvati
  • Umotavamo ih u HandlerDescriptor (klasa iz biblioteke)
  • Ove “deskriptore” umotavamo u instancu HandlersLocator
  • Dodavanje HandlersLocator instanci MessageBus
  • Mi proslijeđujemo skup `SenderInterface` SendersLocatoru, u mom slučaju instance `RedisTransport` klasa, koje su konfigurirane na očigledan način
  • Dodavanje SendersLocator instanci MessageBus

MessageBus ima metodu `->dispatch()` koja traži odgovarajuće rukovaoce u HandlersLocatoru i prosljeđuje im poruku, koristeći odgovarajući `SenderInterface` za slanje preko magistrale (Redis tokovi).

U konfiguraciji kontejnera (u ovom slučaju php-di), cijeli ovaj paket se može konfigurirati ovako:

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

Ovdje možete vidjeti da smo u SendersLocatoru dodijelili različite “transporte” za dvije različite poruke, od kojih svaka ima svoju vezu sa odgovarajućim tokovima.

Napravio sam poseban demo projekat koji demonstrira aplikaciju od tri demona koji međusobno komuniciraju pomoću sljedeće magistrale: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

Ali pokazat ću vam kako se potrošač može strukturirati:

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

Korištenje ove infrastrukture u aplikaciji

Nakon što sam implementirao sabirnicu u svoj backend, odvojio sam pojedinačne faze od stare sinhrone komande i napravio zasebne rukovaoce, od kojih svaki radi svoje.

Cjevovod za dodavanje nove stranice bazi podataka izgledao je ovako:

Prenošenje PHP pozadine na Redis stream sabirnicu i odabir biblioteke nezavisne od okvira

I odmah nakon toga postalo mi je mnogo lakše dodati novu funkcionalnost, na primjer, izdvajanje i raščlanjivanje Rss-a. Jer ovaj proces takođe zahteva originalni sadržaj, a zatim se rukovalac RSS veze, kao što je WebsiteIndexHistoryPersistor, pretplaćuje na poruku „Content/HtmlContent“, obrađuje je i dalje prosleđuje željenu poruku duž svog cevovoda.

Prenošenje PHP pozadine na Redis stream sabirnicu i odabir biblioteke nezavisne od okvira

Na kraju smo dobili nekoliko demona, od kojih svaki održava veze samo s potrebnim resursima. Na primjer demon štramplice sadrži sve rukovaoce koji zahtijevaju odlazak na Internet radi sadržaja i demona istrajati drži vezu sa bazom podataka.

Sada, umjesto odabira iz baze podataka, traženi ID-ovi nakon umetanja od strane perzistera se jednostavno prenose preko magistrale svim zainteresiranim rukovaocima.

izvor: www.habr.com

Dodajte komentar