web-dev-qa-db-fra.com

RabbitMQ / AMQP - Conception de file d'attente / sujet de meilleures pratiques dans une architecture MicroService

Nous envisageons d'introduire une approche basée sur AMQP pour notre infrastructure de microservices (chorégraphie). Nous avons plusieurs services, disons le service client, le service utilisateur, le service d'articles, etc. Nous prévoyons d'introduire RabbitMQ comme notre système de messagerie central.

Je suis à la recherche de meilleures pratiques pour la conception du système en ce qui concerne les sujets/files d'attente, etc. Une option serait de créer une file d'attente de messages pour chaque événement pouvant survenir dans notre système, par exemple:

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

Je pense que ce n'est pas la bonne approche pour créer des centaines de files d'attente de messages, n'est-ce pas?

Je voudrais utiliser Spring et ces belles annotations, par exemple:

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

N'est-il pas préférable d'avoir simplement quelque chose comme "notifications de service utilisateur" comme n file d'attente puis envoyer toutes les notifications à cette file d'attente? Je voudrais quand même inscrire des auditeurs à un sous-ensemble de tous les événements, alors comment résoudre cela?

Ma deuxième question: si je veux écouter dans une file d'attente qui n'a pas été créée auparavant, j'obtiendrai une exception dans RabbitMQ. Je sais que je peux "déclarer" une file d'attente avec l'AmqpAdmin, mais dois-je le faire pour chaque file d'attente de mes centaines dans chaque microservice, car il peut toujours arriver que la file d'attente n'ait pas été créée jusqu'à présent?

52
Fritz

Je trouve généralement préférable de regrouper les échanges par type d'objet/combinaisons de type d'échange.

dans votre exemple d'événements utilisateur, vous pouvez effectuer différentes opérations en fonction des besoins de votre système.

dans un scénario, il pourrait être judicieux d'avoir un échange par événement comme vous l'avez indiqué. vous pouvez créer les échanges suivants

 | échange | tapez | 
 | -------------------- | user.deleted | fanout | 
 | user.created | fanout | 
 | user.updated | fanout | 

cela correspondrait au modèle "--- (pub/sub " de diffusion d'événements à tous les auditeurs, sans se soucier de ce qui est à l'écoute.

avec cette configuration, toute file d'attente que vous liez à l'un de ces échanges recevra tous les messages publiés sur l'échange. c'est idéal pour pub/sub et certains autres scénarios, mais ce n'est peut-être pas ce que vous voulez tout le temps car vous ne pourrez pas filtrer les messages pour des consommateurs spécifiques sans créer un nouvel échange, une file d'attente et une liaison.

dans un autre scénario, vous constaterez peut-être qu'il y a trop d'échanges en cours de création car il y a trop d'événements. vous pouvez également combiner l'échange des événements utilisateur et des commandes utilisateur. cela pourrait se faire avec un échange direct ou thématique:

 | échange | tapez | 
 | -------------------- | utilisateur | sujet | 

Avec une configuration comme celle-ci, vous pouvez utiliser des clés de routage pour publier des messages spécifiques dans des files d'attente spécifiques. Par exemple, vous pouvez publier user.event.created comme clé de routage et le faire router avec une file d'attente spécifique pour un consommateur spécifique.

 | échange | type | clé de routage | file d'attente | 
 | ------------------------------------------ ----------------------- | 
 | utilisateur | sujet | user.event.created | file d'attente créée par l'utilisateur | 
 | utilisateur | sujet | user.event.updated | file d'attente mise à jour par l'utilisateur | 
 | utilisateur | sujet | user.event.deleted | file d'attente supprimée par l'utilisateur | 
 | utilisateur | sujet | user.cmd.create | user-create-queue | 

Avec ce scénario, vous vous retrouvez avec un seul échange et des clés de routage sont utilisées pour distribuer le message à la file d'attente appropriée. notez que j'ai également inclus ici une clé de routage et une file d'attente de "création de commande". cela illustre comment vous pouvez combiner des modèles.

Je voudrais quand même inscrire des auditeurs à un sous-ensemble de tous les événements, alors comment résoudre cela?

en utilisant un échange de fanout, vous créeriez des files d'attente et des liaisons pour les événements spécifiques que vous souhaitez écouter. chaque consommateur créerait sa propre file d'attente et liaison.

