ÖvergĂ„ng av PHP-backend till Redis streams och val av ramverksoberoende bibliotek.

ÖvergĂ„ng av PHP-backend till Redis streams och val av ramverksoberoende bibliotek.

Förord

Min webbplats, som jag jobbar med som en hobby, Àr avsedd för att lagra intressanta hemsidor och personliga sidor. Detta Àmne började intressera mig i början av min programmeringsresa, dÄ jag fascinerades av att hitta stora proffs som skriver om sig sjÀlva, sina intressen och projekt. Vanan att upptÀcka dem för mig har kvarstÄtt och nu: nÀstan pÄ varje kommersiell och mindre webbplats fortsÀtter jag att kolla i sidfoten för att hitta lÀnkar till författarna.

Genomförande av idén

Den första versionen var helt enkelt en html-sida pÄ min personliga webbplats, dÀr jag samlade lÀnkar med kommentarer i en ul-lista. Efter att ha samlat omkring 20 sidor över tid började jag tÀnka att detta inte var sÀrskilt effektivt och bestÀmde mig för att försöka automatisera processen. PÄ stackoverflow mÀrkte jag att mÄnga angav webbplatser i sina profiler, sÄ jag skrev en parser i php som helt enkelt gick igenom profilerna, började med den första (adresser pÄ SO ser fortfarande ut som `\/users\/1`), extraherade lÀnkar frÄn den önskade taggen och sparade dem i SQLite.

Detta kan kallas den andra versionen: en samling av tiotusentals URL:er i en SQLite-tabell, som ersatte den statiska listan i html. UtifrÄn denna lista skapade jag en enkel sökfunktion. Eftersom det endast fanns URL:er, var sökningen enkel sÄvÀl.

Vid detta stadium övergav jag projektet och Äterkom till det efter en lÄng tid. PÄ den tiden hade jag över tre Ärs arbetslivserfarenhet och kÀnde att jag kunde göra nÄgot mer seriöst. Dessutom fanns det en stark vilja att lÀra mig relativt nya teknologier.

Den moderna versionen

Projektet Àr driftsatt i Docker, databasen har bytts till mongoDb, och relativt nyligen har Redis lagts till, som först anvÀndes för caching. En av PHP:s mikroramverk anvÀnds som grund.

Problemet

Nya webbplatser lÀggs till med ett kommandoskript som synkront gör följande:

  • Laddar ner innehĂ„ll frĂ„n URL
  • SĂ€tter en flagga om HTTPS var tillgĂ€ngligt
  • Sparar webbplatsens entitet
  • Sparar den ursprungliga HTML-koden och rubrikerna i historiken "indexering"
  • Parserar innehĂ„llet, extraherar Title och Description
  • Sparar data i en separat samling

Detta var tillrÀckligt för att enkelt lagra webbplatser och visa dem i en lista:

ÖvergĂ„ng av PHP-backend till Redis streams och val av ramverksoberoende bibliotek.

Men idĂ©n att automatiskt indexera, kategorisera och rangordna allt och hĂ„lla allt uppdaterat passar dĂ„ligt in i detta paradigm. Även ett enkelt tillĂ€gg av en webbmethode för att lĂ€gga till sidor krĂ€vde kodduplicering och blockeringar för att undvika potentiell DDoS.

SjÀlvklart kan allt göras synkront, och i webbmethode kan man helt enkelt spara URL:en sÄ att det monstruösa demonet utför alla uppgifter för URL:er frÄn listan. Men Àven hÀr kommer ordet "kö" upp. Och om man inför köer kan man dela upp alla uppgifter och Ätminstone utföra dem asynkront.

Lösning

Införa köer och skapa ett event-driven system för att bearbeta alla uppgifter. Jag har lÀnge velat prova Redis Streams.

AnvÀnda Redis Streams i PHP

Eftersom jag inte anvĂ€nder ett ramverk frĂ„n de tre giganterna Symfony, Laravel, Yii, ville jag hitta ett oberoende bibliotek. Men, som det visade sig (vid första granskningen) – det Ă€r omöjligt att hitta seriösa separata bibliotek. Allt som rör köer Ă€r antingen ett litet projekt med tre commits frĂ„n för fem Ă„r sedan eller kopplat till ett ramverk.

Jag har hört om Symfony som en leverantör av anvÀndbara komponenter, och dessutom anvÀnder jag redan nÄgra av dem. Och frÄn Laravel gÄr det ocksÄ att anvÀnda vissa saker, till exempel deras ORM, utan att behöva ha sjÀlva ramverket.

symfony/messenger

Den första kandidaten verkade omedelbart perfekt och utan tvekan installerade jag den. Men att hitta exempel pÄ anvÀndning utanför Symfony visade sig vara svÄrare. Hur samlar man frÄn en massa klasser med generiska, meningslösa namn, en bus för att överföra meddelanden, och dessutom pÄ Redis?

ÖvergĂ„ng av PHP-backend till Redis streams och val av ramverksoberoende bibliotek.

Dokumentationen pÄ den officiella sidan var tillrÀckligt detaljerad, men initialiseringen beskrevs endast för Symfony med hjÀlp av deras Àlskade YML och andra magiska metoder för dem som inte Àr Symfony-anvÀndare. Jag hade inget intresse för sjÀlva installationsprocessen, sÀrskilt under nyÄrshelgerna. Men jag var tvungen att ge mig pÄ det och det tog ovÀntat lÄng tid.

Att försöka förstÄ instanseringen av systemet utifrÄn Symfony-kÀllkoden Àr inte heller en helt trivial uppgift med tighta tidsramar:

ÖvergĂ„ng av PHP-backend till Redis streams och val av ramverksoberoende bibliotek.

Efter att ha rotat i allt detta och försökt göra nÄgot för hand kom jag till slutsatsen att jag arbetade med nÄgot slags nödlösningar och bestÀmde mig för att prova nÄgot annat.

illuminate/queue

Det visade sig att detta bibliotek Àr djupt kopplat till Laravel-infrastrukturen och mÄnga andra beroenden, sÄ jag lade inte ner mycket tid pÄ det: jag installerade det, tittade, sÄg beroendena och tog bort det.

yiisoft/yii2-queue

HÀr antyder namnet direkt en stark koppling till Yii2. Jag har anvÀnt detta bibliotek och det var ganska bra, men jag hade inte tÀnkt pÄ att det helt beror pÄ Yii2.

Övriga

Allt annat jag hittade pÄ GitHub var opÄlitliga, förÄldrade och övergivna projekt utan stjÀrnor, forks eller ett stort antal commits.

ÅtergĂ„ng till symfony/messenger, tekniska detaljer

Jag var tvungen att sÀtta mig in i detta bibliotek och, efter att ha spenderat lite mer tid, lyckades jag. Det visade sig vara ganska enkelt och klart. För att instansiera busen skapade jag en liten fabrik, eftersom jag planerade att ha flera bussar med olika handlare.

ÖvergĂ„ng av PHP-backend till Redis streams och val av ramverksoberoende bibliotek.

Bara nÄgra steg:

  • Skapa meddelandehanterare som bara ska vara callable
  • Vi sveper in dem i HandlerDescriptor (en klass frĂ„n biblioteket)
  • Dessa "Deskriptorer" sveps in i en instans av HandlersLocator
  • LĂ€gg till HandlersLocator i en instans av MessageBus
  • Överför en uppsĂ€ttning `SenderInterface` till SendersLocator, i mitt fall instanser av klasserna `RedisTransport`, som konfigureras uppenbart
  • LĂ€gg till SendersLocator i en instans av MessageBus

MessageBus har en metod `->dispatch()`, som letar efter motsvarande hanterare i HandlersLocator och överför meddelandet till dem, med hjÀlp av motsvarande `SenderInterface` för att skicka via bussen (Redis streams).

I konfigurationen av containern (i detta fall php-di) kan hela denna koppling konfigureras sÄ hÀr:

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

HÀr kan vi se att vi har tilldelat olika "transporter" för tvÄ olika meddelanden i SendersLocator, var och en av dem har sin egen anslutning till motsvarande strömmar.

Jag har gjort ett separat demoprojekt som visar en applikation av tre demonprocesser som kommunicerar med varandra via en sÄdan buss: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

Men jag visar hur en konsument kan vara organiserad:

anvÀnd 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();

AnvÀndningen av denna infrastruktur i applikationen

Genom att implementera bussen i min backend separerade jag olika steg frÄn det gamla synkrona kommandot och gjorde separata handlare, var och en med sitt eget ansvarsomrÄde.

Pipeline för att lÀgga till en ny webbplats i databasen blev sÄ hÀr:

ÖvergĂ„ng av PHP-backend till Redis streams och val av ramverksoberoende bibliotek.

Och genast efter detta blev det mycket enklare för mig att lÀgga till nya funktioner, till exempel att extrahera och bearbeta Rss. Eftersom denna process ocksÄ krÀver ursprungsinnehÄll, prenumererar handlern som extraherar lÀnkar till rss, precis som WebsiteIndexHistoryPersistor, pÄ meddelandet "Content/HtmlContent", bearbetar det och vidarebefordrar rÀtt meddelande genom sin egen pipeline.

ÖvergĂ„ng av PHP-backend till Redis streams och val av ramverksoberoende bibliotek.

I slutÀndan blev det flera daemoner, var och en som hÄller anslutningar endast till nödvÀndiga resurser. Till exempel Àr daemonen crawlers innehÄller alla hanterare som krÀver Ätkomst till internet för innehÄll, medan daemonen persister hÄller anslutningen till databasen.

Nu, istÀllet för att göra select-frÄgor frÄn databasen, skickas de nödvÀndiga id:n efter att de har lagts till av persistern enkelt genom bussen till alla intresserade hanterare.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster