Neste exemplo criaremos uma fila de trabalho responsável por distribuir tarefas demoradas entre seus consumidores. Um consumidor atarefado só irá receber outra mensagem após informar que está livre.
Caso você esteja começando agora com RabbitMQ, sugiro que inicie por este artigo: Conceitos iniciais sobre RabbitMQ utilizando C#/.NET
Despacho Round-Robin
Ao utilizar as filas de tarefas (Task Queues) pode-se facilmente balancear as cargas de trabalho. Se houver muito acúmulo de trabalho, podemos adicionar mais consumidores/trabalhadores e assim melhorar a escalabilidade e garantir a disponibilidade, especialmente em sistemas distribuídos e em cenários onde o processamento de tarefas pode ser demorado.
Por padrão o RabbitMQ enviará em sequência as mensagens para cada consumidor. Em média cada consumidor receberá a mesma quantidade de mensagens. Esta forma de distribuição de mensagens é denominada Round-Robin.
Problemática
Porém existe uma problemática em cima desta forma de distribuição de mensagens. Existem cenários em que um consumidor inicia uma tarefa longa e morre com ela, deixando a tarefa parcialmente concluída. Neste cenário, após enviar a mensagem para um consumidor, o RabbitMQ a marca imediatamente para exclusão, fazendo com que a mensagem seja perdida caso o consumidor/trabalhador seja encerrado.
Confirmação de mensagem
Para que uma mensagem não seja perdida, o RabbitMQ possui um reconhecimento/confirmação de mensagens. Caso um consumidor morra sem enviar uma confirmação, o RabbitMQ irá entender que a mensagem não foi totalmente processada e a colocará novamente na fila. Se houver outro consumidor online, ele enviará rapidamente ao outro consumidor. Dessa forma, nenhuma mensagem é perdida.
Para confirmar manualmente as mensagens, desativaremos o parâmetro de reconhecimento automático AutoAck
.
Colocaremos o AutoAck
como falso no BasicConsume
e adicionaremos uma chamada para o BasicAck
.
CONSUMIDOR:
... channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false); }; channel.BasicConsume(queue: "payments-queue", autoAck: false, consumer: consumer);
Com isso podemos garantir que mesmo encerrando um nó de trabalho enquanto ele processava uma mensagem, nada será perdido.
Durabilidade da mensagem
Mesmo garantindo que a mensagem não seja perdida caso o consumidor morra, se o servidor do RabbitMQ morrer as tarefas ainda serão perdidas.
Para que o RabbitMQ não esqueça as filas e mensagens caso seja parado, devemos marcar as filas e mensagens como duráveis.
Precisamos ter certeza que a fila sobreviverá à reinicialização do nó do RabbitMQ. Para isso a declaramos como durável (durable: true).
CONSUMIDOR E PRODUTOR:
... channel.QueueDeclare(queue: "payments-queue", durable: true, exclusive: false, autoDelete: false, arguments: null);
Esta alteração deverá ser realizada tanto no consumidor (consumer) quanto no produtor (producer).
Agora, precisamos marcar nossas mensagens como persistentes. No produtor, após o GetBytes iremos definir o IBasicProperties.Persistent
como true
.
PRODUTOR:
... var body = Encoding.UTF8.GetBytes(message); var properties = channel.CreateBasicProperties(); properties.Persistent = true;
Marcar as mensagens como persistentes irá informar ao RabbitMQ para gravar as mensagens no disco.
Despacho justo
Em uma situação onde temos dois consumidores, quando todas as mensagens pares são pesadas e as mensagens ímpares são leves, consequentemente um deles estará constantemente ocupado e o outro estará mais livre.
Normalmente o RabbitMQ não saberá sobre essa situação e continuará enviando as mensagens de forma uniforme. Ele não analisa o número de mensagens não confirmadas de um consumidor, ele apenas despacha a mensagem n para o conumidor n.
Para corrigir este problema, devemos utilizar do método BasicQos
(Qos = Quality of Service) com a configuração prefetchCount = 1
. Isso diz ao RabbitMQ para não fornecer mais de uma mensagem para um consumidor sem que antes este informe que já processou e confirmou a anterior. Ao invés disso ele irá despachar a mensagem para o próximo consumidor que esteja disponível.
Iremos adicionar o BasicQos
após o QueueDeclare
no código do consumidor.
CONSUMIDOR:
... channel.QueueDeclare(queue: "payments-queue", durable: true, exclusive: false, autoDelete: false, arguments: null); channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);
Na prática:
- SEM AS CONFIGURAÇÕES
Na imagem seguinte temos os consoles de um produtor e de dois consumidores, sendo que o código não está com nenhuma das configurações abordadas neste artigo.
Na imagem, podemos ver que o produtor envia sequencialmente as mensagens para os consumidores, que as recebem de forma uniforme, sendo que o consumidor 1 está recebendo as mensagens ímpares e o consumidor 2 as mensagens pares.
Neste cenário o consumidor 1 está com bastante disponibilidade e o consumidor 2 está bastante atarefado, logo vemos que o consumidor 2 está ainda na mensagem 36 e processando a 38, com dezenas de mensagens na sua fila aguardando para serem processadas. Enquanto que o consumidor 1 está equivalente com o produtor, sempre processando a última mensagem enviada.
- COM AS CONFIGURAÇÕES
A próxima imagem tem o mesmo consoles do produtor e dos consumidores, porém agora com todas as configurações abordadas neste artigo. Sendo elas o AutoAck
, durable: true
e BasicQos
.
Neste cenário o consumidor 2 não está mais consumindo somente as mensagens pares, assim como o consumidor 1 não está mais consumindo somente as ímpares.
Os dois estão informando ao RabbitMQ quando estão disponíveis para consumir a próxima mensagem. E assim o RabbitMQ encaminha de forma equilibrada as mensagens para cada consumidor.
Vemos que o consumidor 2 continua atarefado, porém agora ele só recebe uma nova mensagem após processar a anterior.
Ferramentas utilizadas
- Pacote Nuget: RabbitMQ.Client
- Linguagem: C# .NET 6.0
Código no GitHub
Assim finalizamos nossos entendimentos acerca das
queues
e como podemos configurá-las de forma a manter a disponibilidade e persistência de nossas mensagens, de forma a não sobrecarregar nossos consumidores. O código final está disponível no GitHub e seu link está disponível acima.
Referências
RabbitMQ - Work Queues using the .NET Client
RabbitMQ - Consumer Prefetch
Cursos recomendados
Alura - Microsserviços na prática: mensageria com RabbitMQ
Top comments (0)