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

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

Úvod

Můj web, kterému se věnuji jako koníčku, slouží k uchovávání zajímavých domácích stránek a osobních webů. Tohle téma mě začalo zajímat na začátku mé cesty v programování, kdy mě fascinovalo nacházení velkých profesionálů, kteří píší o sobě, svých zálibách a projektech. Zvyk prozkoumávat je mi zůstal a dodnes: skoro na každé komerční i ne příliš stránce stále nahlížím do patičky ve vyhledávání odkazů na autory.

Realizace nápadu

První verze byla jen html stránka na mém osobním webu, kde jsem shromažďoval odkazy s popisy do ul seznamu. Když jsem za nějakou dobu nasbíral asi 20 stránek, začal jsem přemýšlet, že to není příliš efektivní, a rozhodl se zkusit automatizovat proces. Na stackoverflow jsem si všiml, že mnozí uvádějí weby ve svých profilech, proto jsem napsal parser na php, který prostě procházel profily, začínajíc od prvního (adresy na SO jsou dodnes tohoto typu: `/users/1`), vyndaval odkazy z potřebného tagu a ukládal je do SQLite.

Tohle můžeme nazvat druhou verzí: kolekce z desítek tisíc url v SQLite tabulce, která nahradila statický seznam v html. Podle tohoto seznamu jsem udělal jednoduché vyhledávání. Protože byly pouze url, vyhledávání bylo také podle nich.

V této fázi jsem projekt opustil a vrátil se k němu po dlouhé době. V této fázi mého zaměstnání už bylo více než tři roky a cítil jsem, že mohu udělat něco serióznějšího. Navíc jsem měl velkou touhu osvojení si relativně nových technologií.

Moderní verze

Projekt rozložené v dockeru, databáze převedena na mongoDb, a od relativně nedávné doby byl přidán redis, který byl ze začátku pouze pro caching. Jako základ se používá jeden z mikroframeworků PHP.

Problém

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

  • Stahuje obsah podle URL
  • Nastavuje příznak o tom, zda byl HTTPS dostupný
  • Ukládá entitu webu
  • Původní HTML a hlavičky ukládá do historie 'indexování'
  • Parsuje obsah, extrahuje Title a Description
  • Data ukládá do samostatné kolekce

To bylo dostatečné pro prosté uchovávání webů a jejich zobrazení v seznamu:

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

Ale myšlenka, že vše automaticky indexuje, kategorizuje a hodnotí, a udržuje vše v aktuálním stavu, se do této paradigmy příliš nevešla. Dokonce i jednoduché přidání webové metody pro přidání stránek vyžadovalo duplicitu kódu a zámky, aby se předešlo potenciálnímu DDoS.

Samozřejmě, že to lze dělat i synchronně, a ve webové metodě pouze uložit URL, aby mohlo monstrózní démon splnit všechny úkoly pro URL ze seznamu. Ale i tady se vnucuje slovo "fronta". A pokud frontu implementovat, lze všechny úkoly rozdělit a vykonávat alespoň asynchronně.

Řešení

Implementovat fronty a vytvořit event-driven systém zpracování všech úkolů. A už dlouho jsem chtěl vyzkoušet Redis Streams.

Použití Redis streams v PHP

Protože můj framework není z trojice gigantů Symfony, Laravel, Yii, chtěl bych najít nezávislou knihovnu. Ale, jak se ukázalo (při prvním pohledu) — žádné vážné knihovny se najít nedají. Všechno, co se týká front, buď je projektem se 3 commity starými pět let, nebo je vázáno na framework.

Slyšel jsem o Symfony jako o dodavateli jednotlivých užitečných komponentů, navíc některé z nich už používám. A také z Laravelu lze něco použít, například jejich ORM, bez přítomnosti samotného frameworku.

symfony/messenger

