A PHP backend translation to Redis streams and the choice of a framework-independent library

A PHP backend translation to Redis streams and the choice of a framework-independent library

Bevezető

A web portal I maintain as a hobby, designed for storing interesting homepages and personal websites. This theme fascinated me at the beginning of my programming journey, when I was amazed by the great professionals who wrote about themselves, their hobbies, and projects. This habit of discovering them for myself still remains: almost on every commercial and less commercial site, I continue to peek into the footer in search of links to the authors.

Implementation of the idea

The first version was simply an HTML page on my personal website, where I collected links with captions in a ul list. After accumulating about 20 pages over some time, I started thinking that this was not very efficient and decided to try to automate the process. On Stack Overflow, I noticed that many listed their sites in their profiles, so I wrote a PHP parser that simply went through the profiles, starting from the first (the addresses on SO to this day look like: `/users/1`), extracted links from the needed tag, and stored them in SQLite.

This can be called the second version: a collection of tens of thousands of URLs in an SQLite table that replaced the static HTML list. Based on this list, I created a simple search. Since there were only URLs, the search was simply based on them.

At this stage, I abandoned the project and returned to it after a long time. By this point, I had over three years of work experience and felt that I could do something more serious. Moreover, I had a great desire to master relatively new technologies for myself.

Modern version

Projekt deployed in Docker, the database migrated to MongoDB, and relatively recently, Redis was added, which initially served just for caching. A PHP microframework is used as the foundation.

Probléma

New websites are added through a console command, which synchronously performs the following tasks:

  • Downloads content from the URL
  • Sets a flag indicating whether HTTPS was available
  • Stores the entity of the website
  • Saves the original HTML and headers in the 'indexing' history
  • Parses the content, extracting Title and Description
  • Saves the data in a separate collection

This was enough to simply store websites and display them in a list:

A PHP backend translation to Redis streams and the choice of a framework-independent library

De az ötlet, hogy mindent automatikusan indexáljunk, kategorizáljunk és rangsoroljuk, miközben minden naprakész marad, gyengén illeszkedett ebbe a paradigmába. Még egy egyszerű web-módszer hozzáadása is, hogy új oldalakat adjunk hozzá, kódmásolatokat és zárolásokat igényelt a potenciális DDoS elkerülése érdekében.

Tulajdonképpen persze mindent lehet csinálni szinkron módon, és a web-módszerben egyszerűen elmenteni az URL-t, hogy a szörnyű démon végezze el az összes feladatot az URL listájából. De itt is szóba kerül a „sor” fogalma. Ha bevezetünk egy sort, akkor minden feladatot szét tudunk osztani, és legalább aszinkron módon el tudjuk végezni.

Megoldás

Bevezetni a sorokat és létrehozni egy eseményvezérelt rendszert az összes feladat kezelésére. Már régóta szerettem volna kipróbálni a Redis Streams-t.

Redis Streams használata PHP-ben

Mivel a keretrendszerem nem a három óriás közé tartozik, mint a Symfony, Laravel vagy Yii, egy független könyvtárat szerettem volna találni. De, mint kiderült (az első átvizsgálás során) – nem lehetett komolyabb külön könyvtárakat találni. Minden, ami a sorokkal kapcsolatos, vagy egy öt éves, három commitból álló projekt, vagy a keretrendszerhez van kötve.

Hallottam már a Symfony-ról, mint hasznos komponensek beszállítójáról, és néhányat már használok is. Valamint a Laravel-ből is lehet használni néhány dolgot, például az ORM-jüket, a keretrendszer jelenléte nélkül.

symfony/messenger

Az első jelölt azonnal tökéletesnek tűnt, és nem volt sebaj, telepítettem. De példákat találni a Symfony-n kívüli használatra nehezebbnek bizonyult. Hogyan lehet egy üzenetküldő buszt összeállítani a sok osztályból, amelyek univerzális, semmitmondó nevekkel bírnak, és még Redis-ra is?

A PHP backend translation to Redis streams and the choice of a framework-independent library

A hivatalos weboldalon található dokumentáció meglehetősen részletes volt, de a inicializálást csak Symfony-ra írták le, a kedvenc YML-jük és más varázslatos módszereik segítségével, amelyek a nem Symfony felhasználóknak nem igazán világosak. Az installálás folyamatában nem volt különösebb érdeklődésem, különösen az ünnepek alatt. De végül foglalkoznom kellett vele, és váratlanul hosszú időt eltöltöttem vele.

