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



Prefácio


Meu site, que faço por hobby, foi projetado para armazenar páginas iniciais interessantes e sites pessoais. Esse tópico começou a me interessar no começo do meu caminho na programação; naquele momento, fiquei encantado por encontrar grandes profissionais que escrevem sobre si mesmos, seus hobbies e projetos. O hábito de descobri-los permaneceu por enquanto: em quase todos os sites comerciais e não muito, continuo pesquisando no rodapé em busca de links para autores.

Implementação da ideia


A primeira versão era apenas uma página html no meu site pessoal, onde coloquei links com assinaturas em uma lista ul. Depois de digitar 20 páginas por algum tempo, comecei a pensar que não era muito eficaz e decidi tentar automatizar o processo. No stackoverflow, notei que muitos indicam sites em seus perfis, por isso escrevi um analisador php que apenas percorreu os perfis, começando pelo primeiro (endereço no SO e até hoje como este: `/ users / 1`), links extraídos da tag desejada e empilhados no SQLite.

Isso pode ser chamado de segunda versão: uma coleção de dezenas de milhares de URLs em uma placa SQLite que substituiu a lista estática em html. Eu fiz uma pesquisa simples nesta lista. Porque havia apenas URLs, então a pesquisa era apenas para eles.

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

Versão moderna


O projeto foi implantado no docker, o banco de dados foi transferido para o mongoDb e, relativamente recentemente, foram adicionados rabanetes, que inicialmente eram apenas para cache. Como base, um dos microframes PHP é usado.

O problema


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

  • Baixar conteúdo por URL
  • Sinaliza se HTTPS estava disponível
  • Preserva a essência do site
  • HTML de origem e cabeçalhos são salvos no histórico de indexação
  • Analisa o conteúdo, recupera o Título e a Descrição
  • Salva dados em uma coleção separada.

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



Mas a ideia de indexar, categorizar e classificar tudo automaticamente, mantendo tudo atualizado, se encaixa nesse paradigma de maneira fraca. Mesmo adicionando um método da Web para adicionar páginas, é necessário duplicar e bloquear o código para evitar possíveis DDoS.

Em geral, é claro, tudo pode ser feito de forma síncrona e, no método da Web, basta salvar o URL para garantir que o monstruoso daemon execute todas as tarefas dos URLs da lista. Mas, mesmo assim, mesmo aqui a palavra "vire" implora. E se a fila for implementada, todas as tarefas poderão ser divididas e executadas pelo menos de forma assíncrona.

Solução


Introduzir filas e criar um sistema de processamento orientado a eventos para todas as tarefas. E por muito tempo eu quis experimentar o Redis Streams.

Usando fluxos Redis no PHP


Porque Não tenho uma estrutura dos três gigantes Symfony, Laravel, Yii, e gostaria de encontrar uma biblioteca independente. Mas, como se viu (no primeiro exame), é impossível encontrar bibliotecas individuais sérias. Tudo o que está associado às filas é uma projeção de 3 confirmações de cinco anos atrás ou está vinculado a uma estrutura.

Ouvi falar do Symfony como fornecedor de alguns componentes úteis e já uso alguns. E também do Laravel, algo também pode ser usado, por exemplo, seu ORM, sem a presença do próprio framework.

symfony / messenger


O primeiro candidato imediatamente pareceu ideal e, sem dúvida, eu o instalei. Mas foi mais difícil encontrar exemplos de uso no Google fora do Symfony. Como coletar de um monte de classes com nomes universais, sem falar nada, um barramento para enviar mensagens e até Redis?



A documentação no site oficial era bastante detalhada, mas a inicialização foi descrita apenas para o Symfony usando seu YML favorito e outros métodos mágicos para um não-sinfonista. Eu não tinha interesse no processo de instalação, especialmente durante os feriados de Ano Novo. Mas eu 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 por prazos apertados:



Dando uma olhada nisso e tentando fazer algo com as mãos, cheguei à conclusão de que estava fazendo algum tipo de muleta e decidi tentar outra coisa.

iluminar / fila


Aconteceu que essa biblioteca estava intimamente ligada à infraestrutura do Laravel e a muitas outras dependências, então eu não gastei muito tempo nela: instalei, olhei, vi dependências e a excluí.

fila do yiisoft / yii2


Bem, aqui foi imediatamente assumido a partir do nome, novamente uma forte ligação ao Yii2. Eu tive que usar esta biblioteca e não era ruim, mas não achei que dependesse completamente do Yii2.

O resto


Todo o resto que encontrei no github eram projeções desatualizadas e abandonadas não confiáveis, sem estrelas, garfos e um grande número de confirmações.

Voltar ao symfony / messenger, detalhes técnicos


Eu tive que lidar com essa biblioteca e, depois de passar mais algum tempo, consegui. Descobriu-se que tudo é bastante conciso e simples. Para a instanciação do ônibus, fiz uma pequena fábrica, porque Eu tinha vários pneus com manipuladores diferentes.



Apenas alguns passos:

  • Crie manipuladores de mensagens que devem ser chamados apenas
  • Coloque-os em HandlerDescriptor (classe da biblioteca)
  • Envolvemos esses "Descritores" na instância HandlersLocator.
  • Adicionar HandlersLocator à instância MessageBus
  • Passamos ao SendersLocator um conjunto de `SenderInterface`, no meu caso instâncias das classes` RedisTransport`, que são configuradas de maneira óbvia
  • Adicionar SendersLocator à instância MessageBus

O 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 pelo barramento (fluxos Redis).

Na configuração do contêiner (neste caso, php-di), todo esse grupo pode ser configurado da seguinte maneira:

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([ \App\Messages\SecretJsonMessages::class => [CONTAINER_REDIS_TRANSPORT_SECRET], \App\Messages\DaemonLogMessage::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); }, 

Pode-se observar que no SendersLocator atribuímos um "transporte" diferente para duas mensagens diferentes, cada uma com sua própria conexão com os fluxos correspondentes.

Eu fiz um projeto de demonstração separado demonstrando uma aplicação de três demônios se comunicando usando esse barramento: https://github.com/backend-university/products/tree/master/products/02-redis-streams-bus .

Mas mostrarei como um consumidor pode ser organizado:

 use App\Messages\DaemonLogMessage; use Symfony\Component\Messenger\Handler\HandlerDescriptor; use Symfony\Component\Messenger\Handler\HandlersLocator; use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware; use Symfony\Component\Messenger\Middleware\SendMessageMiddleware; use Symfony\Component\Messenger\Transport\Sender\SendersLocator; require_once __DIR__ . '/../vendor/autoload.php'; /** @var \Psr\Container\ContainerInterface $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 \Symfony\Component\Messenger\Worker($receivers, $bus, $container->get(CONTAINER_EVENT_DISPATCHER)); $w->run(); 

Usando essa infraestrutura em um aplicativo


Tendo implementado o barramento no meu back-end, selecionei etapas individuais da antiga equipe síncrona e fiz manipuladores separados, cada um dos quais envolvido em seu próprio negócio.

O pipeline para adicionar um novo site ao banco de dados é o seguinte:



E logo depois ficou muito mais fácil adicionar novas funcionalidades, por exemplo, extrair e analisar Rss. Porque Como esse processo também requer conteúdo de origem, o manipulador-extrator do link rss, bem como WebsiteIndexHistoryPersistor, assina a mensagem "Content / HtmlContent", o processa e passa a mensagem desejada em seu pipeline.



No final, resultou em vários demônios, cada um deles mantendo conexões apenas com os recursos necessários. Por exemplo, o daemon de rastreadores contém todos os manipuladores que exigem acesso à Internet para conteúdo, e o daemon persister mantém uma conexão com o banco de dados.

Agora, em vez de selecionar no banco de dados, o ID necessário após ser inserido pelo persistente é simplesmente passado pelo barramento para todos os manipuladores interessados.

Source: https://habr.com/ru/post/pt483584/


All Articles