API Gateway (REST) ​​+ Microservices pilotés par les événements

16

J'ai un tas de microservices dont j'expose les fonctionnalités via une API REST selon le modèle API Gateway. Comme ces microservices sont des applications Spring Boot, j'utilise Spring AMQP pour réaliser une communication synchrone de style RPC entre ces microservices. Les choses se sont bien passées jusqu'à présent. Cependant, plus je lis sur les architectures de microservices événementiels et regarde des projets tels que Spring Cloud Stream, plus je suis convaincu que je fais peut-être les choses de la mauvaise façon avec le RPC, l'approche synchrone (en particulier parce que j'en aurai besoin pour évoluer) afin de répondre à des centaines ou des milliers de requêtes par seconde des applications clientes).

Je comprends le point derrière une architecture événementielle. Ce que je ne comprends pas vraiment, c'est comment utiliser un tel modèle en étant assis derrière un modèle (REST) ​​qui attend une réponse à chaque demande. Par exemple, si j'ai ma passerelle API en tant que microservice et un autre microservice qui stocke et gère les utilisateurs, comment pourrais-je modéliser une chose comme un GET /users/1d'une manière purement événementielle?

Tony E. Stark
la source

Réponses:

9

Répète après moi:

Les événements REST et asynchrones ne sont pas des alternatives. Ils sont complètement orthogonaux.

Vous pouvez avoir l'un, ou l'autre, ou les deux, ou aucun. Ce sont des outils entièrement différents pour des domaines de problèmes entièrement différents. En fait, la communication demande-réponse à usage général est absolument capable d'être asynchrone, déclenchée par les événements et tolérante aux pannes .


À titre d'exemple trivial, le protocole AMQP envoie des messages via une connexion TCP. Dans TCP, chaque paquet doit être reconnu par le récepteur . Si l'expéditeur d'un paquet ne reçoit pas un ACK pour ce paquet, il continue de renvoyer ce paquet jusqu'à ce qu'il soit ACK ou jusqu'à ce que la couche application "abandonne" et abandonne la connexion. Il s'agit clairement d'un modèle de demande-réponse non tolérant aux pannes car chaque "demande d'envoi de paquet" doit avoir une "réponse d'accusé de réception de paquet" qui l'accompagne, et la non-réponse entraîne l'échec de toute la connexion. Pourtant, AMQP, un protocole normalisé et largement adopté pour la messagerie asynchrone à tolérance de panne, est communiqué via TCP! Ce qui donne?

Le concept de base en jeu ici est que la messagerie évolutive à tolérance de pannes faiblement couplée est définie par les messages que vous envoyez , et non par la façon dont vous les envoyez . En d'autres termes, un couplage lâche est défini au niveau de la couche d'application .

Regardons deux parties communiquant soit directement avec HTTP RESTful, soit indirectement avec un courtier de messages AMQP. Supposons que la partie A souhaite télécharger une image JPEG vers la partie B qui accentuera, compressera ou améliorera l'image. La partie A n'a pas besoin de l'image traitée immédiatement, mais nécessite une référence à celle-ci pour une utilisation et une récupération futures. Voici une façon de procéder dans REST:

  • La partie A envoie un POSTmessage de demande HTTP à la partie B avecContent-Type: image/jpeg
  • La partie B traite l'image (pendant longtemps si elle est grande) pendant que la partie A attend, peut-être en faisant autre chose
  • La partie B envoie un 201 Createdmessage de réponse HTTP à la partie A avec un en- Content-Location: <url>tête qui renvoie à l'image traitée
  • La partie A considère que son travail est effectué car elle a désormais une référence à l'image traitée
  • Dans le futur, lorsque la partie A a besoin de l'image traitée, elle l'obtient en utilisant le lien de l'en- Content-Locationtête précédent

Le 201 Createdcode de réponse indique à un client que non seulement sa demande a réussi, il a également créé une nouvelle ressource. Dans une réponse 201, l'en- Content-Locationtête est un lien vers la ressource créée. Ceci est spécifié dans les sections 6.3.2 et 3.1.4.2 de la RFC 7231.

Maintenant, voyons comment cette interaction fonctionne sur un hypothétique protocole RPC au-dessus d'AMQP:

  • La partie A envoie à un courtier de messages AMQP (appelez-le Messenger) un message contenant l'image et des instructions pour la router vers la partie B pour traitement, puis répondez à la partie A avec une adresse quelconque pour l'image
  • La partie A attend, peut-être en faisant autre chose
  • Messenger envoie le message original de la partie A à la partie B
  • La partie B traite le message
  • La partie B envoie à Messenger un message contenant une adresse pour l'image traitée et des instructions pour acheminer ce message vers la partie A
  • Messenger envoie à la partie A le message de la partie B contenant l'adresse de l'image traitée
  • La partie A considère que son travail est effectué car elle a désormais une référence à l'image traitée
  • Dans le futur, lorsque la partie A a besoin de l'image, elle récupère l'image en utilisant l'adresse (éventuellement en envoyant des messages à une autre partie)

Voyez-vous le problème ici? Dans les deux cas, la partie A ne peut obtenir une adresse d'image qu'après que la partie B a traité l'image . Pourtant, la partie A n'a pas besoin de l'image tout de suite et, par tous les droits, ne s'en soucie pas si le traitement est encore terminé!

Nous pouvons résoudre ce problème assez facilement dans le cas AMQP en demandant à la partie B de dire à A que B a accepté l'image pour le traitement, en donnant à A une adresse indiquant où l'image se trouvera une fois le traitement terminé. La Partie B peut alors envoyer à A un message dans le futur indiquant que le traitement d'image est terminé. La messagerie AMQP à la rescousse!

Sauf devinez quoi: vous pouvez réaliser la même chose avec REST . Dans l'exemple AMQP, nous avons changé un message "voici l'image traitée" en un message "l'image est en cours de traitement, vous pouvez l'obtenir plus tard". Pour ce faire dans HTTP RESTful, nous utiliserons le 202 Acceptedcode et Content-Locationencore:

  • La partie A envoie un POSTmessage HTTP à la partie B avecContent-Type: image/jpeg
  • La partie B renvoie immédiatement une 202 Acceptedréponse qui contient une sorte de contenu "d'opération asynchrone" qui décrit si le traitement est terminé et où l'image sera disponible une fois le traitement terminé. Un en- Content-Location: <link>tête est également inclus qui, dans une 202 Acceptedréponse, est un lien vers la ressource représentée par le corps de la réponse. Dans ce cas, cela signifie que c'est un lien vers notre fonctionnement asynchrone!
  • La partie A considère que son travail est effectué car elle a désormais une référence à l'image traitée
  • Dans le futur, lorsque la partie A a besoin de l'image traitée, elle obtient d'abord la ressource d'opération asynchrone liée à l'en- Content-Locationtête pour déterminer si le traitement est terminé. Si c'est le cas, la partie A utilise ensuite le lien dans l'opération asynchrone elle-même pour obtenir l'image traitée.

La seule différence ici est que dans le modèle AMQP, la partie B indique à la partie A lorsque le traitement d'image est terminé. Mais dans le modèle REST, la partie A vérifie si le traitement est effectué juste avant d'avoir réellement besoin de l'image. Ces approches sont évolutives de manière équivalente . À mesure que le système s'agrandit, le nombre de messages envoyés dans les stratégies async AMQP et async REST augmente avec une complexité asymptotique équivalente. La seule différence est que le client envoie un message supplémentaire au lieu du serveur.

Mais l'approche REST a encore quelques trucs dans sa manche: découverte dynamique et négociation de protocole . Considérez comment les interactions REST de synchronisation et d'async ont commencé. La Partie A a envoyé exactement la même demande à la Partie B, la seule différence étant le type particulier de message de réussite auquel la Partie B a répondu. Et si la partie A voulait choisir si le traitement d'image était synchrone ou asynchrone? Que se passe-t-il si la partie A ne sait pas si la partie B est même capable d'un traitement asynchrone?

Eh bien, HTTP a déjà un protocole standardisé pour cela! C'est ce qu'on appelle les préférences HTTP, en particulier la respond-asyncpréférence de la RFC 7240 Section 4.1. Si la partie A souhaite une réponse asynchrone, elle inclut un en- Prefer: respond-asynctête avec sa demande POST initiale. Si la partie B décide d'honorer cette demande, elle renvoie une 202 Acceptedréponse qui comprend a Preference-Applied: respond-async. Sinon, la partie B ignore simplement l'en- Prefertête et renvoie 201 Createdcomme d'habitude.