První kandidát mi hned připadal dokonalý a bez jakýchkoliv pochybností jsem ho nainstaloval. Ale nalezení příkladů použití mimo Symfony se ukázalo jako obtížnější. Jak sestavit z hromady tříd s univerzálními, nic neříkajícími názvy, sběrnici pro přenos zpráv, a ještě na Redis?

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

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 nesymfonisty. O samotný proces instalace jsem neměl zájem, obzvlášť během novoročních prázdnin. Ale musel jsem se tím zabývat a překvapivě dlouho.

Pok попытka porozumět instancování systému podle zdrojových kódů Symfony není také nejtriviálnější úkol pro zkrácené termíny:

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

Pohrabav se v tom všem a snažíc se něco udělat rukama, jsem dospěl k závěru, že se zabývám nějakými záplatami, a rozhodl jsem se zkusit něco jiného.

illuminate/queue

Ukázalo se, že tato knihovna je pevně svázána s infrastrukturou Laravel a řadou dalších závislostí, takže jsem na ni příliš času nestrávil: nainstaloval jsem ji, podíval se, viděl závislosti a odstranil ji.

yiisoft/yii2-queue

Z názvu bylo okamžitě jasné, že je opět silně vázána na Yii2. S touto knihovnou jsem musel pracovat a byla docela dobrá, ale o tom, že je zcela závislá na Yii2, jsem nepřemýšlel.

Ostatní

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 počtu commitů.

Návrat k symfony/messenger, technické detaily

Musel jsem se touto knihovnou zabývat a po strávení nějakého času jsem to zvládl. Ukázalo se, že je to docela stručné a jednoduché. Pro instancování busu jsem udělal malou továrnu, protože jsem plánoval mít několik busů s různými zpracovateli.

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

Jen několik kroků:

  • Vytváříme zpracovatele zpráv, které by měly být prostě volatelné
  • Zabalíme je do HandlerDescriptor (třída z knihovny)
  • Tyto „Deskriptory“ zabalíme do instance HandlersLocator
  • Přidáme HandlersLocator do instance MessageBus
  • Předáme do SendersLocator sadu `SenderInterface`, v mém případě instance tříd `RedisTransport`, které se konfigurují zjevně
  • Přidáme SendersLocator do instance MessageBus

MessageBus má metodu `->dispatch()`, která hledá odpovídající zpracovatele v HandlersLocator a předává zprávu těmto zpracovatelům, přičemž používá odpovídající `SenderInterface` pro odeslání přes bus (Redis streams).

V konfiguraci kontejneru (v tomto případě php-di) může být celý tento řetězec nakonfigurován 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);
        },

Tady je vidět, že v SendersLocator jsme přiřadili různý 'transport' pro dvě různé zprávy, kdy každá má svůj spoj na příslušné streamy.

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

Ale ukážu, jak může být nastaven consumer:

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

Využití této infrastruktury v aplikaci

Realizací sběrnice ve svém backendu jsem oddělil jednotlivé kroky ze starého synchronního příkazu a vytvořil jednotlivé handlery, z nichž každý se zabývá svým úkolem.

Pipeline pro přidání nové stránky do databáze vypadá takto:

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

A ihned po tom bylo mnohem snazší přidávat nové funkce, například extrakci a parsování Rss. Protože tento proces také vyžaduje zdrojový obsah, handler pro získávání odkazů na RSS, stejně jako WebsiteIndexHistoryPersistor, se přihlásí k zprávě "Content/HtmlContent", zpracovává ji a posílá potřebnou zprávu dále ve svém pipeline.

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

Nakonec se objevilo několik démonů, z nichž každý drží připojení pouze k potřebným zdrojům. Například démon crawlers obsahuje všechny handlery, které vyžadují přístup k obsahu na internetu, zatímco démon persister udržuje připojení k databázi.

Nyní místo selekce z databáze jsou potřebné ID po vložení persisterem jednoduše předávány prostřednictvím sběrnice všem zainteresovaným handlerům.

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster