Matheus Lopes Santos
Posted on September 8, 2023
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} ...
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,
],
],
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,
],
],
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';
}
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
);
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' => [],
],
],
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
- Toma conta da fila
- supervisor-low-priority
- Toma conta das filas
low
edefault
- Máximo de 3 processos
- Vai subir ou matar 1 processo (
maxShift
) a cada 3 segundos (coolDown
) - Timeout de 1 minuto
- Toma conta das filas
- 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
- Toma conta da fila
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 😗🧀
Posted on September 8, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.