Trasferimento del backend PHP sul bus dei flussi Redis e scelta di una libreria indipendente dal framework

Trasferimento del backend PHP sul bus dei flussi Redis e scelta di una libreria indipendente dal framework

prefazione

Il mio sito web, che gestisco per hobby, è progettato per ospitare home page e siti personali interessanti. Questo argomento ha iniziato a interessarmi proprio all'inizio del mio percorso di programmazione; in quel momento ero affascinato dal trovare grandi professionisti che scrivono di se stessi, dei loro hobby e progetti. L'abitudine di scoprirli da solo rimane ancora oggi: su quasi tutti i siti commerciali e poco commerciali, continuo a guardare nel footer alla ricerca di link agli autori.

Realizzazione dell'idea

La prima versione era semplicemente una pagina html sul mio sito web personale, dove inserisco collegamenti con firme in un elenco UL. Dopo aver digitato 20 pagine per un periodo di tempo, ho iniziato a pensare che questo non fosse molto efficace e ho deciso di provare ad automatizzare il processo. Su StackOverflow, ho notato che molte persone indicano siti nei loro profili, quindi ho scritto un parser in php, che semplicemente esaminava i profili, iniziando dal primo (gli indirizzi su SO fino ad oggi sono così: `/users/1` ), ha estratto i collegamenti dal tag desiderato e lo ha aggiunto in SQLite.

Questa può essere chiamata la seconda versione: una raccolta di decine di migliaia di URL in una tabella SQLite, che ha sostituito l'elenco statico in HTML. Ho fatto una semplice ricerca in questa lista. Perché c'erano solo URL, quindi la ricerca si basava semplicemente su di essi.

A questo punto ho abbandonato il progetto e ci sono tornato dopo molto tempo. A questo punto la mia esperienza lavorativa era già di più di tre anni e sentivo che avrei potuto fare qualcosa di più serio. Inoltre, c'era un grande desiderio di padroneggiare tecnologie relativamente nuove.

Versione moderna

Progetto distribuito in Docker, il database è stato trasferito su mongoDb e, più recentemente, è stato aggiunto radish, che inizialmente era solo per la memorizzazione nella cache. Come base viene utilizzato uno dei microframework PHP.

Problema

I nuovi siti vengono aggiunti da un comando della console che esegue in modo sincrono quanto segue:

  • Scarica il contenuto tramite URL
  • Imposta un flag che indica se HTTPS era disponibile
  • Preserva l'essenza del sito web
  • L'HTML di origine e le intestazioni vengono salvati nella cronologia di "indicizzazione".
  • Analizza il contenuto, estrae titolo e descrizione
  • Salva i dati in una raccolta separata

Questo era sufficiente per memorizzare semplicemente i siti e visualizzarli in un elenco:

Trasferimento del backend PHP sul bus dei flussi Redis e scelta di una libreria indipendente dal framework

Ma l’idea di indicizzare, categorizzare e classificare automaticamente tutto, mantenendo tutto aggiornato, non si adattava bene a questo paradigma. Anche la semplice aggiunta di un metodo Web per aggiungere pagine richiedeva la duplicazione e il blocco del codice per evitare potenziali attacchi DDoS.

In generale, ovviamente, tutto può essere fatto in modo sincrono e nel metodo web puoi semplicemente salvare l'URL in modo che il mostruoso demone esegua tutte le attività per gli URL dall'elenco. Ma anche qui la parola “coda” suggerisce la sua presenza. E se viene implementata una coda, tutte le attività possono essere divise ed eseguite almeno in modo asincrono.

Soluzione

Implementa le code e crea un sistema basato sugli eventi per l'elaborazione di tutte le attività. E volevo provare Redis Streams da molto tempo.

Utilizzo dei flussi Redis in PHP

Perché Dato che il mio framework non è uno dei tre giganti Symfony, Laravel, Yii, mi piacerebbe trovare una libreria indipendente. Ma, come si è scoperto (al primo esame), è impossibile trovare singole biblioteche serie. Tutto ciò che riguarda le code è un progetto di 3 commit cinque anni fa oppure è legato al framework.

Ho sentito molto parlare di Symfony come fornitore di singoli componenti utili e ne uso già alcuni. E anche alcune cose di Laravel possono essere utilizzate, ad esempio il loro ORM, senza la presenza del framework stesso.

symfony/messaggero

Il primo candidato mi è sembrato subito l'ideale e senza alcun dubbio l'ho installato. Ma si è rivelato più difficile cercare su Google esempi di utilizzo al di fuori di Symfony. Come assemblare un gruppo di classi con nomi universali e privi di significato, un bus per il passaggio dei messaggi e persino su Redis?

Trasferimento del backend PHP sul bus dei flussi Redis e scelta di una libreria indipendente dal framework

La documentazione sul sito ufficiale era piuttosto dettagliata, ma l'inizializzazione è stata descritta solo per Symfony utilizzando il loro YML preferito e altri metodi magici per i non sinfonisti. Non avevo alcun interesse per il processo di installazione in sé, soprattutto durante le vacanze di Capodanno. Ma ho dovuto farlo per un tempo inaspettatamente lungo.

Anche cercare di capire come istanziare un sistema usando i sorgenti Symfony non è il compito più banale con una scadenza ravvicinata:

Trasferimento del backend PHP sul bus dei flussi Redis e scelta di una libreria indipendente dal framework

Dopo aver approfondito tutto questo e aver provato a fare qualcosa con le mani, sono giunto alla conclusione che stavo facendo una specie di stampelle e ho deciso di provare qualcos'altro.

illuminato/coda

Si è scoperto che questa libreria era strettamente legata all'infrastruttura Laravel e a una serie di altre dipendenze, quindi non ci ho dedicato molto tempo: l'ho installata, l'ho guardata, ho visto le dipendenze e l'ho cancellata.

yiisoft/yii2-queue

Ebbene, qui si è subito ipotizzato dal nome, ancora una volta, uno stretto legame con Yii2. Ho dovuto usare questa libreria e non era male, ma non pensavo al fatto che dipenda completamente da Yii2.

Il resto

Tutto il resto che ho trovato su GitHub erano progetti inaffidabili, obsoleti e abbandonati senza stelle, fork e un gran numero di commit.

Ritorna a symfony/messenger, dettagli tecnici

Dovevo capire questa libreria e, dopo averci passato un po' più di tempo, ci sono riuscito. Si è scoperto che tutto era abbastanza conciso e semplice. Per creare un'istanza dell'autobus, ho creato una piccola fabbrica, perché... Avrei dovuto avere diversi pneumatici e con diversi gestori.

Trasferimento del backend PHP sul bus dei flussi Redis e scelta di una libreria indipendente dal framework

Solo pochi passaggi:

  • Creiamo gestori di messaggi che dovrebbero essere semplicemente richiamabili
  • Li avvolgiamo in HandlerDescriptor (classe dalla libreria)
  • Racchiudiamo questi "Descrittori" in un'istanza HandlersLocator
  • Aggiunta di HandlersLocator all'istanza MessageBus
  • Passiamo un set di `SenderInterface` a SendersLocator, nel mio caso istanze di classi `RedisTransport`, che sono configurate in modo ovvio
  • Aggiunta di SendersLocator all'istanza MessageBus

MessageBus ha un metodo `->dispatch()` che cerca i gestori appropriati in HandlersLocator e passa loro il messaggio, utilizzando la corrispondente `SenderInterface` per inviarlo tramite il bus (flussi Redis).

Nella configurazione del contenitore (in questo caso php-di), l'intero pacchetto può essere configurato in questo modo:

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

Qui puoi vedere che in SendersLocator abbiamo assegnato diversi “trasporti” per due messaggi diversi, ognuno dei quali ha la propria connessione ai flussi corrispondenti.

Ho realizzato un progetto demo separato che mostra un'applicazione di tre demoni che comunicano tra loro utilizzando il seguente bus: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

Ma ti mostrerò come può essere strutturato un consumatore:

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

Utilizzo di questa infrastruttura in un'applicazione

Avendo implementato il bus nel mio backend, ho separato le singole fasi dal vecchio comando sincrono e ho creato gestori separati, ognuno dei quali fa le proprie cose.

La pipeline per l'aggiunta di un nuovo sito al database era simile alla seguente:

Trasferimento del backend PHP sul bus dei flussi Redis e scelta di una libreria indipendente dal framework

E subito dopo è diventato molto più semplice per me aggiungere nuove funzionalità, ad esempio estrarre e analizzare Rss. Perché questo processo richiede anche il contenuto originale, quindi il gestore dell'estrazione del collegamento RSS, come WebsiteIndexHistoryPersistor, si iscrive al messaggio "Content/HtmlContent", lo elabora e passa ulteriormente il messaggio desiderato lungo la sua pipeline.

Trasferimento del backend PHP sul bus dei flussi Redis e scelta di una libreria indipendente dal framework

Alla fine ci siamo ritrovati con diversi demoni, ognuno dei quali mantiene le connessioni solo con le risorse necessarie. Ad esempio un demone crawler contiene tutti i gestori che richiedono l'accesso a Internet per il contenuto e il demone persistente mantiene una connessione al database.

Ora, invece di selezionarli dal database, gli ID richiesti dopo l'inserimento da parte del persistente vengono semplicemente trasmessi tramite il bus a tutti i gestori interessati.

Fonte: habr.com

Aggiungi un commento