La construction d’une API peut impliquer de devoir mettre à disposition des opérations synchrones, c’est-à-dire que l’appelant attend activement une réponse pour pouvoir continuer ses traitements.
Certes, les opérations asynchrones (de type fire-and-forget, ou s’appuyant sur un mécanisme de callback) sont souvent présentées comme la panacée. Mais mettons cela de côté et concentrons-nous sur le cas des opérations synchrones en partant du principe que le besoin de telles opérations est justifié – et il peut parfaitement l’être
En temps que spécialiste de l’intégration, on prône un peu partout le découplage comme bonne pratique fondamentale, sous 3 aspects :
Ce qui doit théoriquement nous permettre d’introduire un maximum de flexibilité et d’adaptabilité dans la couche d’intégration et ainsi de réaliser ce petit exercice de passe-passe connu sous le nom de « faire entrer un rond dans un carré ». Ce sont ces principes de découplage que l’on retrouve dans le pattern de Messaging.
Oui mais voilà :
De prime abord, ces « contraintes » semblent incompatibles avec les exigences d’une API devant présenter des temps de réponses acceptables. C’est d’ailleurs dans ce genre de scenario que l’on peut être tenté d’être « pragmatique », ou de se dire que « le mieux est l’ennemi du bien », etc. Réflexions qui, bien que nécessaires dans une démarche de doute scientifique, sont trop souvent annonciatrices de l’introduction volontaire de dette technique (lire d’ailleurs à ce sujet l’excellent article de Mark Heath : Top 7 Reasons For Introducing Technical Debt) pour des raisons qui relèvent parfois du simple a priori.
Mais le mieux est-il réellement l’ennemi du bien? La bonne pratique de découplage dont je parlais plus haut est-elle inapplicable/incompatible dans le cas des APIs synchrones? Ou formulé autrement : est-on obligé de choisir entre synchrone et asynchrone?
Pour se fixer les idées, prenons un cas concret : imaginons une API de gestion des clients exposant une opération de création d’un client. Cette opération doit:
Dans l’écosystème Azure, les scenarii de messaging peuvent être adressés par plusieurs composants, dont les principaux sont :
Chacun a ses spécificités et ses cas d’usage de prédilection. La documentation de Microsoft sur le sujet est assez claire, aussi je vous conseille d’y jeter un œil si ce n’est pas déjà fait.
Si l’on reprend les deux responsabilités de l’API décrites plus haut :
C’est donc Service Bus que je choisis ici comme colonne vertébrale de mon API. L’architecture technique est très simple :
Ci-dessous un schéma de principe :
Le fonctionnement est le suivant :
Ci-dessous un exemple de code :
Message requestMessage = new Message(System.Text.Encoding.UTF8.GetBytes(requestBody)); requestMessage.ReplyTo = responseEntity; requestMessage.ReplyToSessionId = requestID; requestMessage.SessionId = requestID; requestMessage.UserProperties.Add("EventType", "CreationRequested"); requestMessage.UserProperties.Add("CountryCode", "FRA");
IMessageSession session = await rcvClient.AcceptMessageSessionAsync(requestID); Message response = await session.ReceiveAsync(TimeSpan.FromSeconds(responseTimeout));
outputMessage.SessionId = inputMessage.ReplyToSessionId;
Et c’est tout! On peut bien évidemment enrichir le scenario en imaginant un chaînage des événements et des traitements, mais le principe fondamental reste celui décrit ci-dessus.
C’est bien joli sur le papier, mais quid des performances? Pour une API, le temps de réponse est l’un des nerfs de la guerre; la latence induite par le découplage au travers de Service Bus doit donc rester acceptable et maîtrisée : c’est-à-dire qu’elle peut varier, aussi bien dans le temps qu’en fonction de la charge.
J’ai effectué un certain nombre de tests de performance en me basant sur un simple namespace Service Bus Standard. Les besoins de mon API ne devant pas dépasser 50 requêtes/s, j’ai choisi de prendre quelques précautions : j’ai donc multiplié cette charge par 10 en testant cette mécanique sous 500 requêtes/s. Ci-dessous les résultats :
Les résultats montrent donc une forte cohérence des performances avec une distribution assez étroite, et une latence très bien contenue, n’excédant pas 200ms. Les tests que j’ai pu effectuer sur du Premium ne montrent pas de gain sur ces aspects, mais dans mon cas ces performances sont de toute manière tout à fait suffisantes.
ATTENTION : on pourrait se dire que Premium n’apportant pas vraiment d’avantage de performances, on peut se contenter d’un namespace Standard. Gardez en tête que le Standard n’est pas taillé pour la performance. Il a pour simple vocation de fournir des fonctionnalités Messaging as a Service. En effet il s’appuie sur des infrastructures partagées et le risque de phénomène de « voisin bruyant » est réel – même si Microsoft travaille actuellement à mettre en place des mécanismes de lissage des performances (qui passeront certainement par une forme de throttling). Autrement dit : aucune garantie que les performances décrites plus haut soient cohérentes dans la durée! Service Bus Premium, lui, est taillé pour la performance en s’appuyant notamment sur une infrastructure radicalement différente.
Mon propos n’est pas ici d’énoncer une règle absolue. Il convient à chacun de s’approprier ces éléments et d’étudier leur adéquation avec ses besoins. Néanmoins, je retire personnellement ces enseignements :
Sur ce, je vous souhaite un bon messaging! 🙂