Transferir el backend PHP al bus de fluxos de Redis i triar una biblioteca independent del marc

Transferir el backend PHP al bus de fluxos de Redis i triar una biblioteca independent del marc

Prefaci

El meu lloc web, que porto com a hobby, està dissenyat per allotjar pàgines d'inici interessants i llocs personals. Aquest tema va començar a interessar-me al principi del meu viatge de programació; en aquell moment em va fascinar trobar grans professionals que escriuen sobre ells mateixos, les seves aficions i projectes. L'hàbit de descobrir-los per mi mateix es manté fins als nostres dies: a gairebé tots els llocs comercials i poc comercials, continuo buscant al peu de pàgina a la recerca d'enllaços als autors.

Implementació de la idea

La primera versió era només una pàgina html al meu lloc web personal, on vaig posar enllaços amb signatures en una llista ul. Després d'haver escrit 20 pàgines durant un període de temps, vaig començar a pensar que això no era gaire efectiu i vaig decidir intentar automatitzar el procés. A stackoverflow, em vaig adonar que moltes persones indicaven llocs als seus perfils, així que vaig escriure un analitzador en php, que simplement va passar pels perfils, començant pel primer (les adreces de SO fins avui són així: `/users/1` ), va extreure enllaços de l'etiqueta desitjada i l'ha afegit a SQLite.

Això es pot anomenar la segona versió: una col·lecció de desenes de milers d'URL en una taula SQLite, que va substituir la llista estàtica en html. Vaig fer una cerca senzilla en aquesta llista. Perquè només hi havia URL, llavors la cerca es basava simplement en ells.

En aquesta etapa vaig abandonar el projecte i hi vaig tornar després de molt de temps. En aquesta etapa, la meva experiència laboral ja era de més de tres anys i sentia que podia fer alguna cosa més seriosa. A més, hi havia un gran desig de dominar tecnologies relativament noves.

Versió moderna

Projecte desplegat a Docker, la base de dades es va transferir a mongoDb i, més recentment, es va afegir rave, que al principi era només per a la memòria cau. Un dels microframeworks PHP s'utilitza com a base.

problema

Els llocs nous s'afegeixen mitjançant una ordre de consola que fa el següent de manera sincrònica:

  • Baixa contingut per URL
  • Estableix una marca que indica si HTTPS estava disponible
  • Conserva l'essència del lloc web
  • L'HTML font i les capçaleres es guarden a l'historial de "indexació".
  • Analitza el contingut, extreu el títol i la descripció
  • Desa les dades en una col·lecció separada

Això va ser suficient per emmagatzemar llocs i mostrar-los en una llista:

Transferir el backend PHP al bus de fluxos de Redis i triar una biblioteca independent del marc

Però la idea d'indexar, categoritzar i classificar-ho tot automàticament, mantenir-ho tot actualitzat, no encaixava bé en aquest paradigma. Fins i tot simplement afegir un mètode web per afegir pàgines requeria la duplicació de codi i el bloqueig per evitar possibles DDoS.

En general, per descomptat, tot es pot fer de manera sincrònica, i en el mètode web podeu simplement desar l'URL perquè el dimoni monstruós realitzi totes les tasques dels URL de la llista. Però tot i així, fins i tot aquí la paraula "cua" es suggereix. I si s'implementa una cua, totes les tasques es poden dividir i realitzar almenys de manera asíncrona.

decisió

Implementar cues i crear un sistema basat en esdeveniments per processar totes les tasques. I feia temps que volia provar Redis Streams.

Ús de fluxos de Redis en PHP

Perquè Com que el meu marc no és un dels tres gegants Symfony, Laravel, Yii, m'agradaria trobar una biblioteca independent. Però, com va resultar (en el primer examen), és impossible trobar biblioteques serioses individuals. Tot el relacionat amb les cues és un projecte de 3 commits fa cinc anys, o està lligat al marc.

He sentit parlar molt de Symfony com a proveïdor de components útils individuals i ja en faig servir alguns. I també es poden utilitzar algunes coses de Laravel, per exemple el seu ORM, sense la presència del propi framework.

symfony/messenger

El primer candidat de seguida em va semblar ideal i sense cap mena de dubte el vaig instal·lar. Però va resultar més difícil buscar a Google exemples d'ús fora de Symfony. Com muntar un autobús per passar missatges d'un munt de classes amb noms universals i sense sentit, i fins i tot a Redis?

Transferir el backend PHP al bus de fluxos de Redis i triar una biblioteca independent del marc

La documentació del lloc oficial era força detallada, però la inicialització només es va descriure per a Symfony utilitzant el seu YML favorit i altres mètodes màgics per als no simfonistes. No tenia cap interès en el procés d'instal·lació en si, especialment durant les vacances d'Any Nou. Però vaig haver de fer això durant un temps inesperadament llarg.

Intentar esbrinar com crear una instancia d'un sistema amb fonts de Symfony tampoc és la tasca més trivial per a un termini ajustat:

Transferir el backend PHP al bus de fluxos de Redis i triar una biblioteca independent del marc

Després d'aprofundir en tot això i intentar fer alguna cosa amb les mans, vaig arribar a la conclusió que estava fent una mena de crosses i vaig decidir provar una altra cosa.

il·luminat/cua

Va resultar que aquesta biblioteca estava estretament lligada a la infraestructura de Laravel i a un munt d'altres dependències, així que no hi vaig dedicar gaire temps: la vaig instal·lar, la vaig mirar, vaig veure les dependències i la vaig esborrar.

yiisoft/yii2-queue

Bé, aquí es va assumir immediatament del nom, de nou, una connexió estricta amb Yii2. Vaig haver d'utilitzar aquesta biblioteca i no estava malament, però no vaig pensar en el fet que depèn completament de Yii2.

La resta

Tota la resta que vaig trobar a GitHub eren projectes poc fiables, obsolets i abandonats sense estrelles, forquilles i un gran nombre de commits.

Tornar a symfony/messenger, detalls tècnics

Vaig haver d'esbrinar aquesta biblioteca i, després de passar una estona més, vaig poder. Va resultar que tot era bastant concís i senzill. Per instància de l'autobús, vaig fer una petita fàbrica, perquè... Se suposa que havia de tenir diversos pneumàtics i amb diferents manipuladors.

Transferir el backend PHP al bus de fluxos de Redis i triar una biblioteca independent del marc

Només uns quants passos:

  • Creem gestors de missatges que haurien de ser senzillament trucables
  • Els emboliquem a HandlerDescriptor (classe de la biblioteca)
  • Embolcallem aquests "descriptors" en una instància de HandlersLocator
  • Afegint HandlersLocator a la instància MessageBus
  • Passem un conjunt de "SenderInterface" a SendersLocator, en el meu cas instàncies de classes "RedisTransport", que es configuren d'una manera òbvia
  • Afegint SendersLocator a la instància de MessageBus

MessageBus té un mètode `->dispatch()` que cerca els controladors adequats al HandlersLocator i els passa el missatge, utilitzant la `SenderInterface' corresponent per enviar-lo a través del bus (streams Redis).

A la configuració del contenidor (en aquest cas php-di), tot aquest paquet es pot configurar així:

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

Aquí podeu veure que a SendersLocator hem assignat diferents "transports" per a dos missatges diferents, cadascun dels quals té la seva pròpia connexió amb els fluxos corresponents.

Vaig fer un projecte de demostració independent que demostrava una aplicació de tres dimonis que es comuniquen entre ells mitjançant el bus següent: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

Però us mostraré com es pot estructurar un consumidor:

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

Ús d'aquesta infraestructura en una aplicació

Després d'haver implementat l'autobús al meu backend, vaig separar les etapes individuals de l'antic comandament síncron i vaig fer controladors separats, cadascun dels quals fa les seves coses.

El pipeline per afegir un lloc nou a la base de dades tenia aquest aspecte:

Transferir el backend PHP al bus de fluxos de Redis i triar una biblioteca independent del marc

I immediatament després d'això, em va ser molt més fàcil afegir noves funcionalitats, per exemple, extreure i analitzar Rss. Perquè aquest procés també requereix el contingut original, després el controlador de l'extractor d'enllaços RSS, com WebsiteIndexHistoryPersistor, se subscriu al missatge "Contingut/HtmlContent", el processa i passa el missatge desitjat al llarg del seu pipeline.

Transferir el backend PHP al bus de fluxos de Redis i triar una biblioteca independent del marc

Al final, vam acabar amb diversos dimonis, cadascun dels quals només manté connexions amb els recursos necessaris. Per exemple un dimoni rastrejadors conté tots els controladors que requereixen anar a Internet per obtenir contingut i el dimoni persistir manté una connexió a la base de dades.

Ara, en lloc de seleccionar de la base de dades, els identificadors necessaris després de la inserció pel persistent es transmeten simplement a través del bus a tots els gestors interessats.

Font: www.habr.com

Afegeix comentari