A PHP-háttér átvitele a Redis streams buszra, és egy keretrendszertől független könyvtár kiválasztása

A PHP-háttér átvitele a Redis streams buszra, és egy keretrendszertől független könyvtár kiválasztása

Előszó

Honlapomat, amelyet hobbiból üzemeltetek, úgy tervezték, hogy érdekes kezdőlapokat és személyes oldalakat helyezzen el. Ez a téma már a programozási utam kezdetén érdekelt, abban a pillanatban lenyűgözött, hogy nagyszerű szakembereket találtam, akik magukról, hobbijairól, projektjeikről írnak. Az a szokás, hogy magam fedezzem fel őket, a mai napig megmaradt: szinte minden kereskedelmi és nem túl kommersz oldalon továbbra is a láblécben nézek, keresve a szerzőkre mutató linkeket.

Az ötlet megvalósítása

Az első verzió csak egy html oldal volt a személyes honlapomon, ahol aláírásokkal ellátott hivatkozásokat tettem egy ul listába. Miután 20 oldalt begépeltem egy ideig, kezdtem azt gondolni, hogy ez nem túl hatékony, és úgy döntöttem, hogy megpróbálom automatizálni a folyamatot. A stackoverflow-nál azt vettem észre, hogy sokan oldalakat jeleznek a profiljukban, ezért írtam egy értelmezőt php-ben, ami egyszerűen végigment a profilokon, kezdve az elsővel (a SO-n a mai napig a következő címek vannak: `/users/1` ), kibontotta a hivatkozásokat a kívánt címkéből, és hozzáadta az SQLite-hoz.

Ezt nevezhetjük a második verziónak: több tízezer URL-ből álló gyűjtemény egy SQLite táblában, amely felváltotta a html statikus listát. Végeztem egy egyszerű keresést ezen a listán. Mert csak URL-ek voltak, akkor a keresés egyszerűen ezek alapján történt.

Ebben a szakaszban felhagytam a projekttel, és hosszú idő után visszatértem hozzá. Ebben a szakaszban a munkatapasztalatom már több mint három év volt, és úgy éreztem, hogy tudok valami komolyabbat is csinálni. Emellett nagy volt a vágy a viszonylag új technológiák elsajátítására.

Modern változat

Terv A Dockerben telepítve az adatbázis átkerült a mongoDb-be, újabban pedig a retek is bekerült, ami eleinte csak gyorsítótárazásra szolgált. Az egyik PHP mikrokeretet használják alapul.

probléma

Az új webhelyeket egy konzolparancs adja hozzá, amely szinkron módon a következőket teszi:

  • Töltse le a tartalmat URL alapján
  • Jelzőt állít be, amely jelzi, hogy elérhető volt-e a HTTPS
  • Megőrzi a weboldal lényegét
  • A forrás HTML és a fejlécek mentésre kerülnek az „indexelési” előzményekbe
  • A tartalmat elemzi, kivonja a címet és a leírást
  • Az adatokat külön gyűjteménybe menti

Ez elég volt ahhoz, hogy egyszerűen tárolja a webhelyeket és megjelenítse azokat egy listában:

A PHP-háttér átvitele a Redis streams buszra, és egy keretrendszertől független könyvtár kiválasztása

De az az elképzelés, hogy mindent automatikusan indexelnek, kategorizálnak és rangsorolnak, mindent naprakészen tartanak, nem illett jól ebbe a paradigmába. Még az oldalak hozzáadásához szükséges webes metódus egyszerű hozzáadásához is kódduplázásra és blokkolásra volt szükség az esetleges DDoS elkerülése érdekében.

Általában természetesen mindent meg lehet csinálni szinkronban, és a webes metódusban egyszerűen elmentheti az URL-t, így a szörnyű démon elvégzi az összes feladatot a listából származó URL-ekre. De még itt is a „sor” szó sugallja magát. Ha pedig sor kerül megvalósításra, akkor minden feladat felosztható és legalább aszinkron módon végrehajtható.

döntés

Várólisták megvalósítása és eseményvezérelt rendszer létrehozása az összes feladat feldolgozásához. A Redis Streams-et pedig már régóta ki akartam próbálni.

Redis streamek használata PHP-ben

Mert Mivel a keretrendszerem nem a három óriás Symfony, Laravel, Yii egyike, szeretnék egy független könyvtárat találni. De, mint az első vizsgálatra kiderült, lehetetlen egyedi, komoly könyvtárakat találni. Minden, ami a sorokkal kapcsolatos, vagy egy öt évvel ezelőtti 3 commit projekt, vagy a kerethez kötődik.

Sokat hallottam a Symfony-ról, mint egyedi hasznos komponensek szállítójáról, és már használok is néhányat. És néhány dolog a Laraveltől is használható, például az ORM-jük, a keret jelenléte nélkül.

symfony/mesenger

Az első jelölt azonnal ideálisnak tűnt, és minden kétséget kizáróan beállítottam. De a Symfonyn kívüli felhasználási példák keresése a google-ban nehezebbnek bizonyult. Hogyan állítsunk össze egy buszt üzenetek átadására egy csomó univerzális, értelmetlen nevű osztályból, és még Redis-en is?

