Por: @felipefc, @mariliars
Publicado em: 2018-07-27

Arquitetura das filas de retry

retry-queues

Uma das formas de comunicação entre os nossos serviços é através de um servidor de mensageria: o RabbitMQ. A troca de mensagens é implementada como uma lógica de publishers e subscribers. Por exemplo, um cliente é apagado e o serviço que gerencia os clientes publica uma mensagem "clients.deleted". Os recursos que estão ligados aos clientes estarão “ouvindo” especificamente esse tópico e executarão uma ação como consequência. Por exemplo, apagar todos os certificados SSL vinculados ao cliente recém apagado.

O problema

A arquitetura era básica, mensagens eram publicadas por um serviço, recebidas e processadas por outro. Porém, durante a implementação do processamento de logs do VPNFlex um problema foi identificado: o que acontece quando houver um erro durante o recebimento ou processamento da mensagem? A resposta era simples: a mensagem seria perdida. No entanto, naquela ocasião as mensagens de logs não poderiam ser simplesmente descartadas, visto que a consistência dos relatórios ficaria prejudicada.

A primeira solução pensada foi utilizar um parâmetro próprio da biblioteca AMQP utilizada que reenfileira as mensagens até receber um ack (certificando o recebimento da mensagem). No entanto, com este atributo as mensagens eram reenviadas ilimitadamente até serem processadas. O impacto disso era o enchimento da memória e, em casos extremos, a parada do serviço RabbitMQ. Era preciso pensar em uma solução mais elaborada, onde as mensagens pudessem aguardar o reprocessamento por mais tempo e com limite de tentativas.

Solução

A solução implementada foi montar uma arquitetura personalizada, utilizando dos recursos disponibilizados pelo RabbitMQ: exchanges e filas; e alguns parâmetros como: TTL (tempo de vida da mensagem) e Dead Letter Exchange.

TTL: Time-To-Live

Em nosso cenário este parâmetro foi aplicado sobre as filas de espera. O TTL define qual o intervalo de tempo que uma mensagem aguarda até ser consumida e, se não for, é encaminhada para uma outra Exchange.

DLX: Dead Letter Exchange

No RabbitMQ, quando uma mensagem é rejeitada ou ocorre um evento de erro, ela pode ser simplesmente descartada. No entanto, ao declarar uma Dead Letter Exchange, a mensagem é republicada nessa Exchange e poderá seguir um processamento alternativo ao invés de ser ignorada.

Retomando a construção da nossa arquitetura de filas de Retry, tínhamos como objetivo que as mensagens não processadas deveriam tomar a seguinte sequência de passos:

  1. A mensagem é publicada pelo SERVICE_1
  2. A mensagem é capturada pelo SERVICE_2
  3. Durante o processamento da mensagem ocorre uma Exception. Dessa forma, o tratamento da mensagem não é completado.
  4. Nesse momento, o atributo retry_count (contido no cabeçalho da mensagem) é incrementado. A partir do valor que ele possui é determinado para qual fila de Retry a mensagem será repassada. Como neste exemplo é a primeira tentativa, a mensagem ficará em espera por 1 minuto.
  5. Passado 1 minuto a mensagem volta para a fila de consumo do SERVICE_2 até ser reprocessada.
  6. Novamente uma Exception durante o processamento. E agora?
  7. A mensagem é novamente repassada para uma fila de espera, porém agora aguardará por 15 minutos antes de realizar uma nova tentativa.

A ideia principal gira em torno de várias tentativas de processamento, com intervalos de tempo prédefinidos. A quantidade de tentativas e os tempos são configuráveis em código.

Por exemplo:

RETRY_QUEUES = [
  "10s", # 10 segundos
  "30s", # 30 segundos
  "5m", # 5 minutos
]

A partir dos intervalos definidos acima e construída a aplicação, a arquitetura de filas e exchanges assumiria esta forma:

retry-queues

Basicamente, após expiradas as três tentativas e, mesmo assim a mensagem não for corretamente processada, ela enfim será descartada. Ao determinar longos intervalos de espera pelo reprocessamento, é aberta a possibilidade de correção dos bugs causadores nesse meio tempo, para que na próxima tentativa a mensagem possa ser processada.

Concluindo, é importante ressaltar que esta arquitetura é montada para todos os serviços. Sendo assim, cada serviço dos produtos iTFLEX possui:

  • Uma Dead Letter ExchangeDLX_<SERVICE_NAME>
  • Uma Exchange para encaminhamento da tentativa seguinte  RETRY_<SERVICE_NAME>
  • Três (dependendo da configuração) filas de RetryRETRY.1m-<SERVICE_NAME>``RETRY.15m-<SERVICE_NAME>``RETRY.1h-<SERVICE_NAME>