Transferindo o back-end PHP para o barramento de fluxos Redis e escolhendo uma biblioteca independente de estrutura

Transferindo o back-end PHP para o barramento de fluxos Redis e escolhendo uma biblioteca independente de estrutura

Prefácio

Meu site, que administro como hobby, foi projetado para hospedar páginas iniciais e sites pessoais interessantes. Esse tema começou a me interessar logo no início da minha jornada como programador, naquele momento fiquei fascinado por encontrar grandes profissionais que escrevem sobre si mesmos, seus hobbies e projetos. O hábito de descobri-los por mim mesmo permanece até hoje: em quase todos os sites comerciais e não muito comerciais, continuo a procurar no rodapé em busca de links para os autores.

Implementação da ideia

A primeira versão era apenas uma página html no meu site pessoal, onde colocava links com assinaturas em uma lista ul. Depois de digitar 20 páginas durante um período de tempo, comecei a pensar que isso não era muito eficaz e decidi tentar automatizar o processo. No stackoverflow, notei que muitas pessoas indicam sites em seus perfis, então escrevi um parser em php, que simplesmente percorreu os perfis, começando pelo primeiro (os endereços no SO até hoje são assim: `/users/1` ), extraiu links da tag desejada e adicionou no SQLite.

Isso pode ser chamado de segunda versão: uma coleção de dezenas de milhares de URLs em uma tabela SQLite, que substituiu a lista estática em HTML. Fiz uma pesquisa simples nesta lista. Porque havia apenas URLs, então a pesquisa era simplesmente baseada neles.

Nesta fase abandonei o projeto e voltei a ele depois de muito tempo. Nesta fase, a minha experiência profissional já era de mais de três anos e senti que poderia fazer algo mais sério. Além disso, havia um grande desejo de dominar tecnologias relativamente novas.

Versão moderna

Projeto implantado no Docker, o banco de dados foi transferido para o mongoDb e, mais recentemente, foi adicionado o rabanete, que a princípio era apenas para armazenamento em cache. Um dos microframeworks PHP é usado como base.

problema

Novos sites são adicionados por um comando de console que faz o seguinte de forma síncrona:

  • Baixa conteúdo por URL
  • Define um sinalizador indicando se HTTPS estava disponível
  • Preserva a essência do site
  • O HTML de origem e os cabeçalhos são salvos no histórico de “indexação”
  • Analisa o conteúdo, extrai título e descrição
  • Salva dados em uma coleção separada

Isso foi suficiente para simplesmente armazenar sites e exibi-los em uma lista:

Transferindo o back-end PHP para o barramento de fluxos Redis e escolhendo uma biblioteca independente de estrutura

Mas a ideia de indexar, categorizar e classificar tudo automaticamente, mantendo tudo atualizado, não se enquadrava bem neste paradigma. Até mesmo a simples adição de um método da web para adicionar páginas exigia duplicação e bloqueio de código para evitar possíveis DDoS.

Em geral, é claro, tudo pode ser feito de forma síncrona, e no método web você pode simplesmente salvar a URL para que o daemon monstruoso execute todas as tarefas para as URLs da lista. Mas ainda assim, mesmo aqui a palavra “fila” se sugere. E se uma fila for implementada, todas as tarefas poderão ser divididas e executadas pelo menos de forma assíncrona.

Solução

Implemente filas e crie um sistema orientado a eventos para processar todas as tarefas. E há muito tempo que quero experimentar o Redis Streams.

Usando fluxos Redis em PHP

Porque Como meu framework não é um dos três gigantes Symfony, Laravel, Yii, gostaria de encontrar uma biblioteca independente. Mas, como se viu (no primeiro exame), é impossível encontrar bibliotecas individuais sérias. Tudo relacionado às filas é um projeto de 3 commits há cinco anos ou está vinculado à estrutura.

Já ouvi muito sobre o Symfony como fornecedor de componentes úteis individuais e já uso alguns deles. E também algumas coisas do Laravel também podem ser utilizadas, por exemplo seu ORM, sem a presença do próprio framework.

symfony/mensageiro

O primeiro candidato pareceu-me imediatamente ideal e sem dúvida instalei-o. Mas acabou sendo mais difícil pesquisar no Google exemplos de uso fora do Symfony. Como montar a partir de um monte de classes com nomes universais e sem sentido, um barramento para passagem de mensagens e até no Redis?

Transferindo o back-end PHP para o barramento de fluxos Redis e escolhendo uma biblioteca independente de estrutura

A documentação no site oficial era bastante detalhada, mas a inicialização só foi descrita para o Symfony usando seu YML favorito e outros métodos mágicos para quem não é sinfonista. Não tive interesse no processo de instalação em si, principalmente durante os feriados de Ano Novo. Mas tive que fazer isso por um tempo inesperadamente longo.

Tentar descobrir como instanciar um sistema usando fontes Symfony também não é a tarefa mais trivial para um prazo apertado:

Transferindo o back-end PHP para o barramento de fluxos Redis e escolhendo uma biblioteca independente de estrutura

Depois de me aprofundar em tudo isso e tentar fazer algo com as mãos, cheguei à conclusão de que estava fazendo algum tipo de muleta e resolvi tentar outra coisa.

iluminado/fila

Acontece que essa biblioteca estava fortemente ligada à infraestrutura do Laravel e a um monte de outras dependências, então não gastei muito tempo com ela: instalei, olhei, vi as dependências e apaguei.

yiisoft/yii2-queue

Bem, aqui foi imediatamente assumido pelo nome, novamente, uma conexão estrita com Yii2. Tive que usar essa biblioteca e não foi ruim, mas não pensei no fato de que ela depende totalmente do Yii2.

O resto

Todo o resto que encontrei no GitHub eram projetos não confiáveis, desatualizados e abandonados, sem estrelas, forks e um grande número de commits.

Voltar para symfony/messenger, detalhes técnicos

Tive que descobrir essa biblioteca e, depois de passar mais algum tempo, consegui. Acontece que tudo era bastante conciso e simples. Para instanciar o ônibus, fiz uma pequena fábrica, porque... Eu deveria ter vários pneus e com diferentes manipuladores.

Transferindo o back-end PHP para o barramento de fluxos Redis e escolhendo uma biblioteca independente de estrutura

Apenas alguns passos:

  • Criamos manipuladores de mensagens que devem ser simplesmente chamáveis
  • Nós os envolvemos em HandlerDescriptor (classe da biblioteca)
  • Envolvemos esses “Descritores” em uma instância HandlersLocator
  • Adicionando HandlersLocator à instância MessageBus
  • Passamos um conjunto de `SenderInterface` para SendersLocator, no meu caso instâncias de classes `RedisTransport`, que são configuradas de forma óbvia
  • Adicionando SendersLocator à instância MessageBus

MessageBus possui um método `->dispatch()` que procura os manipuladores apropriados no HandlersLocator e passa a mensagem para eles, usando o `SenderInterface` correspondente para enviar através do barramento (streams Redis).

Na configuração do contêiner (neste caso php-di), todo esse pacote pode ser configurado assim:

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

Aqui você pode ver que no SendersLocator atribuímos diferentes “transportes” para duas mensagens diferentes, cada uma delas com sua própria conexão com os fluxos correspondentes.

Fiz um projeto de demonstração separado demonstrando uma aplicação de três daemons comunicando-se entre si usando o seguinte barramento: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus.

Mas vou mostrar como um consumidor pode ser estruturado:

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 em um aplicativo

Depois de implementar o barramento em meu back-end, separei estágios individuais do antigo comando síncrono e criei manipuladores separados, cada um fazendo sua própria coisa.

O pipeline para adicionar um novo site ao banco de dados ficou assim:

Transferindo o back-end PHP para o barramento de fluxos Redis e escolhendo uma biblioteca independente de estrutura

E imediatamente depois disso, ficou muito mais fácil adicionar novas funcionalidades, por exemplo, extrair e analisar Rss. Porque esse processo também requer o conteúdo original, então o manipulador extrator de link RSS, como WebsiteIndexHistoryPersistor, assina a mensagem “Content/HtmlContent”, processa-a e passa a mensagem desejada ao longo de seu pipeline.

Transferindo o back-end PHP para o barramento de fluxos Redis e escolhendo uma biblioteca independente de estrutura

No final, acabamos com vários daemons, cada um dos quais mantém conexões apenas com os recursos necessários. Por exemplo, um demônio crawlers contém todos os manipuladores que exigem acesso à Internet para obter conteúdo, e o daemon persistir mantém uma conexão com o banco de dados.

Agora, em vez de selecionar no banco de dados, os ids necessários após a inserção pelo persister são simplesmente transmitidos através do barramento para todos os manipuladores interessados.

Fonte: habr.com

Adicionar um comentário