Liberando os poderes do Laravel Horizon

devlopez

Matheus Lopes Santos

Posted on September 8, 2023

Liberando os poderes do Laravel Horizon

Particularmente acho que jobs em background são uma mão na roda. Que seja para importação de um grande arquivo csv, processamento de dados que chegaram de um webhook, dentre tantas outras coisas que podemos designar para serem processadas sem que ninguém esteja olhando.

No Laravel não é diferente. O framework nos provê uma poderosa feature de processamento de filas totalmente out-of-the-box. Vamos recapitular como podemos executar a nossa fila:

php artisan queue:work {connectionName} --queue={queueName} ...
Enter fullscreen mode Exit fullscreen mode

Acima, temos um exemplo bem básico de como iniciar a nossa fila. Existem várias outras opções e, para conhecer todas, sugiro dar uma conferida na documentação oficial do framework.

A vinda do Horizon

O horizon foi lançado em 2017 e é um package para execução e monitoramento da fila (Lembre-se, o que não é medido, não pode ser gerenciado). Ele veio não com a missão de substituir, mas a de complementar as queues do Laravel.

Com ele, temos uma execução bem mais simples de tudo o que eu preciso para rodar a minha fila, além de definir mecanismos para balancear a execução de nossa fila.

Um problema constante…

Um dos problemas que mais vi em aplicações em que dei manutenção foi a subutilização do horizon. Como assim Matheusão? Vejamos:

  • Projetos que utilizavam somente um worker e com todas as filas configuradas nesse worker
  • Gerenciamento de memória deficiente
  • Número de workers incompatível com a quantidade que jobs que o sistema processa

Quer dizer que eu não sei configurar a minha fila, Matheusão?

Não é isso. Cada projeto tem a sua particularidade, e todas as configurações que farei aqui vão depender do uso massivo ou não do sistema de filas.

Neste pequeno artigo, vou mostrar pra vocês como otimizar o seu horizon para que possa receber vários jobs e executar tudo de forma suave.

A configuração padrão

Por padrão, o horizon vem com um worker configurado apenas:

'defaults' => [
    'supervisor-1' => [
    'connection'          => 'redis',
        'queue'               => ['default'],
        'balance'             => 'auto',
        'autoScalingStrategy' => 'time',
        'maxProcesses'        => 3,
        'maxTime'             => 0,
        'maxJobs'             => 0,
        'memory'              => 128,
        'tries'               => 1,
        'timeout'             => 60,
        'nice'                => 0,
    ],
],
Enter fullscreen mode Exit fullscreen mode

Maravilha, mas o que essa configuração mostra pra gente?

  • Nosso worker trabalhará com a fila default
  • Nesse caso, o balanceamento não se aplica, pois só temos uma fila
  • Teremos, no máximo, 3 processos em execução
  • Os jobs serão executados somente uma vez, não sendo permitidas outras tentativas
  • Os jobs terão timeout de 60 segundos

Maravilha, amigos. Com esta configuração padrão, uma aplicação consegue rodar bastante coisa, mas, e se nossa demanda começar a crescer?

Adicionando filas ao worker

Vamos imaginar que eu tenho uma fila para para processar e-mails e outra para os demais jobs do sistema. Top, poderia fazer da seguinte forma:

'defaults' => [
    'supervisor-1' => [
    'connection'          => 'redis',
        'queue'               => ['emails', 'default'],
        'balance'             => 'auto',
        'autoScalingStrategy' => 'time',
        'maxProcesses'        => 3,
        'maxTime'             => 0,
        'maxJobs'             => 0,
        'memory'              => 128,
        'tries'               => 1,
        'timeout'             => 60,
        'nice'                => 0,
    ],
],
Enter fullscreen mode Exit fullscreen mode

Massa, mas, o que temos agora?

  • Duas filas sendo executadas pelo worker
  • máximo de 3 processos para as filas

Com esta configuração, quem estiver precisando mais, leva mais processos com ele. Imagine você que a fila de e-mails tem 40 jobs e a fila default, só 4. O horizon vai disponibilizar 2 processos para a fila emails e 1 processo para a fila default.

Configurando vários workers no horizon

Agora chegaremos ao ápice do nosso cenário. Vamos imaginar o seguinte:

  • Notificações (via slack por exemplo)
  • Envio de emails
  • Recebimento de dados de webhooks
  • Importação de dados via CSV

Cara, não tem coisa demais ai não?

Vamos com calma que o negócio vai dar bom.

Priorizando nossa fila

Pra começar os trabalhos, vamos categorizar as nossas filas

  • Alta prioridade
  • Baixa Prioridade
  • Default
  • Jobs que podem levar muito tempo para finalizarem

Maravilha, agora que já tenho as minhas categorias, vou fazer uso de um enum que guardará essas categorias pra mim:

