Prenos PHP backendu na zbernicu Redis streams a výber knižnice nezávislej od rámca

Prenos PHP backendu na zbernicu Redis streams a výber knižnice nezávislej od rámca

Predslov

Moja webová stránka, ktorú prevádzkujem ako hobby, je navrhnutá tak, aby hosťovala zaujímavé domovské stránky a osobné stránky. Táto téma ma začala zaujímať už na začiatku mojej programátorskej cesty, vtedy ma fascinovalo nájsť skvelých profesionálov, ktorí píšu o sebe, svojich záľubách a projektoch. Zvyk objavovať ich pre seba sa zachoval dodnes: takmer na každej komerčnej a nie veľmi komerčnej stránke sa pri hľadaní odkazov na autorov naďalej pozerám do päty.

Realizácia nápadu

Prvá verzia bola len html stránka na mojej osobnej webovej stránke, kde som dal odkazy s podpismi do zoznamu ul. Po zadaní 20 strán za určitý čas som si začal myslieť, že to nie je príliš efektívne a rozhodol som sa pokúsiť tento proces zautomatizovať. Na stackoverflow som si všimol, že veľa ľudí uvádza stránky vo svojich profiloch, tak som napísal parser v php, ktorý jednoducho prešiel profilmi, počnúc prvým (adresy na SO sú dodnes takto: `/users/1` ), extrahovali odkazy z požadovanej značky a pridali ich do SQLite.

Dá sa to nazvať druhá verzia: zbierka desiatok tisíc adries URL v tabuľke SQLite, ktorá nahradila statický zoznam v HTML. Urobil som jednoduché vyhľadávanie v tomto zozname. Pretože boli tam len URL adresy, potom bolo vyhľadávanie jednoducho založené na nich.

V tejto fáze som projekt opustil a vrátil som sa k nemu po dlhom čase. V tejto fáze boli moje pracovné skúsenosti už viac ako tri roky a cítil som, že by som mohol robiť niečo vážnejšie. Okrem toho tu bola veľká túžba ovládať relatívne nové technológie.

Moderná verzia

Projekt nasadený v Dockeri bola databáza prenesená do mongoDb a nedávno pribudla reďkovka, ktorá bola spočiatku len na ukladanie do vyrovnávacej pamäte. Ako základ sa používa jeden z mikrorámcov PHP.

problém

Nové lokality sa pridávajú príkazom konzoly, ktorý synchrónne vykonáva nasledovné:

  • Sťahuje obsah podľa adresy URL
  • Nastaví príznak označujúci, či bol HTTPS dostupný
  • Zachováva podstatu webu
  • Zdrojový kód HTML a hlavičky sú uložené v histórii „indexovania“.
  • Analyzuje obsah, extrahuje názov a popis
  • Ukladá údaje do samostatnej kolekcie

To stačilo na jednoduché uloženie stránok a ich zobrazenie v zozname:

Prenos PHP backendu na zbernicu Redis streams a výber knižnice nezávislej od rámca

Ale myšlienka automatického indexovania, kategorizácie a hodnotenia všetkého, udržiavania všetkého aktuálneho, do tejto paradigmy dobre nezapadala. Dokonca aj jednoduché pridanie webovej metódy na pridávanie stránok vyžadovalo duplikáciu kódu a blokovanie, aby sa predišlo potenciálnemu DDoS.

Vo všeobecnosti, samozrejme, všetko sa dá robiť synchrónne a vo webovej metóde môžete jednoducho uložiť URL tak, aby monštruózny démon vykonával všetky úlohy pre URL zo zoznamu. Ale aj tu sa slovo „front“ naznačuje. A ak je implementovaný front, potom všetky úlohy môžu byť rozdelené a vykonávané aspoň asynchrónne.

rozhodnutie

Implementujte fronty a vytvorte systém riadený udalosťami na spracovanie všetkých úloh. A už dlho som chcel vyskúšať Redis Streams.

Používanie streamov Redis v PHP

Pretože Keďže môj framework nie je jedným z troch gigantov Symfony, Laravel, Yii, rád by som našiel nezávislú knižnicu. Ako sa však ukázalo (pri prvom preskúmaní), nie je možné nájsť jednotlivé seriózne knižnice. Všetko, čo súvisí s frontami, je buď projekt z 3 záväzkov spred piatich rokov, alebo je viazaný na framework.

O Symfony ako dodávateľovi jednotlivých užitočných komponentov som už veľa počul a niektoré už aj používam. A tiež niektoré veci od Laravelu sa dajú použiť aj napríklad ich ORM, bez prítomnosti samotného frameworku.

symfony/messenger

Prvý kandidát sa mi hneď zdal ideálny a bez akýchkoľvek pochybností som ho nainštaloval. Ukázalo sa však, že je ťažšie vygoogliť príklady použitia mimo Symfony. Ako zostaviť z množstva tried s univerzálnymi, nič nehovoriacimi názvami, autobusom na odovzdávanie správ a dokonca aj na Redis?