en utilisant un échange de sujets, vous pouvez configurer des clés de routage pour envoyer des messages spécifiques à la file d'attente souhaitée, y compris tous les événements avec une liaison comme user.events.#.

si vous avez besoin de messages spécifiques pour aller à des consommateurs spécifiques, --- (vous le faites via le routage et les liaisons .

finalement, il n'y a pas de bonne ou de mauvaise réponse pour quel type d'échange et quelle configuration utiliser sans connaître les spécificités des besoins de chaque système. vous pouvez utiliser n'importe quel type d'échange à peu près n'importe quel but. il y a des compromis avec chacun, et c'est pourquoi chaque demande devra être examinée de près pour comprendre laquelle est correcte.

quant à déclarer vos files d'attente. chaque consommateur de message doit déclarer les files d'attente et les liaisons dont il a besoin avant d'essayer de s'y attacher. cela peut être fait au démarrage de l'instance d'application, ou vous pouvez attendre que la file d'attente soit nécessaire. encore une fois, cela dépend des besoins de votre application.

je sais que la réponse que je donne est plutôt vague et pleine d'options, plutôt que de vraies réponses. il n'y a cependant pas de réponses solides spécifiques. c'est toute la logique floue, des scénarios spécifiques et en regardant les besoins du système.

FWIW, j'ai écrit n petit livre électronique qui couvre ces sujets dans une perspective plutôt unique de raconter des histoires. il répond à bon nombre des questions que vous vous posez, bien que parfois indirectement.

36
Derick Bailey

Les conseils de Derick sont bons, sauf pour la façon dont il nomme ses files d'attente. Les files d'attente ne doivent pas simplement imiter le nom de la clé de routage. Les clés de routage sont des éléments du message, et les files d'attente ne devraient pas s'en soucier. C'est à cela que servent les fixations.

Les noms de file d'attente doivent être nommés d'après ce que fera le consommateur attaché à la file d'attente. Quelle est l'intention de l'opération de cette file d'attente. Supposons que vous souhaitiez envoyer un e-mail à l'utilisateur lors de la création de son compte (lorsqu'un message avec la clé de routage user.event.created est envoyé à l'aide de la réponse de Derick ci-dessus). Vous devez créer un nom de file d'attente sendNewUserEmail (ou quelque chose dans ce sens, dans un style que vous jugez approprié). Cela signifie qu'il est facile de vérifier et de savoir exactement ce que fait cette file d'attente.

Pourquoi est-ce important? Eh bien, vous avez maintenant une autre clé de routage, user.cmd.create. Supposons que cet événement soit envoyé lorsqu'un autre utilisateur crée un compte pour quelqu'un d'autre (par exemple, les membres d'une équipe). Vous souhaitez également envoyer un e-mail à cet utilisateur, vous créez donc la liaison pour envoyer ces messages à la file d'attente sendNewUserEmail.

Si la file d'attente a été nommée d'après la liaison, cela peut provoquer une confusion, en particulier si les clés de routage changent. Gardez les noms de file d'attente découplés et auto-descriptifs.

26
Jason Lotito

Avant de répondre à "un ou plusieurs échanges?" question. En fait, je veux poser une autre question: avons-nous vraiment besoin d'un échange personnalisé pour ce cas?

Différents types d'événements d'objet sont si natifs pour correspondre à différents types de messages à publier, mais ce n'est pas vraiment nécessaire parfois. Que se passe-t-il si nous résumons les 3 types d'événements comme un événement "d'écriture", dont les sous-types sont "créés", "mis à jour" et "supprimés"?

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

Solution 1

La solution la plus simple à prendre en charge est que nous ne pouvions concevoir qu'une file d'attente "user.write" et publier tous les messages d'événement d'écriture d'utilisateur dans cette file d'attente via l'échange par défaut global. Lors de la publication directe dans une file d'attente, la plus grande limitation est qu'elle suppose qu'une seule application s'abonne à ce type de messages. Plusieurs instances d'une application s'abonnant à cette file d'attente conviennent également.

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

Solution 2

La solution la plus simple ne pourrait pas fonctionner lorsqu'une deuxième application (ayant une logique de traitement différente) souhaite s'abonner à tous les messages publiés dans la file d'attente. Lorsqu'il y a plusieurs applications abonnées, nous avons au moins besoin d'un échange de type "fanout" avec des liaisons vers plusieurs files d'attente. Pour que les messages soient publiés sur l'excahnge et l'échange duplique les messages dans chacune des files d'attente. Chaque file d'attente représente le travail de traitement de chaque application différente.

| 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 |

Cette deuxième solution fonctionne bien si chaque abonné se soucie de lui et souhaite gérer tous les sous-types d'événements "user.write" ou au moins pour exposer tous ces événements de sous-type à chaque abonné n'est pas un problème. Par exemple, si l'application d'abonné sert simplement à conserver le journal des transactions; ou bien que l'abonné ne gère que user.created, il est correct de lui faire savoir quand user.updated ou user.deleted se produit. Il devient moins élégant lorsque certains abonnés proviennent de l'extérieur de votre organisation et que vous souhaitez uniquement les informer de certains événements de sous-type spécifiques. Par exemple, si app2 ne veut gérer que user.created et ne doit pas du tout avoir la connaissance de user.updated ou user.deleted.

Solution 3

Pour résoudre le problème ci-dessus, nous devons extraire le concept "user.created" de "user.write". Le type d'échange "thématique" pourrait aider. Lors de la publication des messages, utilisons user.created/user.updated/user.deleted comme clés de routage, afin de pouvoir définir la clé de liaison de "user.write.app1" comme "user. *" Et la clé de liaison de La file d'attente "user.created.app2" soit "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 |

Solution 4

Le type d'échange "sujet" est plus flexible au cas où il y aurait potentiellement plus de sous-types d'événements. Mais si vous connaissez clairement le nombre exact d'événements, vous pouvez également utiliser le type d'échange "direct" à la place pour de meilleures performances.

| 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 |

Revenez à la question "un échange ou plusieurs?". Jusqu'à présent, toutes les solutions n'utilisent qu'un seul échange. Fonctionne bien, rien de mal. Alors, quand pourrions-nous avoir besoin de plusieurs échanges? Il y a une légère baisse de performances si un échange de "sujet" a trop de liaisons. Si la différence de performances de trop de liaisons sur "échange de sujet" devient vraiment un problème, vous pouvez bien sûr utiliser des échanges plus "directs" pour réduire le nombre de liaisons d'échange de "sujet" pour de meilleures performances. Mais, ici, je veux me concentrer davantage sur les limitations fonctionnelles des solutions "à échange unique".

Solution 5

Un cas que nous pourrions considérer nativement comme des échanges multiples concerne différents groupes ou dimensions d'événements. Par exemple, outre les événements créés, mis à jour et supprimés mentionnés ci-dessus, si nous avons un autre groupe d'événements: connexion et déconnexion - un groupe d'événements décrivant les "comportements des utilisateurs" plutôt que "l'écriture des données". Coz différents groupes d'événements peuvent nécessiter des stratégies de routage et des conventions de dénomination des clés de routage et des files d'attente complètement différentes, c'est pour que natual ait un échange de comportement utilisateur séparé.

| 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.*          |

Autres solutions

Il existe d'autres cas où nous pourrions avoir besoin de plusieurs échanges pour un type d'objet. Par exemple, si vous souhaitez définir différentes autorisations sur les échanges (par exemple, seuls les événements sélectionnés d'un type d'objet peuvent être publiés sur un échange à partir d'applications externes, tandis que l'autre échange accepte tous les événements des applications internes). Pour une autre instance, si vous souhaitez utiliser différents échanges suffixés d'un numéro de version pour prendre en charge différentes versions des stratégies de routage du même groupe d'événements. Pour une autre autre instance, vous souhaiterez peut-être définir des "échanges internes" pour les liaisons échange-à-échange, qui pourraient gérer les règles de routage en couches.

En résumé, toujours, "la solution finale dépend des besoins de votre système", mais avec tous les exemples de solutions ci-dessus et avec les considérations de fond, j'espère que cela pourrait au moins faire réfléchir dans la bonne direction.

J'ai également créé n article de blog , rassemblant ce fond de problème, les solutions et autres considérations connexes.

15
Teddy Ma