Rabbit MQ — Extras

iundarigun
Dev Cave
Published in
6 min readMar 29, 2020

No começo do ano, escrevi dos posts sobre RabbitMQ. Podem ler eles aqui e aqui. Algumas coisas ficaram para atrás e quis trazer em forma de mini post dois extras (não relacionadas entre eles) que achei interessante compartilhar.

Delay Queue

Comentamos brevemente nos posts que as filas permitem configurações de timeout. Também a mensagem pode receber uma configuração especifica de expiração. Isso significa que se depois de um determinado tempo a mensagem não foi consumida, perde a utilidade e deve ser descartada. Um caso disso seria a distribuição de vouchers de desconto para um determinado evento. Se a mensagem não foi consumida até certo horário, não é mais interessante o processamento da mesma. Mas o que acontece com essa mensagem?

Quando o Rabbit descarta uma mensagem por timeout, seja ele configurado na mensagem ou na fila, vai tentar encaminhar para o exchange de Dead Letter, caso tiver essa configuração. Se pensamos um pouco além, podemos usar uma fila de wait com o timeout desejado, sem consumidores para ela, e atrelar os consumidores na fila que recebe as mensagens do exchange da Dead Letter.

Existe um plugin do Rabbit que faz exatamente isso de forma transparente, mas achei mais legal explicar a forma mais manual de fazer.

Criando o cenário

Vamos usar o docker para levantar o Rabbit, assim com vimos no primeiro post:

$ docker run -d --hostname localhost --name local-rabbit -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 -d rabbitmq:3-management

Vamos usar a interface web para criar as filas. Basta logar no http://localhost:15672/ com o usuário admin e senha admin.

Na aba Exchanges, criamos a DLQ-EXCHANGE:

Agora, na aba queues, criamos a DLQ-QUEUE. Essa será a fila consumida por nosso sistema:

Vamos fazer o bind entre o exchange e a fila. Entramos no detalhe da DLQ-EXCHANGE e configuramos da seguinte forma:

Agora já podemos criar nossa fila de delay, configurando um timeout de um minuto (60000 milissegundos):

Se a fila receber uma mensagem, após um minuto a mesma é encaminhada para a DLQ:

Um ponto de atenção é sobre performance. Na documentação do plugin explica que não é indicado para grandes quantidades de mensagens:

Current design of this plugin doesn’t really fit scenarios with a high number of delayed messages (e.g. 100s of thousands or millions).

O mesmo cuidado se aplica usando a abordagem apresentada. Mesmo deixando as duas filas como lazy, o que indica que as mensagens não vão ficar na memória RAM e sim em disco, na hora de enviar para a DLQ, caso precisar expirar muitas mensagens, o consumo de memória é altamente impactado. Desconheço se existe um fine tunning que melhore essa performance, mas no exemplo que fizemos, a expiração era de 6 milhões de mensagens em 15 minutos e a memória RAM do único nó que usamos passou de 450Mb a 14Gb, até derrubar o serviço.

Cluster

O segundo ponto que queria trazer é sobre cluster. Já parou para pensar como funciona um cluster de Rabbit? Tipo, normalmente usamos a través de uma lista de endereços numa property do Spring:

spring:
application:
name: rabbitmq-cluster
rabbitmq:
adresses: rabbit1:5672,rabbit2:5672,rabbit3:5672,rabbit4:5672
username: admin
password: admin

Mas que benefícios isso traz? Vamos conferir isso montando um cluster e analisando as caraterísticas do mesmo.

Montando um cluster na mão

Para montar na mão, vamos usar duas instâncias de docker. Inicialmente, vamos subir eles por separado e depois pedir para se juntar no cluster. A única coisa que precisamos para funcionar é que a cookie do Erlang seja a mesma nos dois servidores. Isso é para evitar que um servidor desconhecido se anexe ao cluster.

Subimos as duas instâncias primeiro e passamos uma network comum para eles se enxergarem:

$ docker run -d --hostname cluster-rabbit1 --name cluster-rabbit1 -p 15681:15672 -p 5681:5672 --network rabbit-network -e RABBITMQ_ERLANG_COOKIE=rabbit-docker-cookie rabbitmq:3-management
$ docker run -d --hostname cluster-rabbit2 --name cluster-rabbit2 -p 15682:15672 -p 5682:5672 --network rabbit-network -e RABBITMQ_ERLANG_COOKIE=rabbit-docker-cookie rabbitmq:3-management

Agora temos as duas instâncias rodando. Vamos conetar na segunda instância e executar os comandos necessários para se juntar ao cluster:

$ docker exec -it cluster-rabbit2 bash
# rabbitmqctl stop_app
# rabbitmqctl join_cluster rabbit@cluster-rabbit1
# rabbitmqctl start_app

Agora temos um cluster rodando! Se entrar na interface web no endereço http://localhost:15681 ou http://localhost:15682 teremos a seguinte informação na aba de Overview:

Agora vamos criar uma fila nova. Aparece um combo para dizer em que nó do cluster vamos criar ela:

Vamos criar uma fila em cada nó e colocar mensagens nelas:

Vamos derrubar o nó 2. Qual seria o comportamento esperado?

Temos a descrição da fila, mas não podemos acessar ao conteúdo. Então, montamos um cluster onde podemos distribuir as mensagens entre os nós, mas se um cair, o que tiver nele fica indisponível até o nó voltar.

Isso é um cenário ideal? Depende. O que você está trafegando? Qual é o motivo de montar o cluster? Talvez, começamos a casa pelo telhado. A necessidade de montar o cluster vem por dois motivos:

  • Resiliência
  • Escalabilidade

Se nossa intenção era a resiliência, fracassamos miseravelmente. Não conseguimos processar as mensagens na fila do segundo nó. Não conseguimos postar mensagens novas na fila. Mas se nossa intenção é só escalabilidade, conseguimos distribuir melhor nossa carga entre os nós.

Adicionando resiliência

Vamos configurar o cluster para oferecer resiliência. Para isso, vamos na aba de administração, submenu de policies. Criamos uma nova policy com ha-mode all:

Isso vai fazer que as filas e as mensagens sejam replicadas em todos os nós:

Se a fila já estiver criada, talvez precisa fazer o sync inicial na mão

Se derrubar o segundo nó, o primeiro vai assumir o lugar dele e não vamos perder dados:

Se parar para pensar, ganhamos resiliência mas perdemos escalabilidade, pois agora os dados são replicados em todos os nós. Podemos ter os dois? A resposta é sim. Mas para isso, precisamos mais nós.

Cluster com Docker Compose

Deixei na pasta cluster do meu repositório um docker-compose que cria 4 nós em cluster para não precisar fazer na mão:

Podemos iniciar nosso novo cluster executando a seguinte instrução desde a pasta onde tiver o arquivo:

$ docker-compose up -d

Acessamos ao cluster pela url http://localhost:15691:

Vamos criar uma nova policy:

Estamos indicando que queremos um ha (high availability) de 2 nós.

Agora se criamos uma fila em cada nó, veremos que só são replicadas em um dos outros nós e não em todos:

O +1 do lado do nome do nó indica a quantidade de réplicas.

Agora, se houver alguma fila muito crítica que precise estar nos 4 nós por garantia, podemos fazer isso criando uma policy especial usando o campo pattern:

Se criar uma fila com ha no nome, teremos réplica nos 4 nós:

Levando em conta que para ter resiliência e escalabilidade precisamos 4 nós, é claro que o custo também será elevado.

Conclusão

Bom, esses eram os dois extras que queria explicar. Se tiver alguma dúvida, crítica ou sugestão, não duvida em me procurar!

Referências

--

--

iundarigun
Dev Cave

Java and Kotlin software engineer at Clearpay