Dans la plupart des sites web d’aujourd’hui, les traitements sont faits de manière séquentielle. On effectue une tâche à la fois de manière chaînée et tout le code est organisé de manière monolithique et sans découpage métier.
On a ensuite commencé à découper les architectures en micros-services afin de séparer le code de la base de données en éléments plus simples, afin de faciliter la maintenabilité du code. Cependant, plus nous faisons évoluer nos applications et plus les dépendances entre celles-ci deviennent fortes, augmentant la complexité de code et la maintenabilité de notre système. Alors, comment pouvons-nous pallier ce problème ?
L’implémentation d’architecture orientée événements est une des solutions possibles et je vais essayer de vous la présenter au travers de ce retour d’expérience !
En prenant l’exemple d’un site d’e-commerce, nous pouvons imaginer plusieurs services comme suit :
L’utilisateur effectue une nouvelle commande ;
L’API commande crée une nouvelle commande ;
Il demande à l’API de paiement d’initialiser un nouveau paiement qui est renvoyé à l’utilisateur pour que celui-ci puisse l’effectuer ;
L’utilisateur envoie ses informations bancaires qui sont vérifiées par l’API de paiement ;
Une fois que le paiement a été validé par l’API éponyme, celle-ci va demander séquentiellement aux services de gestion de stocks, de livraison et d’emailing d’effectuer individuellement un traitement.
Dans une architecture REST classique, l’API chargée d’envoyer les emails de confirmation va devoir attendre, tout comme l’utilisateur. Si jamais nous souhaitons faire évoluer notre site d’e-commerce en y ajoutant de nouveaux services qui interviendront dans le processus d’achat, nous allons potentiellement devoir ajouter des appels dans notre API commande, qui n’auront pas non plus besoin d’attendre que les autres tâches des services soient résolues pour avancer.
Pour pallier ce problème d’attente, nous allons implémenter une architecture orientée événements en utilisant notamment RabbitMQ, un broker (ou courtier dans la langue de Molière). Il va permettre à nos différentes API de communiquer entre elles. Voyons ensemble son principe de base :
RabbitMQ est un outil de messaging implémentant AMQP.Il nous permet de développer une communication asynchrone entre les différents éléments de notre système. Et rien de tel qu’un schéma pour expliquer son fonctionnement, celui-ci nous vient d’Erlang Solutions :
Le but de l’asynchronisme est de séparer toutes les dépendances métiers. Chaque service va alors effectuer son traitement puis envoyer un message (souvent en json) sur un broker. Il sera ensuite converti selon les besoins des systèmes en écoute (en DTO pour des APIs Spring Boot, par exemple). Il existe quatre méthodes d’échange de messages :
Direct Exchange : Utilisée lorsque l’on veut envoyer des trames sur des queues dont la clé de routage correspond exactement à celle du service producteur.
Topic Exchange : Utilisée pour faire correspondre des queues à des regex de clés de routage. Par exemple, si une trame est envoyée sur un échange de type Topic “colis.achat.valide”, on peut très bien avoir un service en écoute sur une clé “*.achat.valide” et un autre service en écoute sur “colis.achat.*”. Ainsi, les deux services vont recevoir le message envoyé.
Fanout Exchange : Permet d’envoyer le message reçu par le producteur à toutes les queues liées à cette méthode en ignorant la clé de routage, si il y en a une.
Header Exchange : Permet de faire des routages beaucoup plus complexes en se basant sur les headers des messages, qui sont similaires aux métadonnées lors d’un appel REST.
Les différents systèmes vont s’abonner à des queues afin de pouvoir traiter les différents sujets qui les concernent. Lors de la séparation métier, il faut faire relativement attention à bien définir les différents besoins de données en entrées. Reprenons l’exemple de notre site e-commerce. Voici à quoi il va ressembler après avoir fait le passage en architecture orientée événements :
Le fonctionnement reste le même jusqu’à la validation de paiement puisque ce sont les séquences d’après qui nous intéressent. L’API de paiement va envoyer un message (que l’on peut appeler événement) à RabbitMQ. Cet événement sera en même temps consommé par l’API gérant les stocks et par l’API s’occupant de tout ce qui est lié à la livraison puisqu’elles n’ont pas de dépendance entre elles.
L’API livraison envoie alors un événement quand son traitement est terminé (que la livraison est préparée) et l’API d’emailing consomme ce message afin d’envoyer un mail au client. Il reçoit son numéro de suivi et peut commencer à suivre son colis.
Si jamais nous avons besoin d’ajouter de nouvelles APIs (pour gérer des promotions ou des jeux concours, par exemple) nous n’aurons pas besoin de toucher à l’existant. Les nouvelles APIs vont consommer les messages RabbitMQ et s’exécuter d'elles-mêmes.
Vous l’avez compris, au lieu qu’une API communique potentiellement avec une myriade d’autres en leur disant à tour de rôle “Hé, j'ai fini mon traitement, fais ceci !” elle s’exprimera plutôt en disant “J’ai fini ce que j’avais à faire, voici des informations qui pourrait intéresser plusieurs d’entre vous !”.
L’asynchronisme n’est cependant pas une méthode magique qui résout tous nos problèmes (un peu comme la méthode agile). On préférera certainement rester sur des appels REST pour vérifier les tokens de connexions de nos utilisateurs, par exemple. Comme toujours il faut cerner et étudier les différents besoins que nous avons afin de faire les choix d’architecture les plus pertinents pour notre SI !