Prenos PHP backendu na zbernicu Redis streams a výber knižnice nezávislej od rámca

Dokumentácia na oficiálnej stránke bola pomerne podrobná, ale inicializácia bola popísaná iba pre Symfony pomocou ich obľúbeného YML a iných magických metód pre nesymfonikov. Nemal som záujem o samotný proces inštalácie, najmä počas novoročných sviatkov. Ale musel som to robiť nečakane dlho.

Pokúšať sa zistiť, ako vytvoriť inštanciu systému pomocou zdrojov Symfony, tiež nie je najtriviálnejšia úloha v krátkom termíne:

Prenos PHP backendu na zbernicu Redis streams a výber knižnice nezávislej od rámca

Po tom, čo som sa do toho všetkého ponorila a snažila som sa niečo robiť rukami, som dospela k záveru, že robím nejaké barly a rozhodla som sa skúsiť niečo iné.

osvetlené/rad

Ukázalo sa, že táto knižnica bola úzko prepojená s infraštruktúrou Laravel a množstvom ďalších závislostí, takže som na nej nestrávil veľa času: nainštaloval som ju, pozrel som si ju, videl som závislosti a odstránil som ju.

yiisoft/yii2-front

No, tu sa z názvu okamžite predpokladalo, opäť prísne spojenie s Yii2. Musel som použiť túto knižnicu a nebolo to zlé, ale nepremýšľal som o tom, že úplne závisí od Yii2.

Zvyšok

Všetko ostatné, čo som našiel na GitHub, boli nespoľahlivé, zastarané a opustené projekty bez hviezdičiek, forkov a veľkého množstva commitov.

Návrat na symfony/messenger, technické detaily

Musel som prísť na túto knižnicu a potom, čo som strávil viac času, sa mi to podarilo. Ukázalo sa, že všetko bolo celkom stručné a jednoduché. Aby som vytvoril inštanciu autobusu, vytvoril som malú továreň, pretože... Mal som mať niekoľko pneumatík a s rôznymi manipulátormi.

Prenos PHP backendu na zbernicu Redis streams a výber knižnice nezávislej od rámca

Len pár krokov:

  • Vytvárame obslužné programy správ, ktoré by mali byť jednoducho volateľné
  • Zabalíme ich do HandlerDescriptor (trieda z knižnice)
  • Tieto „deskriptory“ zabalíme do inštancie HandlersLocator
  • Pridanie HandlersLocator do inštancie MessageBus
  • Odovzdáme množinu `SenderInterface` do SenderLocator, v mojom prípade inštancie tried `RedisTransport`, ktoré sú nakonfigurované zrejmým spôsobom
  • Pridanie SenderLocator do inštancie MessageBus

MessageBus má metódu `->dispatch()`, ktorá vyhľadá príslušné obslužné programy v HandlersLocator a odovzdá im správu pomocou zodpovedajúceho rozhrania `SenderInterface` na odoslanie cez zbernicu (streamy Redis).

V konfigurácii kontajnera (v tomto prípade php-di) môže byť celý tento balík nakonfigurovaný takto:

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

Tu môžete vidieť, že v SenderLocator sme priradili rôzne „prepravy“ pre dve rôzne správy, z ktorých každá má svoje vlastné pripojenie k zodpovedajúcim streamom.

Urobil som samostatný demo projekt demonštrujúci aplikáciu troch démonov komunikujúcich medzi sebou pomocou nasledujúcej zbernice: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

Ale ukážem vám, ako môže byť spotrebiteľ štruktúrovaný:

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

Použitie tejto infraštruktúry v aplikácii

Po implementácii zbernice v mojom backende som oddelil jednotlivé fázy od starého synchrónneho príkazu a vytvoril samostatné handlery, z ktorých každý robí svoju vlastnú vec.

Postup na pridanie novej lokality do databázy vyzeral takto:

Prenos PHP backendu na zbernicu Redis streams a výber knižnice nezávislej od rámca

A hneď potom bolo pre mňa oveľa jednoduchšie pridávať nové funkcie, napríklad extrahovať a analyzovať Rss. Pretože tento proces vyžaduje aj pôvodný obsah, potom sa obslužný program extraktora odkazov RSS, ako napríklad WebsiteIndexHistoryPersistor, prihlási na odber správy „Content/HtmlContent“, spracuje ju a odošle požadovanú správu ďalej.

Prenos PHP backendu na zbernicu Redis streams a výber knižnice nezávislej od rámca

Nakoniec sme skončili s niekoľkými démonmi, z ktorých každý udržiava spojenie len s potrebnými zdrojmi. Napríklad démon roboti obsahuje všetky obslužné programy, ktoré vyžadujú prístup na internet kvôli obsahu, a démona vytrvať drží spojenie s databázou.

Teraz namiesto výberu z databázy sa požadované ID po vložení perzistentným zariadením jednoducho prenesú cez zbernicu všetkým zainteresovaným handlerom.

Zdroj: hab.com

Pridať komentár