RabbitMQ/AMQP-Fila de Melhores Práticas/Design de Tópicos em uma Arquitetura de MicroServiço




esb spring-amqp (2)

Antes de responder a "uma troca, ou muitas?" questão. Na verdade, quero fazer outra pergunta: precisamos mesmo de uma troca personalizada para esse caso?

Diferentes tipos de eventos de objetos são tão naturais para combinar diferentes tipos de mensagens a serem publicadas, mas às vezes isso não é realmente necessário. E se nós abstrairmos todos os 3 tipos de eventos como um evento de “gravação”, cujos subtipos são “criados”, “atualizados” e “excluídos”?

| object | event   | sub-type |
|-----------------------------|
| user   | write   | created  |
| user   | write   | updated  |
| user   | write   | deleted  |

Solução 1

A solução mais simples para suportar isso é que podemos projetar apenas uma fila “user.write” e publicar todas as mensagens de evento de gravação do usuário nessa fila diretamente por meio da troca padrão global. Ao publicar em uma fila diretamente, a maior limitação é presumir que apenas um aplicativo assina esse tipo de mensagem. Várias instâncias de um aplicativo inscrito nessa fila também estão bem.

| queue      | app  |
|-------------------|
| user.write | app1 |

Solução 2

A solução mais simples não funcionaria quando houvesse um segundo aplicativo (com lógica de processamento diferente) para assinar qualquer mensagem publicada na fila. Quando há vários aplicativos inscritos, pelo menos precisamos de uma troca de tipo de "fanout" com ligações para várias filas. Assim, essas mensagens são publicadas no excahnge e a troca duplica as mensagens para cada uma das filas. Cada fila representa o trabalho de processamento de cada aplicativo diferente.

| queue           | subscriber  |
|-------------------------------|
| user.write.app1 | app1        |
| user.write.app2 | app2        |

| exchange   | type   | binding_queue   |
|---------------------------------------|
| user.write | fanout | user.write.app1 |
| user.write | fanout | user.write.app2 |

Esta segunda solução funciona bem se cada assinante se preocupa e quer lidar com todos os subtipos de eventos “user.write” ou pelo menos para expor todos esses eventos de subtipo para cada assinante não é um problema. Por exemplo, se o aplicativo do assinante for simplesmente manter o log de transação; ou, embora o assinante manipule apenas o user.created, não há problema em informar quando o user.updated ou o user.deleted acontece. Ele se torna menos elegante quando alguns assinantes são externos à sua organização e você só deseja notificá-los sobre alguns eventos de subtipos específicos. Por exemplo, se app2 só quer manipular user.created e não deve ter o conhecimento de user.updated ou user.deleted de todo.

Solução 3

Para resolver o problema acima, temos que extrair o conceito “user.created” de “user.write”. O tipo de troca “tópico” poderia ajudar. Ao publicar as mensagens, vamos usar user.created / user.updated / user.deleted como chaves de roteamento, para que possamos definir a chave de ligação da fila “user.write.app1” como “user. *” E a chave de ligação de A fila “user.created.app2” é “user.created”.

| queue             | subscriber  |
|---------------------------------|
| user.write.app1   | app1        |
| user.created.app2 | app2        |

| exchange   | type  | binding_queue     | binding_key  |
|-------------------------------------------------------|
| user.write | topic | user.write.app1   | user.*       |
| user.write | topic | user.created.app2 | user.created |

Solução 4

O tipo de troca de "tópico" é mais flexível no caso de haver mais subtipos de evento. Mas se você souber claramente o número exato de eventos, também poderá usar o tipo de troca "direta" para obter melhor desempenho.

| queue             | subscriber  |
|---------------------------------|
| user.write.app1   | app1        |
| user.created.app2 | app2        |

| exchange   | type   | binding_queue    | binding_key   |
|--------------------------------------------------------|
| user.write | direct | user.write.app1   | user.created |
| user.write | direct | user.write.app1   | user.updated |
| user.write | direct | user.write.app1   | user.deleted |
| user.write | direct | user.created.app2 | user.created |