A PHP-háttér átvitele a Redis streams buszra, és egy keretrendszertől független könyvtár kiválasztása

A hivatalos oldalon található dokumentáció meglehetősen részletes volt, de az inicializálást csak a Symfony számára írták le, a kedvenc YML-lel és más varázslatos módszerekkel a nem szimfonikus számára. Maga a telepítési folyamat nem érdekelt, különösen az újévi ünnepek alatt. De ezt váratlanul sokáig kellett csinálnom.

A Symfony-források használatával való rendszer példányosítása sem a legtriviálisabb feladat egy szűk határidő mellett:

A PHP-háttér átvitele a Redis streams buszra, és egy keretrendszertől független könyvtár kiválasztása

Miután elmélyültem ebben az egészben, és megpróbáltam valamit csinálni a kezemmel, arra a következtetésre jutottam, hogy valamilyen mankóval foglalkozom, és úgy döntöttem, hogy megpróbálok valami mást.

világít/sor

Kiderült, hogy ez a könyvtár szorosan kötődik a Laravel infrastruktúrához és egy csomó egyéb függőséghez, így nem sok időt fordítottam rá: telepítettem, megnéztem, láttam a függőségeket és töröltem.

yiisoft/yii2-queue

Nos, itt rögtön a névből feltételezték, ismét szoros kapcsolat a Yii2-vel. Ezt a könyvtárat kellett használnom, és nem volt rossz, de nem gondoltam arra, hogy ez teljesen a Yii2-től függ.

A többit

Minden más, amit a GitHubon találtam, megbízhatatlan, elavult és félbehagyott projekt volt csillagok, elágazások és nagyszámú commit nélkül.

Vissza a symfonyhoz/messengerhez, technikai részletek

Ki kellett találnom ezt a könyvtárat, és miután több időt töltöttem, sikerült is. Kiderült, hogy minden nagyon tömör és egyszerű. A busz példányosításához készítettem egy kis gyárat, mert... Több abroncsom kellett volna, és különböző kezelőkkel.

A PHP-háttér átvitele a Redis streams buszra, és egy keretrendszertől független könyvtár kiválasztása

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

  • Üzenetkezelőket hozunk létre, amelyeknek egyszerűen hívhatónak kell lenniük
  • HandlerDescriptorba csomagoljuk (osztály a könyvtárból)
  • Ezeket a „leírókat” egy HandlersLocator példányba csomagoljuk
  • HandlersLocator hozzáadása a MessageBus példányhoz
  • A SenderInterface egy halmazát adjuk át a SendersLocatornak, az én esetemben a RedisTransport osztályok példányait, amelyek nyilvánvaló módon vannak beállítva.
  • A SendersLocator hozzáadása a MessageBus példányhoz

A MessageBus rendelkezik egy "->dispatch()" metódussal, amely megkeresi a megfelelő kezelőket a HandlersLocatorban, és átadja nekik az üzenetet a megfelelő "SenderInterface" használatával a buszon keresztül történő küldéshez (Redis folyamok).

A konténer konfigurációban (jelen esetben a php-di) ez a teljes köteg a következőképpen 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áthatja, hogy a SendersLocatorban különböző „szállításokat” rendeltünk két különböző üzenethez, amelyek mindegyike saját kapcsolattal rendelkezik a megfelelő adatfolyamokhoz.

Készítettem egy külön bemutató projektet, amely három démon alkalmazását mutatja be, amelyek a következő buszon keresztül kommunikálnak egymással: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

De megmutatom, hogyan lehet egy fogyasztót felépíteni:

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

Ennek az infrastruktúrának az alkalmazása egy alkalmazásban

Miután megvalósítottam a buszt a háttérrendszeremben, az egyes szakaszokat leválasztottam a régi szinkron parancsról, és külön kezelőket készítettem, amelyek mindegyike a maga dolgát végzi.

Az új webhely adatbázishoz való hozzáadásának folyamata így nézett ki:

A PHP-háttér átvitele a Redis streams buszra, és egy keretrendszertől független könyvtár kiválasztása

És közvetlenül ezután sokkal könnyebbé vált számomra új funkciók hozzáadása, például az Rss kibontása és elemzése. Mert ehhez a folyamathoz is szükség van az eredeti tartalomra, majd az RSS-hivatkozás kibontó kezelője, mint a WebsiteIndexHistoryPersistor, feliratkozik a „Content/HtmlContent” üzenetre, feldolgozza azt, és továbbítja a kívánt üzenetet a folyamatban.

A PHP-háttér átvitele a Redis streams buszra, és egy keretrendszertől független könyvtár kiválasztása

Végül több démonhoz jutottunk, amelyek mindegyike csak a szükséges erőforrásokkal tart kapcsolatot. Például egy démon csúszómászó tartalmazza az összes olyan kezelőt, amelyhez tartalomért az Internetre kell menni, és a démont kitartani kapcsolatot tart fenn az adatbázissal.

Most az adatbázisból történő kiválasztás helyett a szükséges azonosítók a perzisztor általi beillesztést követően egyszerűen elküldésre kerülnek a buszon keresztül minden érdeklődő kezelőhöz.

Forrás: will.com

Hozzászólás