Cela permet à la partie A de négocier avec le serveur, en s'adaptant dynamiquement à l'implémentation du traitement d'image avec laquelle elle se trouve. De plus, l'utilisation de liens explicites signifie que la partie A n'a pas à connaître d'autres parties que B: pas de courtier de messages AMQP, pas de mystérieuse partie C qui sait comment transformer réellement l'adresse d'image en données d'image, pas de deuxième B-Async partie si des demandes synchrones et asynchrones doivent être effectuées, etc. Il décrit simplement ce dont il a besoin, ce qu'il aimerait éventuellement, puis réagit aux codes d'état, au contenu des réponses et aux liens. Ajouter àCache-Controlen-têtes pour des instructions explicites sur le moment de conserver des copies locales des données, et maintenant les serveurs peuvent négocier avec les clients les ressources que les clients peuvent conserver des copies locales (ou même hors ligne!) de. C'est ainsi que vous créez des microservices à tolérance de panne à couplage lâche dans REST.

Jack
la source
1

Le fait que vous ayez ou non besoin d'être uniquement axé sur les événements dépend, bien sûr, de votre scénario spécifique. En supposant que vous en avez vraiment besoin, vous pouvez résoudre le problème en:

Stocker une copie locale et en lecture seule des données en écoutant les différents événements et en capturant les informations dans leurs charges utiles. Bien que cela vous donne des lectures plus rapides pour ces données, stockées sous une forme adaptée à cette application exacte, cela signifie également que vos données seront finalement cohérentes entre les services.

Pour modéliser GET /users/1cette approche, on peut écouter la UserCreatedet des UserUpdatedévénements, et stocker le sous - ensemble utile des données des utilisateurs du service. Lorsque vous devez ensuite obtenir les informations de ces utilisateurs, vous pouvez simplement interroger votre magasin de données local.

Supposons un instant que le service qui expose le /users/point de terminaison ne publie aucune sorte d'événements. Dans ce cas, vous pourriez réaliser une chose similaire en mettant simplement en cache les réponses aux requêtes HTTP que vous faites, éliminant ainsi la nécessité de faire plus d'une requête HTTP par utilisateur dans un certain délai.

Andy Hunt
la source
Je comprends. Mais qu'en est-il de la gestion des erreurs (et des rapports) pour les clients dans ce scénario?
Tony E. Stark
Je veux dire, comment signaler aux clients REST les erreurs qui se produisent lors de la gestion de l' UserCreatedévénement (par exemple, un nom d'utilisateur ou un e-mail en double ou une panne de base de données).
Tony E. Stark
Cela dépend de l'endroit où vous effectuez l'action. Si vous êtes dans le système utilisateur, vous pouvez faire toute votre validation, écrire dans le magasin de données qui s'y trouve, puis publier l'événement. Sinon, je considère qu'il est parfaitement acceptable d'effectuer une demande HTTP standard vers le /users/point de terminaison et de permettre à ce système de publier son événement s'il réussit et de répondre à la demande avec la nouvelle entité
Andy Hunt
0

Avec un système d'origine événementielle, les aspects asynchrones entrent normalement en jeu lorsque quelque chose qui représente l'état, peut-être une base de données ou une vue agrégée de certaines données, est modifié. En utilisant votre exemple, un appel à GET / api / users pourrait simplement renvoyer la réponse d'un service qui a une représentation à jour d'une liste d'utilisateurs dans le système. Dans un autre scénario, la demande à GET / api / users pourrait amener un service à utiliser le flux d'événements depuis le dernier instantané d'utilisateurs pour créer un autre instantané et simplement renvoyer les résultats. Un système piloté par événements n'est pas nécessairement purement asynchrone de la demande à la réponse, mais a tendance à être au niveau où les services doivent interagir avec d'autres services. Souvent, il n'est pas logique de renvoyer de manière asynchrone une demande GET et vous pouvez donc simplement renvoyer la réponse d'un service,

Lloyd Moore
la source