Přenos PHP backendu na sběrnici Redis streams a výběr knihovny nezávislé na frameworku

Přenos PHP backendu na sběrnici Redis streams a výběr knihovny nezávislé na frameworku

předmluva

Můj web, který provozuji jako hobby, je navržen tak, aby hostoval zajímavé domovské stránky a osobní stránky. Toto téma mě začalo zajímat na úplném začátku mé programátorské cesty, v tu chvíli mě fascinovalo najít skvělé profesionály, kteří píší o sobě, svých zálibách a projektech. Zvyk objevovat je pro sebe přetrvává dodnes: téměř na každém komerčním i nepříliš komerčním webu se při hledání odkazů na autory stále dívám do zápatí.

Realizace nápadu

První verze byla pouze html stránka na mém osobním webu, kam jsem vkládal odkazy s podpisy do seznamu ul. Když jsem za určité období napsal 20 stránek, začal jsem si myslet, že to není příliš efektivní, a rozhodl jsem se pokusit tento proces zautomatizovat. Na stackoverflow jsem si všiml, že mnoho lidí uvádí stránky ve svých profilech, tak jsem napsal parser v php, který jednoduše prošel profily, počínaje prvním (adresy na SO jsou dodnes takto: `/users/1` ), extrahoval odkazy z požadované značky a přidal je do SQLite.

To lze nazvat druhou verzí: sbírkou desítek tisíc URL v tabulce SQLite, která nahradila statický seznam v HTML. V tomto seznamu jsem provedl jednoduché vyhledávání. Protože existovaly pouze adresy URL, pak bylo vyhledávání jednoduše založeno na nich.

V této fázi jsem projekt opustil a vrátil se k němu po dlouhé době. V této fázi byly moje pracovní zkušenosti již více než tři roky a cítil jsem, že bych mohl dělat něco vážnějšího. Navíc byla velká touha ovládat relativně nové technologie.

Moderní verze

projekt nasazena v Dockeru byla databáze přenesena do mongoDb a nově přibyla ředkev, která byla zpočátku jen pro cachování. Jako základ je použit jeden z mikrorámců PHP.

problém

Nové weby se přidávají příkazem konzoly, který synchronně provádí následující:

  • Stahuje obsah podle URL
  • Nastavuje příznak označující, zda byl HTTPS dostupný
  • Zachovává podstatu webu
  • Zdrojový kód HTML a záhlaví se ukládají do historie „indexování“.
  • Analyzuje obsah, extrahuje název a popis
  • Ukládá data do samostatné kolekce

To stačilo k jednoduchému uložení webů a jejich zobrazení v seznamu:

Přenos PHP backendu na sběrnici Redis streams a výběr knihovny nezávislé na frameworku

Ale myšlenka automatického indexování, kategorizace a hodnocení všeho, udržování všeho aktuálního, do tohoto paradigmatu příliš nezapadala. I pouhé přidání webové metody pro přidání stránek vyžadovalo duplikaci kódu a blokování, aby se zabránilo potenciálnímu DDoS.

Obecně lze samozřejmě vše dělat synchronně a ve webové metodě stačí URL jednoduše uložit, aby monstrózní démon prováděl všechny úkoly pro URL ze seznamu. Ale i zde se slovo „fronta“ nabízí samo o sobě. A pokud je implementována fronta, pak lze všechny úkoly rozdělit a provádět alespoň asynchronně.

rozhodnutí

Implementujte fronty a vytvořte událostmi řízený systém pro zpracování všech úkolů. A už dlouho jsem chtěl vyzkoušet Redis Streams.

Používání streamů Redis v PHP

Protože Protože můj framework není jedním ze tří gigantů Symfony, Laravel, Yii, rád bych našel nezávislou knihovnu. Jak se ale ukázalo (při prvním zkoumání), najít jednotlivé seriózní knihovny je nemožné. Vše, co souvisí s frontami, je buď projekt ze 3 commitů před pěti lety, nebo je svázán s frameworkem.

O Symfony jako dodavateli jednotlivých užitečných komponent jsem hodně slyšel a některé už používám. A také některé věci od Laravelu lze také použít, například jejich ORM, bez přítomnosti samotného frameworku.

symfony/messenger

První kandidát se okamžitě zdál ideální a bez jakýchkoli pochyb jsem ho nainstaloval. Ukázalo se ale, že je složitější vygooglit příklady použití mimo Symfony. Jak sestavit z hromady tříd s univerzálními, nic neříkajícími názvy, sběrnicí pro předávání zpráv a dokonce i na Redis?

Přenos PHP backendu na sběrnici Redis streams a výběr knihovny nezávislé na frameworku

Dokumentace na oficiálních stránkách byla poměrně podrobná, ale inicializace byla popsána pouze pro Symfony pomocí jejich oblíbeného YML a dalších magických metod pro nesymfoniky. Neměl jsem zájem o samotný proces instalace, zejména během novoročních svátků. Ale musel jsem to dělat nečekaně dlouho.

Pokoušet se přijít na to, jak vytvořit instanci systému pomocí zdrojů Symfony, také není nejtriviálnější úkol pro krátký termín:

Přenos PHP backendu na sběrnici Redis streams a výběr knihovny nezávislé na frameworku

Poté, co jsem se do toho všeho ponořil a zkusil něco udělat rukama, jsem došel k závěru, že dělám nějaké berličky a rozhodl jsem se zkusit něco jiného.

svítí/fronta

Ukázalo se, že tato knihovna byla pevně svázána s infrastrukturou Laravel a spoustou dalších závislostí, takže jsem s ní nestrávil mnoho času: nainstaloval jsem ji, podíval se na ni, viděl závislosti a smazal ji.

yiisoft/yii2-fronta

No, tady se to hned z názvu vyvodilo, opět striktní spojení s Yii2. Musel jsem použít tuto knihovnu a nebylo to špatné, ale nepřemýšlel jsem o tom, že zcela závisí na Yii2.

Zbytek

Všechno ostatní, co jsem na GitHubu našel, byly nespolehlivé, zastaralé a opuštěné projekty bez hvězdiček, forků a velkého množství commitů.

Návrat na symfony/messenger, technické detaily

Musel jsem přijít na tuto knihovnu a poté, co jsem tam strávil ještě nějaký čas, se mi to podařilo. Ukázalo se, že vše bylo celkem stručné a jednoduché. Abych vytvořil instanci autobusu, udělal jsem malou továrnu, protože... Měl jsem mít několik pneumatik a s různými manipulátory.

Přenos PHP backendu na sběrnici Redis streams a výběr knihovny nezávislé na frameworku

Jen pár kroků:

  • Vytváříme obslužné programy zpráv, které by měly být jednoduše volatelné
  • Zabalíme je do HandlerDescriptor (třída z knihovny)
  • Tyto „deskriptory“ zabalíme do instance HandlersLocator
  • Přidání HandlersLocator do instance MessageBus
  • Do SendersLocator předáme sadu `SenderInterface`, v mém případě instance tříd `RedisTransport`, které jsou nakonfigurovány zřejmým způsobem
  • Přidání SenderLocator do instance MessageBus

MessageBus má metodu `->dispatch()`, která vyhledá příslušné obslužné rutiny v HandlersLocator a předá jim zprávu pomocí odpovídajícího `SenderInterface` k odeslání přes sběrnici (streamy Redis).

V konfiguraci kontejneru (v tomto případě php-di) lze celý tento balíček nakonfigurovat 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);
        },

Zde můžete vidět, že v SenderLocator jsme přiřadili různé „přepravy“ pro dvě různé zprávy, z nichž každá má své vlastní připojení k odpovídajícím streamům.

Vytvořil jsem samostatný demo projekt demonstrující aplikaci tří démonů, které spolu komunikují pomocí následující sběrnice: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

Ale ukážu vám, jak může být spotřebitel strukturován:

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žití této infrastruktury v aplikaci

Po implementaci sběrnice v mém backendu jsem oddělil jednotlivé fáze od starého synchronního příkazu a vytvořil samostatné handlery, z nichž každý dělá svou vlastní věc.

Postup pro přidání nového webu do databáze vypadal takto:

Přenos PHP backendu na sběrnici Redis streams a výběr knihovny nezávislé na frameworku

A hned poté pro mě bylo mnohem jednodušší přidávat nové funkce, například extrahovat a analyzovat Rss. Protože tento proces také vyžaduje původní obsah, pak se obslužný program pro extrakci odkazů RSS, jako je WebsiteIndexHistoryPersistor, přihlásí k odběru zprávy „Content/HtmlContent“, zpracuje ji a předá požadovanou zprávu dále svým kanálem.

Přenos PHP backendu na sběrnici Redis streams a výběr knihovny nezávislé na frameworku

Nakonec jsme skončili s několika démony, z nichž každý udržuje připojení pouze k nezbytným zdrojům. Například démon šatičky pro batole obsahuje všechny obslužné rutiny, které vyžadují přístup na internet pro obsah, a démona vytrvat udržuje připojení k databázi.

Nyní, místo výběru z databáze, jsou požadovaná ID po vložení perzistentním zařízením jednoduše přenášena přes sběrnici všem zainteresovaným handlerům.

Zdroj: www.habr.com

Přidat komentář