Transferir el backend de PHP al bus de transmisiones de Redis y elegir una biblioteca independiente del marco

Transferir el backend de PHP al bus de transmisiones de Redis y elegir una biblioteca independiente del marco

prefacio

Mi sitio web, que administro como hobby, está diseñado para albergar páginas de inicio y sitios personales interesantes. Este tema empezó a interesarme desde el principio de mi andadura en programación, en ese momento me fascinaba encontrar grandes profesionales que escribieran sobre sí mismos, sus aficiones y proyectos. La costumbre de descubrirlos por mí mismo persiste hasta el día de hoy: en casi todos los sitios comerciales y no muy comerciales, sigo mirando el pie de página en busca de enlaces a los autores.

Implementación de la idea.

La primera versión era solo una página html en mi sitio web personal, donde puse enlaces con firmas en una lista ul. Después de escribir 20 páginas durante un período de tiempo, comencé a pensar que esto no era muy efectivo y decidí intentar automatizar el proceso. En Stackoverflow, noté que muchas personas indican sitios en sus perfiles, así que escribí un analizador en PHP, que simplemente revisó los perfiles, comenzando con el primero (las direcciones en SO hasta el día de hoy son así: `/users/1` ), extrajo enlaces de la etiqueta deseada y los agregó en SQLite.

Esto se puede llamar la segunda versión: una colección de decenas de miles de URL en una tabla SQLite, que reemplazó la lista estática en HTML. Hice una búsqueda simple en esta lista. Porque solo había URL, entonces la búsqueda se basaba simplemente en ellas.

En esta etapa abandoné el proyecto y volví a él después de mucho tiempo. En esta etapa mi experiencia laboral ya era de más de tres años y sentí que podía hacer algo más serio. Además, existía un gran deseo de dominar tecnologías relativamente nuevas.

Versión moderna

proyecto Implementada en Docker, la base de datos se transfirió a mongoDb y, más recientemente, se agregó radish, que al principio era solo para almacenamiento en caché. Se utiliza como base uno de los microframeworks PHP.

problema

Los sitios nuevos se agregan mediante un comando de consola que sincrónicamente hace lo siguiente:

  • Descarga contenido por URL
  • Establece una bandera que indica si HTTPS estaba disponible
  • Preserva la esencia del sitio web.
  • El HTML de origen y los encabezados se guardan en el historial de "indexación"
  • Analiza el contenido, extrae el título y la descripción.
  • Guarda datos en una colección separada

Esto fue suficiente para simplemente almacenar sitios y mostrarlos en una lista:

Transferir el backend de PHP al bus de transmisiones de Redis y elegir una biblioteca independiente del marco

Pero la idea de indexar, categorizar y clasificar todo automáticamente, manteniendo todo actualizado, no encajaba bien en este paradigma. Incluso simplemente agregar un método web para agregar páginas requería duplicación y bloqueo de código para evitar posibles DDoS.

En general, por supuesto, todo se puede hacer de forma sincrónica, y en el método web simplemente puedes guardar la URL para que el monstruoso demonio realice todas las tareas para las URL de la lista. Pero aun así, incluso aquí se sugiere la palabra “cola”. Y si se implementa una cola, todas las tareas se pueden dividir y realizar al menos de forma asincrónica.

Solución

Implemente colas y cree un sistema basado en eventos para procesar todas las tareas. Y hace tiempo que quiero probar Redis Streams.

Usando transmisiones de Redis en PHP

Porque Como mi framework no es uno de los tres gigantes Symfony, Laravel, Yii, me gustaría encontrar una biblioteca independiente. Pero, como resultó (tras un primer examen), es imposible encontrar bibliotecas serias individuales. Todo lo relacionado con las colas es un proyecto de 3 confirmaciones de hace cinco años o está vinculado al marco.

He oído mucho sobre Symfony como proveedor de componentes útiles individuales y ya uso algunos de ellos. Y también se pueden usar algunas cosas de Laravel, por ejemplo su ORM, sin la presencia del framework en sí.

Symfony/mensajero

El primer candidato me pareció inmediatamente ideal y sin duda lo instalé. Pero resultó más difícil buscar en Google ejemplos de uso fuera de Symfony. ¿Cómo ensamblar un bus para pasar mensajes a partir de un montón de clases con nombres universales y sin sentido, e incluso en Redis?

Transferir el backend de PHP al bus de transmisiones de Redis y elegir una biblioteca independiente del marco

La documentación en el sitio oficial era bastante detallada, pero la inicialización solo se describió para Symfony usando su YML favorito y otros métodos mágicos para quienes no son sinfonistas. No tenía ningún interés en el proceso de instalación en sí, especialmente durante las vacaciones de Año Nuevo. Pero tuve que hacer esto durante un tiempo inesperadamente largo.

Tratar de descubrir cómo crear una instancia de un sistema utilizando fuentes de Symfony tampoco es la tarea más trivial para un plazo ajustado:

Transferir el backend de PHP al bus de transmisiones de Redis y elegir una biblioteca independiente del marco

Después de profundizar en todo esto e intentar hacer algo con las manos, llegué a la conclusión de que estaba haciendo una especie de muletas y decidí probar con otra cosa.

iluminado/cola

Resultó que esta biblioteca estaba estrechamente ligada a la infraestructura de Laravel y a muchas otras dependencias, por lo que no le dediqué mucho tiempo: la instalé, la miré, vi las dependencias y la eliminé.

yiisoft/yii2-cola

Bueno, aquí se supuso inmediatamente por el nombre, nuevamente, una conexión estricta con Yii2. Tuve que usar esta biblioteca y no estuvo mal, pero no pensé en el hecho de que depende completamente de Yii2.

El resto

Todo lo demás que encontré en GitHub fueron proyectos poco confiables, obsoletos y abandonados sin estrellas, bifurcaciones y una gran cantidad de confirmaciones.

Volver a symfony/messenger, detalles técnicos

Tenía que descubrir esta biblioteca y, después de dedicar un poco más de tiempo, pude hacerlo. Resultó que todo era bastante conciso y sencillo. Para crear una instancia del autobús, hice una pequeña fábrica, porque... Se suponía que tenía varios neumáticos y con diferentes manejadores.

Transferir el backend de PHP al bus de transmisiones de Redis y elegir una biblioteca independiente del marco

Sólo unos pocos pasos:

  • Creamos controladores de mensajes que deberían ser simplemente invocables.
  • Los envolvemos en HandlerDescriptor (clase de la biblioteca)
  • Envolvemos estos "Descriptores" en una instancia de HandlersLocator
  • Agregar HandlersLocator a la instancia de MessageBus
  • Pasamos un conjunto de `SenderInterface` a SendersLocator, en mi caso instancias de clases `RedisTransport`, que están configuradas de una manera obvia.
  • Agregar SendersLocator a la instancia de MessageBus

MessageBus tiene un método `->dispatch()` que busca los controladores apropiados en HandlersLocator y les pasa el mensaje, utilizando la `SenderInterface` correspondiente para enviar a través del bus (flujos de Redis).

En la configuración del contenedor (en este caso php-di), todo este paquete se puede 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í puedes ver que en SendersLocator hemos asignado diferentes "transportes" para dos mensajes diferentes, cada uno de los cuales tiene su propia conexión a los flujos correspondientes.

Hice un proyecto de demostración por separado que demuestra una aplicación de tres demonios que se comunican entre sí mediante el siguiente bus: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

Pero te mostraré cómo se puede 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();

Usando esta infraestructura en una aplicación

Después de implementar el bus en mi backend, separé las etapas individuales del antiguo comando sincrónico e hice controladores separados, cada uno de los cuales hace lo suyo.

El proceso para agregar un nuevo sitio a la base de datos se veía así:

Transferir el backend de PHP al bus de transmisiones de Redis y elegir una biblioteca independiente del marco

E inmediatamente después de eso, me resultó mucho más fácil agregar nuevas funciones, por ejemplo, extraer y analizar Rss. Porque este proceso también requiere el contenido original, luego el controlador del extractor de enlaces RSS, como WebsiteIndexHistoryPersistor, se suscribe al mensaje "Contenido/Contenido HTML", lo procesa y pasa el mensaje deseado a lo largo de su canalización.

Transferir el backend de PHP al bus de transmisiones de Redis y elegir una biblioteca independiente del marco

Al final, terminamos con varios demonios, cada uno de los cuales mantiene conexiones solo con los recursos necesarios. Por ejemplo un demonio crawlers contiene todos los controladores que requieren ir a Internet para obtener contenido, y el demonio persistir mantiene una conexión con la base de datos.

Ahora, en lugar de seleccionar de la base de datos, los identificadores requeridos después de que el persistente los inserte simplemente se transmiten a través del bus a todos los controladores interesados.

Fuente: habr.com

Añadir un comentario