Volte para a questão “uma troca, ou muitas?”. Até agora, todas as soluções usam apenas uma troca. Funciona bem, nada de errado. Então, quando poderemos precisar de várias trocas? Há uma leve queda de desempenho se uma troca de "tópico" tiver muitas ligações. Se a diferença de desempenho de muitas ligações na “troca de tópico” realmente se tornar um problema, é claro que você poderia usar mais trocas “diretas” para reduzir o número de ligações de troca de “tópico” para um melhor desempenho. Mas, aqui, quero me concentrar mais nas limitações de função das soluções de “uma troca”.

Solução 5

Um caso que podemos considerar, na verdade, múltiplas trocas é para diferentes grupos ou dimensões de eventos. Por exemplo, além dos eventos criados, atualizados e excluídos, memtioned acima, se tivermos outro grupo de eventos: login e logout - um grupo de eventos que descreve “comportamentos do usuário” em vez de “gravação de dados”. Por exemplo, um grupo diferente de eventos pode precisar de estratégias de roteamento completamente diferentes e de convenções de nomenclatura de chaves e filas de roteamento, portanto, é necessário que haja uma troca separada entre usuário e comportamento.

| queue              | subscriber  |
|----------------------------------|
| user.write.app1    | app1        |
| user.created.app2  | app2        |
| user.behavior.app3 | app3        |

| exchange      | type  | binding_queue      | binding_key     |
|--------------------------------------------------------------|
| user.write    | topic | user.write.app1    | user.*          |
| user.write    | topic | user.created.app2  | user.created    |
| user.behavior | topic | user.behavior.app3 | user.*          |

Outras soluções

Existem outros casos em que podemos precisar de várias trocas para um tipo de objeto. Por exemplo, se você deseja definir permissões diferentes em trocas (por exemplo, apenas eventos selecionados de um tipo de objeto podem ser publicados em uma troca de aplicativos externos, enquanto a outra troca aceita os eventos de aplicativos internos). Para outra instância, se você quiser usar trocas diferentes com um número de versão para suportar diferentes versões de estratégias de roteamento do mesmo grupo de eventos. Para outra outra instância, você pode querer definir algumas “trocas internas” para ligações de troca para troca, que poderiam gerenciar as regras de roteamento de uma maneira em camadas.

Em resumo, ainda assim, “a solução final depende das necessidades do seu sistema”, mas com todos os exemplos de solução acima, e com as considerações básicas, espero que pelo menos consiga pensar na direção certa.

Eu também criei uma postagem no blog , reunindo esse histórico de problemas, as soluções e outras considerações relacionadas.

Estamos pensando em introduzir uma abordagem baseada no AMQP para nossa infra-estrutura de microsserviço (coreografia). Temos vários serviços, digamos, serviço ao cliente, serviço ao usuário, artigo-serviço, etc. Estamos planejando introduzir o RabbitMQ como nosso sistema central de mensagens.

Estou procurando as melhores práticas para o design do sistema em relação a tópicos / filas etc. Uma opção seria criar uma fila de mensagens para cada evento único que pode ocorrer em nosso sistema, por exemplo:

user-service.user.deleted
user-service.user.updated
user-service.user.created
...

Eu acho que não é a abordagem correta para criar centenas de filas de mensagens, não é?

Eu gostaria de usar o Spring e essas anotações agradáveis, por exemplo:

  @RabbitListener(queues="user-service.user.deleted")
  public void handleEvent(UserDeletedEvent event){...

Não é melhor ter apenas algo como "notificações de serviço de usuário" como uma fila e enviar todas as notificações para essa fila? Eu ainda gostaria de registrar ouvintes apenas para um subconjunto de todos os eventos, então como resolver isso?

Minha segunda pergunta: Se eu quiser ouvir em uma fila que não foi criada antes, receberei uma exceção no RabbitMQ. Eu sei que posso "declarar" uma fila com o AmqpAdmin, mas devo fazer isso para cada fila de minhas centenas em cada microsserviço, como sempre pode acontecer que a fila não tenha sido criada até agora?


Eu geralmente acho que é melhor ter trocas agrupadas por tipo de objeto / combinações de tipo de troca.

Em seu exemplo de eventos do usuário, você pode fazer várias coisas diferentes, dependendo do que seu sistema precisa.

Em um cenário, pode fazer sentido ter uma troca por evento como você listou. você poderia criar as seguintes trocas

| exchange     | type   |
|-----------------------|
| user.deleted | fanout |
| user.created | fanout |
| user.updated | fanout |

isso se encaixaria no padrão " pub/sub " dos eventos de transmissão para qualquer ouvinte, sem se preocupar com o que está escutando.

com essa configuração, qualquer fila que você ligar a qualquer uma dessas trocas receberá todas as mensagens publicadas na troca. isso é ótimo para pub / sub e alguns outros cenários, mas pode não ser o que você quer o tempo todo, pois você não poderá filtrar mensagens para consumidores específicos sem criar uma nova troca, fila e ligação.

em outro cenário, você pode descobrir que há muitas trocas sendo criadas porque há muitos eventos. você também pode querer combinar a troca de eventos do usuário e comandos do usuário. isso poderia ser feito com uma troca direta ou por tópicos:

| exchange     | type   |
|-----------------------|
| user         | topic  |

Com uma configuração como essa, você pode usar chaves de roteamento para publicar mensagens específicas em filas específicas. Por exemplo, você pode publicar user.event.created como uma chave de roteamento e rotear com uma fila específica para um consumidor específico.

| exchange     | type   | routing key        | queue              |
|-----------------------------------------------------------------|
| user         | topic  | user.event.created | user-created-queue |
| user         | topic  | user.event.updated | user-updated-queue |
| user         | topic  | user.event.deleted | user-deleted-queue |
| user         | topic  | user.cmd.create    | user-create-queue  |

Com esse cenário, você acaba com uma única troca e as chaves de roteamento são usadas para distribuir a mensagem para a fila apropriada. observe que eu também incluí uma chave de roteamento de "comando de criação" e fila aqui. Isso ilustra como você pode combinar padrões.

Eu ainda gostaria de registrar ouvintes apenas para um subconjunto de todos os eventos, então como resolver isso?

Usando uma troca de fanout, você cria filas e ligações para os eventos específicos que deseja ouvir. cada consumidor criaria sua própria fila e ligação.

Usando uma troca de tópicos, você pode configurar chaves de roteamento para enviar mensagens específicas para a fila desejada, incluindo todos os eventos com uma ligação como user.events.# .

Se você precisar de mensagens específicas para ir a consumidores específicos, faça isso por meio do roteamento e das ligações .

em última análise, não há resposta certa ou errada para qual tipo de troca e configuração usar sem conhecer as especificidades das necessidades de cada sistema. você poderia usar qualquer tipo de troca para qualquer propósito. Há trocas com cada um, e é por isso que cada aplicativo precisará ser examinado de perto para entender qual deles está correto.

quanto a declarar suas filas. cada consumidor de mensagem deve declarar as filas e ligações necessárias antes de tentar anexá-las. isso pode ser feito quando a instância do aplicativo é iniciada ou você pode esperar até que a fila seja necessária. Novamente, isso depende do que seu aplicativo precisa.

Eu sei que a resposta que estou fornecendo é bastante vaga e cheia de opções, ao invés de respostas reais. não há respostas sólidas específicas, no entanto. é tudo lógica fuzzy, cenários específicos e olhando para as necessidades do sistema.

FWIW, eu escrevi um pequeno eBook que cobre esses tópicos a partir de uma perspectiva única de contar histórias. Ele aborda muitas das perguntas que você tem, embora às vezes indiretamente.