DEV Community

Cover image for Trabalhando com Queues (Filas) no RabbitMQ utilizando C#/.NET
Larissa Tavares
Larissa Tavares

Posted on

Trabalhando com Queues (Filas) no RabbitMQ utilizando C#/.NET

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.

Imagem 1

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.

Imagem 2

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

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)