Arquitetura

Como um Serviços perde mensagens no RabbitMQ – Parte 1

Nesse posts vamos abordar alguns conceitos básicos dessa incrível ferramenta de mensageria que é utilizada por diversos times da HypeFlame, o RabbitMQ. Como toda ferramenta, a utilização consciente é fundamental, portanto, vamos destacar alguns cenários de atenção em que é possível perder mensagens com ela, especialmente do ponto de vista da implementação de consumidores.

Se você ainda não conhece a ferramenta, ou gostaria de revisitar alguns conceitos, não se preocupe, o post inicia com um overview geral da ferramenta. No entanto, fique à vontade para pular as seções iniciais e ir direto ao ponto se você já é expert no assunto.

Ao final, apresentaremos alguns exemplos práticos que você mesmo pode reproduzir baixando o código fonte na sua máquina.

Conceitos básicos

Connection

Todos protocolos suportados pelo RabbitMQ são baseados em TCP/IP com conexões duráveis (long-lived connections), isso quer dizer que a conexão não é aberta e fechada a cada mensagem enviada ou recebida. A conexão permanece ativa até seu encerramento explícito pela aplicação cliente.

Cada conexão de cliente com o broker utiliza uma única conexão TCP na porta destinada ao protocolo específico que estiver sendo utilizado.

Após conectar-se ao broker, os clientes podem publicar e consumir mensagens, definir a topologia (queues, exchanges, bindings) e utilizar as operações suportadas por cada protocolo.

Como a conexão permanece ativa, as mensagens são entregues de forma push-based, ou seja, não é necessário realizar polling, elas são enviadas de forma reativa.

Channel

Os channels podem ser vistos como conexões lógicas leves que são úteis quando uma mesma aplicação deseja ter múltiplos canais isolados de comunicação com o broker. Além disso, eles permitem o compartilhamento de uma mesma conexão TCP/IP com múltiplas conexões lógicas ao broker, evitando o uso excessivo de recursos de hardware e simplificando questões como liberação de portas em firewall.

Todos os channels de uma conexão são excluídos quando sua conexão é encerrada, e se baseiam em um número identificador compartilhado entre o cliente e o broker, o channel number.

Com o uso de channels é possível isolar tanto o tratamento de erros quanto as métricas de utilização do Message Broker.

Queue

Uma queue, ou fila, é uma estrutura sequencial de dados, que permite realizar as ações de publicação (enqueue) e consumo (dequeue) de mensagens entre produtores e consumidores. Funciona como um grande buffer, limitado à quantidade de memória e disco disponíveis.

As filas podem ser classificadas quanto à sua persistência:

  • Duráveis: persistem mesmo após o reinício do broker
  • Transientes: são apagadas uma vez que o broker é reiniciado

Filas também podem ser criadas como “Exclusivas”, o que significa que poderão ser utilizadas por apenas uma conexão, e são excluídas quando essa conexão é encerrada.

Resumidamente: uma fila durável existe até que seja explicitamente excluída, uma fila transiente existe até que o broker seja reiniciado e uma fila exclusiva existe até que sua conexão esteja ativa.

Message

São as mensagens enviadas ao broker, que podem ser classificadas quanto ao tipo de entrega (Delivery Mode):

  • Persistentes: são escritas no disco assim que elas chegam à fila, porém, são mantidas em memória também na medida do possível.
  • Não Persistentes: são escritas no disco somente quando o espaço em memória estiver sob pressão (memory pressure)

É possível configurar as operações de liberação de memória de uma fila com o parâmetro:  lazy_queue_explicit_gc_run_operation_threshold

(quanto mais alto, maior performance, porém, maior o uso de memória)

Exchange

Os exchanges podem ser entendidos como o mecanismo de roteamento de mensagens provido pelo RabbitMQ. Com eles, podemos implementar diversos Enterprise Integration Patterns, direcionando mensagens para uma fila específica, ou multiplexar uma mesma mensagem para várias filas distintas. Por padrão, toda mensagem é enviada para um exchange, mesmo que queiramos postar em uma única fila. Nesse caso, o nome da fila é utilizado como routing key do exchange.

Fonte da imagem:

https://www.rabbitmq.com/tutorials/amqp-concepts.html

Exchange – Enterprise Integration Patterns

Abaixo estão ilustrados alguns Enterprise Integration Patterns que podem ser implementados por filas e exchanges no RabbitMQ:

  • Message Router: funciona como um filtro que roteia de um canal para outro baseado em alguma condição – por ex.: uma routing key.
  • Publish Subscribe: roteia de um canal para N outros baseado em um canal/tópico de interesse
  • Content Based Router: roteia de um canal para outro baseado no conteúdo da mensagem

Fonte das imagens:

https://www.enterpriseintegrationpatterns.com/patterns/messaging/

https://camel.apache.org/components/latest/eips/message-router.html

https://camel.apache.org/components/latest/eips/publish-subscribe-channel.html

https://camel.apache.org/components/latest/eips/content-based-router-eip.html

Tipos de Exchanges

Os tipos de algoritmos de roteamento usados dependem do tipo de Exchange e das regras configuradas, também chamadas de bindings.

Tipo de ExchangeDescrição
DefaultPor padrão, toda fila é vinculada a esse exchange, onde a Routing Key utilizada é o próprio nome da fila. Interessante para a troca de mensagens simples entre produtor e consumidor.
DirectEntrega mensagens para uma fila baseado em uma routing key. Interessante para implementar o padrão Message Router.
FanoutEntrega as mensagens para todas as filas que estiverem vinculadas a ele, ignorando as routing keys. Interessante para implementar comunicação anycast.
TopicEntrega as mensagens para uma ou mais filas baseando-se na routing key e no padrão que foi utilizado para vincular a fila ao exchange. Interessante para o padrão publish/subscribe.
HeadersDesenhado para rotear mensagens baseado em múltiplos atributos que são melhores expressos como cabeçalhos de mensagens do que uma única routing key. Interessante para implementar o padrão Content Based Router.

Protocolos

Há diversos protocolos suportados e que podem ser habilitados como plugins. O protocolo mais comumente utilizado é o AMQP versões 0-9-*.

AMQP 0-9-1 + extensões – é a versão mais recente do protocolo “core” suportado pelo broker, porém, possui diversas extensões como, por exemplo:

  • Controle de TTL a nível de fila ou de mensagem
  • Dead letter queue – que permite rotear uma mensagem para outros tópicos caso elas sejam expiradas ou rejeitadas
  • Limite de tamanho de filas

AMQP 1.0 – versão difere bastante do AMQP 0-9-1, possui diversas limitações, e menos clients disponíveis. No entanto, é mais simples de adicionar suporte a ela em diferentes brokers.

  • O plugin não suporta, por exemplo, entrega do tipo Exacly Once

STOMP –  Simple Text Oriented Messaging Protocol é um protocolo simples baseado em texto, pode ser usado, inclusive, direto via telnet.

MQTT – protocolo binário leve, destinado à comunicação pub/sub.

  • Se baseia em algoritmo de consenso, ficando disponível somente se a maioria dos nós do cluster estiver online.
  • Suporta armazenamento em memória ou disco
  • Suporta utilização de um adaptador para WebSockets

Cenários de possível perda de mensagens

AutoAck

Existem bibliotecas clientes escritas nas mais diferentes linguagens para o RabbitMQ, e é possível configurá-las para enviarem um sinal de Acknowledgement automaticamente, logo após realizar a leitura da mensagem.

Esse mecanismo traz certa comodidade de implementação quando não precisamos nos preocupar se o processamento da mensagem foi finalizado. O desenho abaixo ilustra um cenário de sucesso com essa configuração.

No desenho acima, a seguinte sequência de eventos acontece:

  1. Produtor envia M1 para o broker
  2. Consumidor recebe M1 e responde com o AutoAck
  3. Consumidor processa com sucesso M1
  4. Broker exclui a mensagem (concorrente com 3)

 

No caso acima, não houve perda de mensagem, pois o processamento ocorreu com sucesso.

O ponto de atenção é que, consumidores podem falhar logo após enviar o ACK para o broker, o que pode ser indesejado em alguns casos.

O desenho abaixo ilustra um cenário de perda de mensagem utilizando AutoAck.

No desenho acima, a seguinte sequência de eventos acontece:

  1. Produtor envia M1 para o broker
  2. Consumidor recebe M1 e responde com o AutoAck
  3. Consumidor FALHA e NÃO PROCESSA M1
  4. Broker exclui a mensagem (concorrente com 3)

Como o consumidor confirmou o recebimento da mensagem, o broker não se preocupará em entregá-la novamente.

Nesse caso, a mensagem foi perdida 😕

Uma forma alternativa e mais resiliente de implementar é realizando o envio manual do sinal de Acknowledgement somente após o processamento com SUCESSO.

Dessa forma, se ocorrer algum erro no processamento, o broker tentará entregar a mensagem novamente, até que algum cliente a processe com êxito.

No desenho acima, a seguinte sequência de eventos acontece:

  1. Produtor envia M1 para o broker, que é destinada ao consumidor
  2. Consumidor recebe M1, FALHA e NÃO PROCESSA M1
  3. Broker reenvia M1 para o consumidor
  4. Consumidor recebe M1 pela segunda vez e processa a mensagem
  5. Consumidor envia ACK de M1 para o broker
  6. Broker exclui a mensagem (somente após o passo 4)

Repare que, mesmo com a indisponibillidade momentânea do consumidor, a aplicação tolerou a falha, evitando que a mensagem fosse perdida.

Atenção: Se o processamento do consumidor não for idempotente, é possível gerar inconsistências, por exemplo, um pagamento realizado em duplicidade.

TTL – Time-to-live

Dependendo do protocolo utilizado, existe suporte para a definição de um tempo máximo de vida da mensagem na fila (TTL).

Isso significa que, se a mensagem permanecer no broker por um determinado limite de tempo, ela será automaticamente descartada.

Para evitar que a mensagem seja perdida, é possível configurar uma DLQ (Dead Letter Queue), assim, as mensagens que não forem lidas a tempo pelos consumidores serão direcionadas para essa outra fila, que pode ter configurações mais duráveis.

Vale lembrar que, mesmo a DLQ pode ter seus próprios limites de retenção, e os recursos do broker são limitados.  Se sua aplicação não pode estar exposta à perda de mensagens, talvez você precise torná-la momentaneamente indisponível até que os consumidores voltem a ter capacidade para processar as mensagens e liberar recursos do broker.

No desenho abaixo, temos um cenário hipotético em que uma mensagem foi perdida pelo broker devido ao TTL mal dimensionado, associado à ausência de uma DLQ.

  1. Produtor envia M1
  2. Produtor envia M2
  3. Produtor envia M3
  4. Produtor envia M4
  5. M1 atinge seu TTL e é excluída da fila
  6. Consumidor inicia a leitura de mensagens
  7. Consumidor lê M2, pois M1 já foi excluída da fila

Note que, esse cenário independe da estratégia de AutoAck escolhida, pois o consumidor não teve a oportunidade de receber a mensagem nem mesmo uma única vez.O desenho abaixo ilustra uma alternativa mais resiliente utilizando uma Dead Letter Queue.

  1. Produtor envia M1
  2. Produtor envia M2
  3. Produtor envia M3
  4. Produtor envia M4
  5. M1 atinge seu TTL
  6. M1 é movida para a DLQ
  7. Consumidor inicia a leitura de mensagens
  8. Consumidor lê M2, pois M1 já foi excluída da fila

Obs.: M1 pode ser reprocessada futuramente e, inclusive, ser reenviada para a fila original.A configuração da DLQ é realizada adicionando uma policy à fila, abaixo temos um exemplo de código Java possível:

Mensagens não persistentes

Alguns cenários de perda de mensagens ocorrem devido a alguma indisponibilidade do ponto de vista do servidor.

Quando há indisponibilidade de um ou mais nós do cluster de RabbitMQ, mensagens que estejam APENAS na memória desses nós podem ser perdidas.

O desenho abaixo ilustra um cenário de perda de mensagens após o reinício do broker, considerando que não estamos utilizando nenhum mecanismo de High Availability, como as Mirrored Queues ou as Quorum Queues.

  1. Produtor envia M1
  2. Produtor envia M2
  3. Produtor envia M3
  4. Produtor envia M4
  5. Broker é reiniciado antes de as mensagens serem enviadas ao consumidor, com isso, as mensagens armazenadas na fila são PERDIDAS

Parte 2

Esta imagem possuí um atributo alt vazio; O nome do arquivo é arthur-2.png
%d blogueiros gostam disto: