Transferir o backend de PHP ao bus de fluxos de Redis e escoller unha biblioteca independente do marco

Transferir o backend de PHP ao bus de fluxos de Redis e escoller unha biblioteca independente do marco

Prefacio

O meu sitio web, que dirixo como hobby, está deseñado para albergar páxinas de inicio e sitios persoais interesantes. Este tema empezou a interesarme no inicio da miña andaina de programación, nese momento fascinábame atopar grandes profesionais que escribisen sobre si mesmos, as súas afeccións e proxectos. O costume de descubrilos por min mesmo segue sendo a día de hoxe: en case todos os sitios comerciais e pouco comerciais, sigo buscando no pé de páxina na procura de ligazóns aos autores.

Implementación da idea

A primeira versión era só unha páxina html no meu sitio web persoal, onde puxen ligazóns con sinaturas nunha lista ul. Despois de escribir 20 páxinas durante un período de tempo, comecei a pensar que isto non era moi efectivo e decidín tentar automatizar o proceso. En stackoverflow, notei que moitas persoas indican sitios nos seus perfís, polo que escribín un analizador en php, que simplemente pasaba polos perfís, comezando polo primeiro (os enderezos en SO ata hoxe son así: `/users/1` ), extraeu ligazóns da etiqueta desexada e engadiuno en SQLite.

Esta pode chamarse a segunda versión: unha colección de decenas de miles de URL nunha táboa SQLite, que substituíu a lista estática en HTML. Fixen unha busca sinxela nesta lista. Porque só había URL, entón a busca simplemente baseábase neles.

Nesta fase abandonei o proxecto e volvín a el despois de moito tempo. Nesta fase, a miña experiencia laboral xa era de máis de tres anos e sentía que podía facer algo máis serio. Ademais, había un gran desexo de dominar tecnoloxías relativamente novas.

Versión moderna

Proxecto despregado en Docker, a base de datos foi transferida a mongoDb e, máis recentemente, engadiuse o rabanete, que nun principio era só para almacenar na caché. Un dos microframeworks PHP utilízase como base.

problema

Os novos sitios engádense mediante un comando de consola que fai o seguinte de forma sincronizada:

  • Descarga contido por URL
  • Establece unha marca que indica se HTTPS estaba dispoñible
  • Conserva a esencia do sitio web
  • O HTML fonte e as cabeceiras gárdanse no historial de "indexación".
  • Analiza o contido, extrae o título e a descrición
  • Garda os datos nunha colección separada

Isto foi suficiente para almacenar sitios e mostralos nunha lista:

Transferir o backend de PHP ao bus de fluxos de Redis e escoller unha biblioteca independente do marco

Pero a idea de indexar, categorizar e clasificar todo automaticamente, mantendo todo actualizado, non encaixaba ben neste paradigma. Incluso simplemente engadir un método web para engadir páxinas requiriu duplicación de código e bloqueo para evitar posibles DDoS.

En xeral, por suposto, todo se pode facer de forma sincronizada, e no método web pode simplemente gardar o URL para que o monstruoso daemon realice todas as tarefas para os URL da lista. Pero aínda así, mesmo aquí suxire a palabra "cola". E se se implementa unha cola, todas as tarefas pódense dividir e realizar polo menos de forma asíncrona.

decisión

Implementar colas e crear un sistema impulsado por eventos para procesar todas as tarefas. E levaba moito tempo querendo probar Redis Streams.

Usando fluxos de Redis en PHP

Porque Como o meu marco non é un dos tres xigantes Symfony, Laravel, Yii, gustaríame atopar unha biblioteca independente. Pero, como se viu (no primeiro exame), é imposible atopar bibliotecas serias individuais. Todo o relacionado coas colas ou é un proxecto de 3 commits hai cinco anos, ou está ligado ao marco.

Escoitei moito falar de Symfony como provedor de compoñentes útiles individuais, e xa uso algúns deles. E tamén se poden usar algunhas cousas de Laravel, por exemplo o seu ORM, sen a presenza do propio framework.

symfony/messenger

O primeiro candidato pareceume enseguida ideal e sen dúbida instaleino. Pero resultou máis difícil buscar en Google exemplos de uso fóra de Symfony. Como montar unha morea de clases con nomes universais e sen sentido, un autobús para pasar mensaxes e mesmo en Redis?

Transferir o backend de PHP ao bus de fluxos de Redis e escoller unha biblioteca independente do marco

A documentación do sitio oficial foi bastante detallada, pero a inicialización só se describiu para Symfony usando o seu YML favorito e outros métodos máxicos para os non sinfonistas. Non tiña interese no proceso de instalación en si, especialmente durante as vacacións de Ano Novo. Pero tiven que facelo durante un tempo inesperadamente longo.

Tentar descubrir como crear unha instancia dun sistema usando fontes de Symfony tampouco é a tarefa máis trivial para un prazo axustado:

Transferir o backend de PHP ao bus de fluxos de Redis e escoller unha biblioteca independente do marco

Despois de afondar en todo isto e intentar facer algo coas mans, cheguei á conclusión de que estaba a facer unha especie de muletas e decidín probar outra cousa.

iluminado/cola

Resultou que esta biblioteca estaba moi ligada á infraestrutura de Laravel e a unha morea doutras dependencias, polo que non dediquei moito tempo a ela: instaleina, mireina, vin as dependencias e borreina.

yiisoft/yii2-queue

Ben, aquí asumiuse inmediatamente a partir do nome, de novo, unha conexión estrita con Yii2. Tiven que usar esta biblioteca e non estaba mal, pero non pensei no feito de que depende completamente de Yii2.

O resto

Todo o demais que atopei en GitHub eran proxectos pouco fiables, obsoletos e abandonados sen estrelas, forks e un gran número de commits.

Volver a symfony/messenger, detalles técnicos

Tiven que descubrir esta biblioteca e, despois de pasar algún tempo máis, puiden. Resultou que todo era bastante conciso e sinxelo. Para instanciar o autobús, fixen unha pequena fábrica, porque... Suponse que tiña varios pneumáticos e con diferentes manipuladores.

Transferir o backend de PHP ao bus de fluxos de Redis e escoller unha biblioteca independente do marco

Só uns pasos:

  • Creamos controladores de mensaxes que deberían ser simplemente invocables
  • Envolvémolos en HandlerDescriptor (clase da biblioteca)
  • Envolvemos estes "Descriptores" nunha instancia de HandlersLocator
  • Engadindo HandlersLocator á instancia MessageBus
  • Pasamos un conxunto de `SenderInterface` a SendersLocator, no meu caso instancias de clases `RedisTransport`, que están configuradas dunha forma obvia
  • Engadindo SendersLocator á instancia MessageBus

MessageBus ten un método `->dispatch()` que busca os controladores apropiados no HandlersLocator e lles pasa a mensaxe, usando a `SenderInterface` correspondente para enviar a través do bus (fluxos Redis).

Na configuración do contedor (neste caso php-di), todo este paquete pódese configurar así:

        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í podes ver que en SendersLocator asignamos diferentes "transportes" para dúas mensaxes diferentes, cada unha das cales ten a súa propia conexión cos fluxos correspondentes.

Fixen un proxecto de demostración separado que demostra unha aplicación de tres daemons que se comunican entre si usando o seguinte bus: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

Pero vouche mostrar como se pode estruturar 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();

Usando esta infraestrutura nunha aplicación

Despois de implementar o bus no meu backend, separei etapas individuais do antigo comando sincrónico e fixen controladores separados, cada un dos cales fai as súas propias cousas.

A canalización para engadir un novo sitio á base de datos era o seguinte:

Transferir o backend de PHP ao bus de fluxos de Redis e escoller unha biblioteca independente do marco

E inmediatamente despois diso, fíxome moito máis fácil engadir novas funcionalidades, por exemplo, extraer e analizar Rss. Porque este proceso tamén require o contido orixinal, entón o controlador do extractor de ligazóns RSS, como WebsiteIndexHistoryPersistor, subscríbese á mensaxe "Content/HtmlContent", procesa e pasa a mensaxe desexada ao longo da súa canalización.

Transferir o backend de PHP ao bus de fluxos de Redis e escoller unha biblioteca independente do marco

Ao final, acabamos con varios daemons, cada un dos cales mantén conexións só cos recursos necesarios. Por exemplo, un demo crawlers contén todos os controladores que requiren ir a Internet para buscar contido e o daemon persistir ten unha conexión coa base de datos.

Agora, en lugar de seleccionar a partir da base de datos, os identificadores necesarios despois da inserción polo persistente simplemente transmítense a través do bus a todos os controladores interesados.

Fonte: www.habr.com

Engadir un comentario