<?php

declare(strict_types=1);

namespace App\Enums;

enum QueuePriority: string
{
    case Low         = 'low';
    case High        = 'high';
    case LongTimeout = 'long-timeout';
}
Enter fullscreen mode Exit fullscreen mode

Note que eu não adicionei a fila default nesse enum, pelo simples fato de que os jobs que não forem categorizados já vão direto pra ela. Mas se quiser colocar, fique à vontade 😃.

Dessa forma, podemos despachar os nossos jobs da seguinte forma:

InviteUser::dispatch($user)->onQueue(
    QueuePriority::High->value
);
Enter fullscreen mode Exit fullscreen mode

Elegante, não?

Tunando o nosso horizon

Agora, poderemos criar mais workers e deixar nossa fila ainda mais separada e organizada:

'defaults' => [
    'supervisor-high-priority' => [
        'connection'          => 'redis',
        'queue'               => [QueuePriority::High->value],
        'balance'             => 'auto',
        'minProcesses'        => 1,
        'maxProcesses'        => 6,
        'balanceMaxShift'     => 3,
        'balanceCooldown'     => 2,
        'autoScalingStrategy' => 'size',
        'maxTime'             => 0,
        'maxJobs'             => 0,
        'memory'              => 128,
        'tries'               => 1,
        'timeout'             => 60,
        'nice'                => 0,
    ],
    'supervisor-low-priority' => [
        'connection'          => 'redis',
        'queue'               => [QueuePriority::Low->value, 'default'],
        'balance'             => 'auto',
        'minProcesses'        => 1,
        'maxProcesses'        => 3,
        'balanceMaxShift'     => 1,
        'balanceCooldown'     => 3,
        'autoScalingStrategy' => 'size',
        'maxTime'             => 0,
        'maxJobs'             => 0,
        'memory'              => 128,
        'tries'               => 1,
        'timeout'             => 60,
        'nice'                => 0,
    ],
    'supervisor-long-timeout' => [
        'connection'          => 'redis',
        'queue'               => [QueuePriority::LongTimeout->value],
        'balance'             => 'auto',
        'minProcesses'        => 1,
        'maxProcesses'        => 3,
        'balanceMaxShift'     => 1,
        'balanceCooldown'     => 3,
        'autoScalingStrategy' => 'size',
        'maxTime'             => 0,
        'maxJobs'             => 0,
        'memory'              => 128,
        'tries'               => 1,
        'timeout'             => 600,
        'nice'                => 0,
    ],
],

'environments' => [
    'production' => [
        'supervisor-high-priority' => [],
        'supervisor-low-priority'  => [],
        'supervisor-long-timeout'  => [],
    ],

    'staging' => [
        'supervisor-high-priority' => [],
        'supervisor-low-priority'  => [],
        'supervisor-long-timeout'  => [],
    ],

    'local' => [
        'supervisor-high-priority' => [],
        'supervisor-low-priority'  => [],
        'supervisor-long-timeout'  => [],
    ],
],
Enter fullscreen mode Exit fullscreen mode

O que temos aqui, meu querido? Agora temos 3 workers para diferentes filas, com as seguintes configurações:

  • supervisor-high-priority
    • Toma conta da fila high
    • Máximo de 6 processos
    • Vai subir ou matar 3 processos (maxShift) a cada 2 segundos (coolDown)
    • Timeout de 1 minuto
  • supervisor-low-priority
    • Toma conta das filas low e default
    • Máximo de 3 processos
    • Vai subir ou matar 1 processo (maxShift) a cada 3 segundos (coolDown)
    • Timeout de 1 minuto
  • supervisor-long-timeout
    • Toma conta da fila long-timeout
    • Máximo de 3 processos
    • Vai subir ou matar 1 processo (maxShift) a cada 3 segundos (coolDown)
    • Timeout de 10 minutos

Uma configuração simples, mas que dá conta do recado 😃.

Pra finalizar…

Agora a nossa fila está configurada e pronta para metralhar uma boa quantidade de jobs. Mas lembre-se que, antes de aumentar os workers, certifique-se de que seu servidor tenha recursos disponíveis para seus novos workers.

Da forma que apresentei para vocês, a fila em si não trará gargalos para seu servidor; o que pode vir a trazer problemas, caso use o Redis por exemplo, é ver a sua aplicação cair por falta de memória, mas aí já é papo para outra conversa.

Sempre comece do pouco, aumente um processo aqui, outro ali, um timeout acolá. Jamais coloque tudo que você achar o máximo que o seu servidor consegue processar. Aos poucos e com prudência, você consegue achar o equilíbrio perfeito entre o que é processado em background e o restante da aplicação.

Um abraço e até a próxima 😗🧀

💖 💪 🙅 🚩
devlopez
Matheus Lopes Santos

Posted on September 8, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related