Prijenos PHP pozadine na Redis streams sabirnicu i odabir biblioteke neovisne o okviru

Prijenos PHP pozadine na Redis streams sabirnicu i odabir biblioteke neovisne o okviru

predgovor

Moja web stranica, koju vodim iz hobija, osmišljena je tako da ugošćuje zanimljive početne stranice i osobne stranice. Ova me tema počela zanimati na samom početku mog programerskog puta, tada sam bio fasciniran pronalaskom velikih profesionalaca koji pišu o sebi, svojim hobijima i projektima. Navika da ih sam otkrivam ostala je do danas: na gotovo svakoj komercijalnoj i ne baš komercijalnoj stranici nastavljam gledati u podnožje u potrazi za poveznicama na autore.

Provedba ideje

Prva verzija bila je samo html stranica na mojoj osobnoj web stranici, gdje sam stavio poveznice s potpisima u ul listu. Nakon što sam tipkao 20 stranica u određenom vremenskom razdoblju, počeo sam misliti da to nije baš učinkovito i odlučio sam pokušati automatizirati proces. Na stackoverflowu primijetio sam da mnogi ljudi označavaju stranice u svojim profilima, pa sam napisao parser u php-u, koji je jednostavno prošao kroz profile, počevši od prvog (adrese na SO-u do danas su ovakve: `/users/1` ), izvukao je veze iz željene oznake i dodao je u SQLite.

Ovo se može nazvati drugom verzijom: zbirka desetaka tisuća URL-ova u SQLite tablici, koja je zamijenila statički popis u html-u. Napravio sam jednostavnu pretragu na ovom popisu. Jer postojali su samo URL-ovi, onda se pretraga jednostavno temeljila na njima.

U ovoj fazi sam napustio projekt i vratio mu se nakon dugo vremena. U ovoj fazi moje radno iskustvo je već bilo više od tri godine i osjetio sam da mogu raditi nešto ozbiljnije. Osim toga, postojala je velika želja za ovladavanjem relativno novim tehnologijama.

Moderna verzija

Projekt implementiran u Dockeru, baza podataka je prebačena u mongoDb, a nedavno je dodan i radish, koji je isprva služio samo za predmemoriju. Kao osnova korišten je jedan od PHP mikrookvira.

problem

Nova web mjesta dodaju konzolna naredba koja sinkrono radi sljedeće:

  • Preuzima sadržaj prema URL-u
  • Postavlja oznaku koja pokazuje je li HTTPS bio dostupan
  • Čuva bit web stranice
  • Izvorni HTML i zaglavlja spremaju se u povijest "indeksiranja".
  • Raščlanjuje sadržaj, izdvaja naslov i opis
  • Sprema podatke u zasebnu zbirku

To je bilo dovoljno za jednostavno pohranjivanje stranica i njihovo prikazivanje na popisu:

Prijenos PHP pozadine na Redis streams sabirnicu i odabir biblioteke neovisne o okviru

Ali ideja o automatskom indeksiranju, kategoriziranju 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.

Općenito, naravno, sve se može raditi sinkrono, au web metodi možete jednostavno spremiti URL tako da monstruozni demon obavlja sve zadatke za URL-ove s popisa. Ali ipak, čak i ovdje riječ "red" sugerira se sama od sebe. A ako je implementiran red čekanja, tada se svi zadaci mogu podijeliti i izvoditi barem asinkrono.

odluka

Implementirajte redove čekanja i napravite sustav vođen događajima za obradu svih zadataka. I već dugo želim isprobati Redis Streams.

Korištenje Redis tokova u PHP-u

Jer Budući da moj framework nije jedan od tri diva Symfony, Laravel, Yii, želio bih pronaći neovisnu biblioteku. No, kako se pokazalo (na prvom pregledu), nemoguće je pronaći pojedinačne ozbiljne knjižnice. Sve vezano uz redove čekanja je ili projekt od 3 komita prije pet godina, ili je vezano uz okvir.

Čuo sam puno o Symfonyu kao dobavljaču pojedinačnih korisnih komponenti, a neke od njih već koristim. A također se mogu koristiti i neke stvari iz Laravela, na primjer njihov ORM, bez prisustva samog frameworka.

simfonija/glasnik

Prvi kandidat mi se odmah učinio idealnim i bez sumnje sam ga instalirao. Ali pokazalo se da je teže guglati primjere korištenja izvan Symfonya. Kako sastaviti od gomile klasa sa univerzalnim, besmislenim imenima, sabirnicom za prosljeđivanje poruka, pa još na Redisu?

Prijenos PHP pozadine na Redis streams sabirnicu i odabir biblioteke neovisne o okviru

Dokumentacija na službenoj stranici bila je prilično detaljna, ali je inicijalizacija opisana samo za Symfony koristeći njihov omiljeni YML i druge čarobne metode za ne-simfoničare. Sam proces instalacije me nije zanimao, pogotovo za vrijeme novogodišnjih praznika. Ali morao sam to raditi neočekivano dugo.

Pokušaj otkriti kako instancirati sustav koristeći Symfony izvore također nije najtrivijalniji zadatak za kratak rok:

Prijenos PHP pozadine na Redis streams sabirnicu i odabir biblioteke neovisne o okviru

Nakon što sam se udubio u sve to i pokušao nešto napraviti rukama, došao sam do zaključka da radim nekakve štake i odlučio pokušati nešto drugo.

osvijetljen/red

Ispostavilo se da je ta biblioteka bila čvrsto vezana za Laravel infrastrukturu i hrpu drugih ovisnosti, pa nisam trošio puno vremena na nju: instalirao sam je, pogledao, vidio ovisnosti i izbrisao.

yiisoft/yii2-red čekanja

Pa, ovdje se odmah pretpostavilo iz imena, opet, stroga veza s Yii2. Morao sam koristiti ovu biblioteku i nije bila loša, ali nisam razmišljao o tome da u potpunosti ovisi o Yii2.

Ostalo

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

Povratak na symfony/messenger, tehničke pojedinosti

Morao sam shvatiti ovu knjižnicu i, nakon što sam proveo još neko vrijeme, uspio sam. Ispostavilo se da je sve bilo prilično sažeto i jednostavno. Da bih pokrenuo autobus, napravio sam malu tvornicu, jer... Trebao sam imati nekoliko guma i s različitim rukovateljima.

Prijenos PHP pozadine na Redis streams sabirnicu i odabir biblioteke neovisne o okviru

Samo nekoliko koraka:

  • Stvaramo rukovatelje porukama koji bi se trebali jednostavno pozvati
  • Omotamo ih u HandlerDescriptor (klasa iz biblioteke)
  • Ove "deskriptore" omotavamo u instancu HandlersLocator
  • Dodavanje HandlersLocator instanci MessageBus
  • Prosljeđujemo skup `SenderInterface` u SendersLocator, u mom slučaju instance klasa `RedisTransport`, koje su konfigurirane na očit način
  • Dodavanje SendersLocator instanci MessageBus

MessageBus ima metodu `->dispatch()` koja traži odgovarajuće rukovatelje u HandlersLocatoru i prosljeđuje im poruku koristeći odgovarajuće `SenderInterface` za slanje putem sabirnice (Redis streamovi).

U konfiguraciji spremnika (u ovom slučaju php-di), cijeli ovaj paket može se 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 vlastitu vezu s odgovarajućim tokovima.

Napravio sam zaseban demo projekt koji demonstrira primjenu tri demona koji međusobno komuniciraju pomoću sljedeće sabirnice: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

Ali pokazat ću vam kako potrošač može biti strukturiran:

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 stupnjeve od stare sinkrone naredbe i napravio zasebne rukovatelje, od kojih svaki radi svoje.

Cjevovod za dodavanje nove stranice u bazu podataka izgledao je ovako:

Prijenos PHP pozadine na Redis streams sabirnicu i odabir biblioteke neovisne o okviru

I odmah nakon toga, postalo mi je puno lakše dodati nove funkcije, na primjer, izdvajanje i raščlanjivanje Rss-a. Jer ovaj proces također zahtijeva izvorni sadržaj, tada se rukovatelj RSS veze za izvlačenje, kao što je WebsiteIndexHistoryPersistor, pretplaćuje na poruku "Content/HtmlContent", obrađuje je i prosljeđuje željenu poruku duž svog cjevovoda dalje.

Prijenos PHP pozadine na Redis streams sabirnicu i odabir biblioteke neovisne o okviru

Na kraju smo dobili nekoliko demona, od kojih svaki održava veze samo s potrebnim resursima. Na primjer demon štramplice sadrži sve rukovatelje koji zahtijevaju odlazak na Internet za sadržaj i demon ustrajati drži vezu s bazom podataka.

Sada, umjesto odabira iz baze podataka, potrebni ID-ovi nakon umetanja od strane persistera jednostavno se prenose sabirnicom svim zainteresiranim rukovateljima.

Izvor: www.habr.com

Dodajte komentar