A Symfony forrásaiból való rendszeren alapuló beállítás megértése sem triviális feladat a szoros határidők miatt:

A PHP backend translation to Redis streams and the choice of a framework-independent library

Miután turkáltam az egészben és megpróbáltam valamit a kezemmel csinálni, arra a következtetésre jutottam, hogy valami kényszerítő megoldással foglalkozom, és megpróbáltam valami mást.

illuminate/queue

Kiderült, hogy ez a könyvtár szorosan kapcsolódik a Laravel infrastruktúrájához és számos más függőséghez, ezért nem pazaroltam sok időt rá: telepítettem, megnéztem, láttam a függőségeket, és eltávolítottam.

yiisoft/yii2-queue

Itt az elnevezés alapján azonnal a Yii2-re utalt, ami ismét szoros kötődést jelentett. Ezt a könyvtárat korábban használtam, és nem volt rossz, de arra nem gondoltam, hogy teljesen a Yii2-től függ.

A többi

Minden más, amit a GitHubon találtam - megbízhatatlan, elavult, elhagyott projektek, csillagok, forkok és sok commit nélkül.

Visszatérve a symfony/messengerhez, technikai részletek

Meg kellett értenem ezt a könyvtárat, és miután eltöltöttem még egy kis időt, sikerült. Kiderült, hogy minden eléggé tömör és egyszerű. Az busz példányosításához készítettem egy kis gyárat, mivel több buszom is volt, különböző feldolgozókkal.

A PHP backend translation to Redis streams and the choice of a framework-independent library

Csak néhány lépés:

  • Készítünk üzenetkezelőket, amelyek egyszerűen callable-ek kell hogy legyenek
  • Ezeket HandlerDescriptor-be (a könyvtár osztálya) csomagoljuk
  • Ezeket a „Leírókat” átkonvertáljuk egy HandlersLocator példányba
  • Hozzáadjuk a HandlersLocator-t az üzenetbusz példányhoz
  • Átadjuk a SendersLocator-nak a `SenderInterface` példányokat, az én esetemben a `RedisTransport` osztályok példányait, amelyeket nyilvánvaló módon konfigurálunk
  • Hozzáadjuk a SendersLocator-t az üzenetbusz példányhoz

Az üzenetbusznak van egy `->dispatch()` metódusa, amely az megfelelő kezelőket keresi a HandlersLocator-ban és az üzenetet nekik adja át, a megfelelő `SenderInterface`-t használva az üzenetbuszon (Redis streamen) keresztüli küldéshez.

A konténer konfigurációjában (jelen esetben php-di) ez az összekapcsolás így konfigurálható:

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

Itt látható, hogy a SendersLocator-ban két különböző üzenethez különböző "transzportot" rendelünk, mindegyik a megfelelő streamhez tartozó kapcsolatával.

Készítettem egy külön demo projektet, amely bemutatja a három démonnal rendelkező alkalmazást, amelyek kommunikálnak egymással egy ilyen buszon keresztül: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

De megmutatom, hogyan épülhet fel a fogyasztó:

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

Az infrastruktúra alkalmazásban való használata

A háttérben a busz megvalósításával különböző lépésekre bontottam a régi szinkron parancsokat, és külön handler-eket készítettem, amelyek mindegyike a saját feladatával foglalkozik.

Az új webhely adatbázisba való hozzáadásának folyamata a következőképpen alakult:

A PHP backend translation to Redis streams and the choice of a framework-independent library

És ezután sokkal egyszerűbb lett új funkciók hozzáadása, például az RSS tartalom kinyerése és parse-olása. Mivel ez a folyamat szintén igényli az induló tartalmat, ezért a linkek RSS-ből való kinyerésére szolgáló handler, akárcsak a WebsiteIndexHistoryPersistor, feliratkozik a 'Content/HtmlContent' üzenetre, feldolgozza azt, és továbbítja a szükséges üzenetet a saját pipeline-ján keresztül.

A PHP backend translation to Redis streams and the choice of a framework-independent library

Ennek eredményeként több daemon keletkezett, amelyek mindegyike csak a szükséges erőforrásokhoz tartja fenn a kapcsolatot. Például a daemon crawlers minden olyan handler-t tartalmaz, amely internetkapcsolatra van szüksége a tartalom beszerzéséhez, míg a daemon persister tartja a kapcsolatot az adatbázissal.

Most már az adatbázisból való szelektálás helyett a szükséges id-kat, miután a persister feldolgozta őket, egyszerűen továbbítják a buszon minden érdekelt handler számára.

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster