Preparando nosso produto para escalar com as filas do Laravel

Uma tradução do artigo foi preparada especialmente para os alunos do curso “Framework Laravel” .




Olá, sou Valerio, um engenheiro de software da Itália.

Este guia é destinado a todos os desenvolvedores de PHP que já possuem aplicativos on-line com usuários reais, mas que não têm um entendimento mais profundo de como implementar (ou melhorar significativamente) a escalabilidade em seu sistema usando filas do Laravel. Eu aprendi sobre o Laravel no final de 2013 no início da 5ª versão do framework. Naquele momento, eu ainda não era desenvolvedor envolvido em projetos sérios, e um dos aspectos das estruturas modernas, especialmente no Laravel, que me pareceu o mais incompreensível, foram as filas.

Ao ler a documentação, imaginei o potencial, mas sem experiência real em desenvolvimento, tudo isso permaneceu apenas no nível das teorias em minha cabeça.
Hoje, eu sou o criador Inspector.dev- painel em tempo real , que realiza milhares de tarefas a cada hora, e eu entendo essa arquitetura muito melhor do que antes.



Neste artigo, mostrarei como descobri filas e tarefas e quais configurações me ajudaram a processar grandes quantidades de dados em tempo real, economizando recursos do servidor.

Introdução


Quando um aplicativo PHP recebe uma solicitação http de entrada, nosso código é executado sequencialmente passo a passo até que a solicitação seja concluída e uma resposta seja retornada ao cliente (por exemplo, o navegador do usuário). Esse comportamento síncrono é verdadeiramente intuitivo, previsível e fácil de entender. Enviei uma solicitação http para o terminal, o aplicativo extrai dados do banco de dados, converte-os para o formato apropriado, executa algumas tarefas adicionais e as envia de volta. Tudo acontece linearmente.

Tarefas e filas apresentam comportamento assíncrono que viola esse fluxo de linha. É por isso que esses recursos me pareceram um pouco estranhos no começo.
Mas, às vezes, uma solicitação http recebida pode desencadear um ciclo de tarefas demoradas, por exemplo, enviando notificações por email a todos os membros da equipe. Isso pode significar o envio de seis ou dez e-mails, o que pode levar quatro ou cinco segundos. Portanto, toda vez que o usuário clica no botão correspondente, ele precisa esperar cinco segundos antes de poder continuar usando o aplicativo.

Quanto mais o aplicativo cresce, pior esse problema se torna.

O que é uma tarefa?


Um trabalho é uma classe que implementa o método handle que contém a lógica que queremos executar de forma assíncrona.

<?php

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Bus\Queueable;


class CallExternalAPI implements ShouldQueue
{
    use Dispatchable,
        InteractsWithQueue,
        Queueable;
        
    /**
     * @var string
     */
    protected $url;

    /**
     *    .
     *
     * @param array $Data
     */
    public function __construct($url)
    {
        $this->url = $url;
    }
    

    /**
     *  ,   .
     *
     * @return void
     * @throws \Throwable
     */
    public function handle()
    {
        file_get_contents($this->url);
    }
}

Como mencionado acima, o principal motivo para concluir um trecho de código no Job é concluir uma tarefa demorada, sem forçar o usuário a aguardar a conclusão.

O que queremos dizer com "tarefas trabalhosas"?


Esta é uma pergunta natural. O envio de e-mails é o exemplo mais comum usado em artigos que falam sobre filas, mas quero falar sobre a experiência real do que eu precisava fazer.

Como proprietário de um produto, é muito importante para mim sincronizar as informações de viagem do usuário com nossas ferramentas de marketing e suporte ao cliente. Assim, com base nas ações do usuário, atualizamos as informações do usuário em vários softwares externos via API (ou chamadas http externas) para fins de serviço e marketing.
Um dos pontos de extremidade mais usados ​​no meu aplicativo pode enviar 10 e-mails e fazer 3 chamadas http para serviços externos antes da conclusão. Nenhum usuário vai esperar tanto tempo - provavelmente, todos eles simplesmente param de usar meu aplicativo.

Graças às filas, posso encapsular todas essas tarefas nas classes alocadas, transferir as informações necessárias para a execução de seu trabalho para o construtor e agendá-las para serem concluídas em uma data posterior em segundo plano, para que meu controlador possa retornar imediatamente a resposta.

<?php

class ProjectController 
{
    public function store(Request $request)
    {
        $project = Project::create($request->all());
        
        //  NotifyMembers, TagUserActive, NotifyToProveSource 
        //   ,     
        Notification::queue(new NotifyMembers($project->owners));
        $this->dispatch(new TagUserAsActive($project->owners));
        $this->dispatch(new NotifyToProveSource($project->owners));
        
        return $project;
    }
}

Não preciso aguardar a conclusão de todos esses processos antes de retornar a resposta; pelo contrário, a espera será igual ao tempo necessário para publicá-los na fila. E isso significa a diferença entre 10 segundos e 10 milissegundos !!!

Quem executa essas tarefas depois de enviá-las para a fila?


Essa é a arquitetura clássica de editor / consumidor . Já publicamos nossas tarefas na fila do controlador, agora vamos entender como a fila é usada e, finalmente, as tarefas são executadas.



Para usar a fila, precisamos executar um dos comandos artesanais mais populares:

php artisan queue:work

Como diz a documentação: O
Laravel inclui um trabalhador de fila que processa novas tarefas quando estão na fila.

Ótimo! O Laravel fornece uma interface pronta para tarefas de enfileiramento e um comando pronto para usar para recuperar tarefas da fila e executar seu código em segundo plano.

Função de supervisor


