Les architectures event-driven offrent souvent des solutions élégantes pour fournir du code maintenable, gérer des tâches asynchrones et construire des applications fiables.
En tant que développeurs chez Theodo, nous aidons chaque jour nos clients à créer des produits de qualité. Au cours de mes missions, j'ai été amené plusieurs fois à implémenter et faire évoluer des architectures event-driven : cet article se base sur ces expériences pour présenter comment vous pouvez en tirer profit dans vos choix d’architecture.
En complément, si vous cherchez plus d’informations pratiques sur comment implémenter votre propre architecture event-driven, je vous conseille d'aller voir le site de RabbitMQ, qui contient d’excellents tutoriels.
Qu'est-ce qu'une architecture event-driven ?
Bien que très simple, cette question est assez délicate. Je recommande un très bon article à ce sujet, de Martin Fowler (en anglais) : What do you mean by event-driven? (si vous préférez les vidéos, Martin Fowler présente également le contenu de l'article dans cette conférence).
Quelques points clés à retenir de cet article :
- "Event-driven" est une expression assez mal définie, qui regroupe des design patterns très différents.
- Il existe au moins quatre design patterns intéressants qui impliquent l’utilisation d’événements : Event Notification, Event-Carried State Transfer, Event Sourcing et Command-Query Responsibility Segregation (CQRS)
Chacun de ces patterns est intéressant et mérite d’y consacrer un peu de temps ! Dans la suite de l'article, nous allons nous concentrer plus en détail le pattern Event Notification et montrer comment l’implémenter concrètement.
Un exemple pour mieux comprendre
Nous allons illustrer le pattern Event Notification à l'aide d'un exemple. Imaginons que je viens d'acheter un nouvel article sur un site d’e-commerce, et que je demande à sauvegarder les informations de ma carte de crédit pour un achat ultérieur.
Lorsque je clique sur le bouton "Acheter", une demande de paiement est soumise, puis acceptée (si tout se passe bien). Le succès d'un paiement est un changement significatif dans l'état de l'application : en d'autres termes, un événement, selon la définition de Wikipédia.
Suite à cet événement, le site Web doit déclencher plusieurs actions, par exemple :
- Créer un nouvel ordre d'expédition pour le vendeur
- Sauvegarder ma carte de crédit pour une utilisation ultérieure
- M’envoyer un e-mail de confirmation de commande
- Charger une page de confirmation sur le site web
Chacune de ces actions est indépendante des autres, et doit être réalisée par différentes parties du code : disons différents microservices (on abrègera “microservice” en MS dans la suite de l’article).
La question que l’on va se poser est la suivante : comment notifier à chaque MS dans la partie droite du schéma qu'un événement "payment_succeeded" s'est produit dans le MS Payment ?
Nous allons présenter deux architectures différentes pour réaliser cette tâche (attention spoiler : l'une d'elles est ce que nous appelons le pattern Event Notification - arriverez-vous à deviner laquelle ?)
Notification d'événement : orchestration vs chorégraphie
Dans l’excellent livre Building Microservices, Sam Newman illustre très bien la différence entre ces deux types d’architecture, à travers une comparaison entre orchestration et chorégraphie :
"Avec l'orchestration, nous comptons sur un cerveau central pour guider et diriger le processus, à l’image d’un chef d'orchestre.
Avec la chorégraphie, nous informons chaque partie du système de son travail, et nous les laissons ensuite gérer les détails, comme des danseurs qui trouvent tous leur chemin et réagissent les uns aux autres dans un ballet.”
L'orchestration en action
Dans notre exemple, le cerveau central serait la partie du code qui détecte l'événement initial, c'est-à-dire le MS Payment. Il donnerait alors la consigne, de manière hiérarchique, aux autres MS pour qu'ils effectuent les actions requises, via des requêtes sur des endpoints spécialement prévus à cet effet. Par conséquent, ce type d'architecture est appelé architecture request-driven.
1 : L'application front envoie une demande au MS Payment, l'informant que j'ai cliqué sur le bouton "Acheter" (avec la case "Enregistrer ma carte de crédit" également cochée) ;
2 : Le MS Payment traite la demande et accepte le paiement ;
3, 4, 5 : le MS Payment envoie successivement des demandes au MS Order, MS Messenger et MS Customer pour leur passer la consigne d'effectuer les actions requises (respectivement créer une nouvelle commande, envoyer un email de confirmation de commande, et sauvegarder ma carte de crédit) ;
6 : Le MS Payment peut enfin envoyer une réponse à la requête de l'application Front, pour charger la page de confirmation.
La chorégraphie en action
Il existe une alternative plus découplée pour transmettre les informations du MS Payment aux autres MS : la chorégraphie.
La réussite d’un paiement est du domaine de responsabilité du MS Payment. Mais est-il de sa responsabilité de connaître la liste de toutes les tâches qui doivent être déclenchées suite à la réussite du paiement ? Pas vraiment... Il serait plutôt du domaine de responsabilité du MS Order de savoir quoi faire de son côté lorsqu'un paiement est accepté. Même chose pour les MS Messenger et Client. C’est ici qu’intervient... le pattern Event Notification, alias chorégraphie ! 🎉
Suivant ce pattern, le MS Payment notifiera simplement qu'un événement "payment_succeeded" s'est produit, en utilisant ce qu’on appelle un Messaging System. Le Messaging System transmettra ensuite ce message à tous les MS intéressés par cette information. Chaque MS consommera finalement le message et déclenchera les actions correspondantes.
L’intérêt de ce pattern, c'est que le MS Payment n'a pas besoin de savoir quelles actions seront déclenchées dans d'autres parties du code après la notification de l'événement : cela garantit un niveau élevé de découplage entre les MS.
1 : L'application Front envoie une demande au MS Payment, l'informant que j'ai cliqué sur le bouton "Acheter" (avec une case à cocher "Enregistrer ma carte de crédit" également activée) ;
2 : Le MS Payment traite la demande et accepte le paiement ;
3 : Le MS Payment publie un message "payment_succeeded" dans un messaging system ;
4 : Les MS Order, Messenger et Client sont avertis de manière asynchrone que l'événement s'est produit en recevant le message (= la "notification d'événement"). Parallèlement, le MS Payment peut envoyer directement la réponse à l'application frontale, sans attendre que les trois MS exécutent leurs tâches.
Au coeur du messaging system : les principaux concepts de RabbitMQ
L'élément central du pattern Event Notification est le messaging system.
Plusieurs solutions existent pour traiter les messages : nous ne répondrons pas à la question "Comment choisir votre messaging system" dans cet article, mais si vous voulez quelques éléments sur le sujet, les solutions les plus utilisées à ce jour sont : RabbitMQ, Kafka ou Amazon SQS. Vous pourrez également trouver ici l'histoire d'une équipe de développement qui explique comment ils ont fait leur choix. Nous allons donner dans la suite un aperçu du fonctionnement de RabbitMQ.
RabbitMQ suit un protocole appelé AMQP (Advanced Message Queueing Protocol), qui définit un standard de communication par messages. Les principaux concepts sont les suivants :
- Dans les messaging systems, l'information est transportée par les messages, qui contiennent des attributs (comme les en-têtes dans une requête) et un payload (le contenu du message).
- Les messaging systems reçoivent les messages de publishers (applications qui produisent le message) et les acheminent aux consumers (applications qui traitent le message).
- Les messages sont publiés à une entité appelée exchange : c'est l’équivalent d’une boîte aux lettres, où le publisher dépose son message.
- Les exchanges distribuent ensuite les messages à des queues, (dans RabbitMQ, en suivant un ordre FIFO - First In First Out - mais ce n’est pas le cas pour tous les messaging systems).
- Consommation : les messages qui sont stockés dans les queues sont alors soit livrés en continu aux consumers qui s'y sont abonnés, soit récupérés dans les queues par les consommateurs sur demande.
- Routing : les règles de livraison des messages aux bonnes queues sont définies par des bindings (liens entre les exchanges et les queues) et des routing keys (un attribut de message spécifique utilisé pour le routage).
Pour une présentation plus détaillée du fonctionnement interne de RabbitMQ et d'autres messaging systems conformes à l'AMQP, vous pouvez aller jeter un coup d'oeil à cet excellent article !
Implémenter le pattern Event Notification avec RabbitMQ
Maintenant que nous connaissons mieux les concepts de base de RabbitMQ, nous allons voir comment nous les avons implémentés dans notre exemple précédent.
Configuration RabbitMQ
La figure suivante décrit comment configurer RabbitMQ.
- Tout d'abord, nous avons créé un exchange appelé "ms.payment". Créer un exchange dédié par micro-service n’est pas obligatoire. Cependant, c'est une organisation simple et maintenable que nous conseillons.
- A droite (partie verte de la figure), nous avons défini une queue par tâche qui doit être exécutée lorsque les événements sont consommés. Nous avons également préfixé chaque nom de queue par le nom du MS qui consomme le message. Encore une fois, ce n'est pas obligatoire, mais c'est une bonne solution.
- Enfin, nous avons déclaré les bindings entre l'exchange "ms.payment" et les trois queues, de sorte que lorsqu'un message avec la routing key "payment_succeeded" est publié sur l'exchange, une copie est routé vers chacune des trois queues
Publication et consommation des messages
Enfin, voici le tableau complet, de la publication à la consommation des messages.
1 : Le MS Payment publie un nouveau message avec la routing key "payment_succeded" sur l'exchange "ms.payment";
2 : Grâce aux bindings que nous avons déclarés entre l'exchange et les queues, 3 copies du message sont créées et distribuées aux queues ;
3 : Les consommateurs qui ont souscrit aux queues reçoivent leur copie du message et déclenchent l'exécution de leur tâche.
Assez simple finalement, non ?
Conclusion : avantages et limites de l’Event Notification
En conclusion, dans quels cas le pattern Event Notification est-il intéressant ? Présentons quelques avantages et inconvénients pour vous aider à répondre à cette question :
Avantages
- Découplage élevé : l'utilisation des messages permet un niveau élevé de découplage entre le publisher et les consumers, puisque le publisher n'a pas besoin de savoir quels services consommeront ses messages, ni quelles actions ils déclencheront.
- Traitement asynchrone des tâches : dans notre exemple, les actions qui suivent le succès du paiement sont indépendantes (du point de vue de la logique métier ; par exemple, nous ne voulons pas vérifier si l'e-mail de confirmation a été envoyé avant de sauvegarder la carte de crédit). La notification d'événement est un moyen d'exécuter toutes ces actions en parallèle, de manière asynchrone.
- Fiabilité améliorée : si, pour une raison quelconque, un consommateur est temporairement hors service, la queue sert de buffer temporaire. Elle stocke les messages jusqu'à ce que le consommateur soit de nouveau opérationnel et recommence à les traiter.
- Mise en place de queues de retry : nous n’avons pas beaucoup abordé le sujet dans cet article, mais une caractéristique intéressante des messaging systems est la mise en place de queues de retry. Ces queues permettent de republier automatiquement un message dans une queue donnée si le traitement a échoué, après un certain temps. Par exemple, si un consommateur doit obtenir des informations d'une API peu fiable, cette fonctionnalité permet de réessayer plusieurs fois au cas où l'API ne répondrait pas.
Inconvénients
- Découplage élevé : oui, le découplage peut aussi être une limite ! Parfois, la logique métier veut simplement que les choses soient couplées : avez-vous remarqué dans notre exemple d'article que l'application Front communique avec le MS Payment par une requête, et non par un message ? C'est parce que l'application Front doit savoir si le paiement a réussi ou non avant de charger la page suivante... Donc l’Event Notification est utile seulement dans les cas où le publisher ne se soucie pas de la réponse du ou des consumers !
- Coût de mise en place : la mise en place de RabbitMQ représente un peu de travail additionnel d'implémentation. Cependant, le processus est bien documenté et si votre besoin s’y prête (événements génériques qui déclenchent plusieurs actions différentes dans des parties distinctes du code), la mise en place d'un messaging system vaut certainement le coup.
Vous avez besoin d’un coup de main pour implémenter une architecture event-driven sur le back-end de votre projet ? N'hésitez pas à contacter l'un de nos experts back-end Python !