Foi outra "coisa estranha" no começo. Eu acho que é normal descobrir coisas novas. Após essa etapa do treinamento, escrevo esses artigos para ajudar a organizar minhas habilidades e, ao mesmo tempo, ajudar outros desenvolvedores a expandir seus conhecimentos.

Se a tarefa falhar ao lançar uma exceção, a equipe queue:workinterromperá seu trabalho.

Para que um processo queue:workseja executado continuamente (consumindo suas filas), você deve usar um monitor de processo como o Supervisor para garantir que o comando queue:worknão pare de funcionar, mesmo que a tarefa gere uma exceção.O supervisor reinicia o comando após o travamento, iniciando novamente da próxima tarefa, deixando de lado a exceção lançada.

As tarefas serão executadas em segundo plano no servidor, não mais dependentes da solicitação HTTP. Isso introduz algumas mudanças que eu tive que considerar ao implementar o código da tarefa.

Aqui estão os mais importantes aos quais prestarei atenção:

Como posso descobrir que o código da tarefa não está funcionando?


Ao executar em segundo plano, você não pode perceber imediatamente que sua tarefa está causando erros.
Você não terá mais comentários imediatos, como ao fazer uma solicitação http do navegador. Se a tarefa falhar, ela será executada silenciosamente e ninguém notará.

Considere a possibilidade de integrar uma ferramenta de monitoramento em tempo real, como o Inspector, para apresentar todas as falhas.

Você não tem uma solicitação http


Não há mais solicitação HTTP. Seu código será executado a partir do CLI.

Se você precisar de parâmetros de consulta para concluir suas tarefas, precisará passá-los ao construtor de tarefas para uso posterior em tempo de execução:

<?php

//   

class TagUserJob implements ShouldQueue
{
    public $data;
    
    public function __construct(array $data)
    {
        $this->data = $data;
    }
}

//       

$this->dispatch(new TagUserJob($request->all()));

Você não sabe quem fez login


Não há mais uma sessão . Da mesma forma, você não saberá a identidade do usuário que efetuou login, portanto, se precisar de informações sobre o usuário para concluir a tarefa, será necessário passar o objeto de usuário para o construtor de tarefas:

<?php

//   
class TagUserJob implements ShouldQueue
{
    public $user;
    
    public function __construct(User $user)
    {
        $this->user= $user;
    }
}

//       
$this->dispatch(new TagUserJob($request->user()));

Entenda como dimensionar


Infelizmente, em muitos casos, isso não é suficiente. Usar uma linha e um consumidor pode se tornar inútil em breve.

Filas são buffers FIFO ("primeiro a entrar, primeiro a sair"). Se você planejou muitas tarefas, talvez até de tipos diferentes, elas precisam esperar que outras pessoas concluam suas tarefas antes de concluí-las.

Há duas maneiras de escalar:

Vários consumidores por fila.Portanto



, cinco tarefas serão removidas da fila por vez, acelerando o processamento da fila.

Filas de finalidade especial

Você também pode criar filas específicas para cada tipo de tarefa a ser iniciada com um consumidor dedicado para cada fila.



Assim, cada fila será processada independentemente, sem aguardar a conclusão de outros tipos de tarefas.

Horizonte


O Laravel Horizon é um gerenciador de filas que fornece controle total sobre quantas filas você deseja configurar e a capacidade de organizar os consumidores, permitindo que os desenvolvedores combinem essas duas estratégias e implementem a que melhor se adapta às suas necessidades de escalabilidade.

O lançamento ocorre através do horizonte php artisan em vez da fila php artisan: work . Este comando varre seu arquivo de configuração horizon.phpe inicia uma série de trabalhadores da fila, dependendo da configuração:

<?php

'production' => [
    'supervisor-1' => [
        'connection' => "redis",
        'queue' => ['adveritisement', 'logs', 'phones'],
        'processes' => 9,
        'tries' => 3,
        'balance' => 'simple', //   simple, auto  null

    ]
]

No exemplo acima, o Horizon iniciará três filas com três processos atribuídos para processar cada fila. Conforme mencionado na documentação do Laravel, a abordagem orientada a código do Horizon permite que minha configuração permaneça em um sistema de controle de versão em que minha equipe possa colaborar. Esta é a solução perfeita usando uma ferramenta de IC.

Para descobrir o significado dos parâmetros de configuração em detalhes, leia este maravilhoso artigo .

Minha própria configuração



<?php

'production' => [
    'supervisor-1' => [
        'connection' => 'redis',
        'queue' => ['default', 'ingest', 'notifications'],
        'balance' => 'auto',
        'processes' => 15,
        'tries' => 3,
    ],
]

O Inspector usa principalmente três filas:

  • ingerir processos para analisar dados de aplicativos externos;
  • as notificações são usadas para agendar notificações imediatamente se um erro for detectado ao receber dados;
  • O padrão é usado para outras tarefas que eu não quero misturar com os processos de ingestão e notificação .

Com o uso do balance=autoHorizon, ele entende que o número máximo de processos ativados é 15, que serão distribuídos dinamicamente, dependendo do carregamento das filas.

Se as filas estiverem vazias, o Horizon suportará um processo ativo para cada fila, o que permitirá ao consumidor processar a fila imediatamente se uma tarefa estiver agendada.

Observações finais


A execução simultânea em segundo plano pode causar muitos outros erros imprevisíveis, como o MySQL "Tempo limite de bloqueio" e muitos outros problemas de design. Leia mais aqui .

Espero que este artigo tenha ajudado você a usar filas e tarefas com mais confiança. Se você quiser saber mais sobre nós, visite nosso site em www.inspector.dev.

Anteriormente publicado aqui www.inspector.dev/what-worked-for-me-using-laravel-queues-from-the-basics-to -horizonte



Entre no curso e obtenha um desconto.



All Articles