web-dev-qa-db-fra.com

Transactions à travers REST microservices?

Supposons que nous ayons un microservices Utilisateur, Portefeuille REST et une passerelle API qui collent les éléments. Lorsque Bob s'inscrit sur notre site Web, notre passerelle API doit créer un utilisateur via le microservice de l'utilisateur et un portefeuille via le microservice de portefeuille.

Maintenant, voici quelques scénarios où les choses pourraient mal se passer:

  • La création de l'utilisateur Bob échoue: c'est correct, nous renvoyons simplement un message d'erreur à Bob. Nous utilisons des transactions SQL afin que personne n'ait jamais vu Bob dans le système. Tout est bien :)

  • L'utilisateur Bob est créé, mais avant que notre portefeuille puisse être créé, notre passerelle API se bloque. Nous avons maintenant un utilisateur sans portefeuille (données incohérentes).

  • L'utilisateur Bob est créé et, lors de la création du portefeuille, la connexion HTTP est interrompue. La création du portefeuille peut avoir réussi ou non.

Quelles solutions sont disponibles pour éviter ce type d'incohérence dans les données? Existe-t-il des modèles permettant aux transactions de s'étendre sur plusieurs requêtes REST? J'ai lu la page Wikipedia sur Le commit en deux phases qui semble toucher à cette question, mais je ne sais pas comment l'appliquer dans la pratique. This Atomic Distributed Transactions: un projet RESTful papier semble également intéressant bien que je ne l’aie pas encore lu.

Sinon, je sais que REST pourrait ne pas convenir à ce cas d'utilisation. La bonne façon de gérer cette situation serait-elle de supprimer complètement le [REST] _ et d'utiliser un protocole de communication différent, comme un système de file d'attente de messages? Ou devrais-je imposer la cohérence dans mon code d'application (par exemple, en ayant un travail d'arrière-plan qui détecte les incohérences et les corrige ou en ayant un attribut "state" sur mon modèle utilisateur avec des valeurs "creation", "créé", etc.)?

145
Olivier Lalonde

Ce qui n'a pas de sens:

  • transactions distribuées avec les services REST. Par définition, les services REST sont sans état. Ils ne doivent donc pas participer à une limite transactionnelle couvrant plusieurs services. Votre scénario d’utilisation d’enregistrement d’utilisateur est logique, mais la conception avec les microservices REST pour créer des données d’utilisateur et de portefeuille n’est pas satisfaisante. 

Qu'est-ce qui va vous donner des maux de tête: 

  • EJB avec transactions distribuées. C'est l'une de ces choses qui fonctionnent en théorie mais pas en pratique. Pour le moment, j'essaie de faire fonctionner une transaction distribuée pour des EJB distants sur des instances JBoss EAP 6.3. Cela fait des semaines que nous parlons au support technique RedHat, et cela n'a pas encore fonctionné. 
  • Solutions de validation en deux phases en général. Je pense que le protocole 2PC est un excellent algorithme (je l’ai implémenté il ya de nombreuses années en C avec RPC). Cela nécessite des mécanismes complets de récupération après échec, avec des tentatives, un référentiel d'état, etc. Toute la complexité est cachée dans le cadre de la transaction (ex: JBoss Arjuna). Cependant, 2PC n'est pas une preuve d'échec. Il y a des situations où la transaction ne peut tout simplement pas aboutir. Ensuite, vous devez identifier et corriger manuellement les incohérences de la base de données. Cela peut arriver une fois sur un million si vous avez de la chance, mais cela peut arriver une fois sur 100 selon votre plate-forme et votre scénario. 
  • Sagas (transactions compensatoires). Il y a la surcharge de mise en œuvre liée à la création des opérations de compensation et le mécanisme de coordination pour activer la compensation à la fin. Mais l'indemnisation n'est pas une preuve d'échec non plus. Vous pouvez toujours vous retrouver avec des incohérences (= quelques maux de tête). 

Quelle est probablement la meilleure alternative:

  • Cohérence éventuelle. Ni les transactions distribuées de type ACID ni les transactions compensatoires ne sont une preuve irréfutable, et les deux peuvent entraîner des incohérences. La cohérence éventuelle est souvent préférable à une "incohérence occasionnelle". Il existe différentes solutions de conception, telles que:
    • Vous pouvez créer une solution plus robuste utilisant la communication asynchrone. Dans votre scénario, lorsque Bob s'enregistre, la passerelle API peut envoyer un message à une file d'attente NewUser et répondre immédiatement à l'utilisateur en lui disant: "Vous recevrez un courrier électronique pour confirmer la création du compte". Un service consommateur de file d'attente peut traiter le message, effectuer les modifications de la base de données en une seule transaction et envoyer le courrier électronique à Bob pour notifier la création du compte. 
    • Le microservice utilisateur crée l'enregistrement utilisateur et un enregistrement de portefeuille dans la même base de données. Dans ce cas, le magasin de portefeuille figurant dans le microservice de l'utilisateur est une réplique du magasin de portefeuille principal visible uniquement par le microservice de portefeuille. Il existe un mécanisme de synchronisation des données basé sur des déclencheurs ou permettant d'envoyer périodiquement les modifications de données (par exemple, de nouveaux portefeuilles) du réplica au maître, et inversement.

Mais si vous avez besoin de réponses synchrones?

  • Remodeler les microservices. Si la solution avec la file d'attente ne fonctionne pas car le consommateur de service a besoin d'une réponse immédiate, je préférerais alors remodeler les fonctionnalités d'utilisateur et de portefeuille afin qu'elles soient colocalisées dans le même service (ou au moins dans le même VM éviter les transactions distribuées). Oui, c’est un pas en avant par rapport aux microservices et un peu plus proche d’un monolithe, mais cela vous évitera des maux de tête. 
107
Paulo Merson

Tous les systèmes distribués ont des problèmes de cohérence transactionnelle. La meilleure façon de faire est, comme vous l'avez dit, d'avoir un commit en deux phases. Demandez au portefeuille et à l'utilisateur d'être créés dans un état en attente. Une fois créé, effectuez un appel séparé pour activer l'utilisateur.

Ce dernier appel doit pouvoir être répété en toute sécurité (au cas où votre connexion serait interrompue). 

Cela nécessitera que le dernier appel connaisse les deux tables (pour pouvoir le faire dans une seule transaction JDBC). 

Sinon, vous voudrez peut-être réfléchir à la raison pour laquelle vous êtes si inquiet au sujet d'un utilisateur sans portefeuille. Croyez-vous que cela va causer un problème? Si tel est le cas, peut-être que les appels en repos séparés sont une mauvaise idée. Si un utilisateur ne doit pas exister sans portefeuille, vous devez probablement l'ajouter à l'utilisateur (dans l'appel d'origine POST pour créer l'utilisateur).

25
Rob Conklin

IMHO L'un des aspects clés de l'architecture des microservices est que la transaction est limitée au microservice individuel (principe de responsabilité unique). 

Dans l'exemple actuel, la création de l'utilisateur serait une transaction propre. La création de l'utilisateur pousserait un événement USER_CREATED dans une file d'attente d'événements. Le service de portefeuille s'abonnerait à l'événement USER_CREATED et créerait le portefeuille.

9
mithrandir

Si mon portefeuille était simplement un autre groupe d'enregistrements dans la même base de données SQL que l'utilisateur, je placerais probablement le code de création d'utilisateur et de portefeuille dans le même service et le gérerais à l'aide des installations de transaction de base de données normales.

Il me semble que vous demandez ce qui se passe lorsque le code de création de portefeuille nécessite de toucher un ou plusieurs autres systèmes. Je dirais que tout dépend de la complexité et des risques du processus de création. 

S'il s'agit simplement de toucher un autre magasin de données fiable (par exemple, un magasin qui ne peut pas participer à vos transactions SQL), alors, en fonction des paramètres système globaux, je serais peut-être prêt à prendre le risque infime que la deuxième écriture ne se produise pas. Je pourrais ne rien faire, mais lever une exception et traiter les données incohérentes via une transaction de compensation ou même une méthode ad hoc. Comme je le dis toujours à mes développeurs, "si ce genre de chose se produit dans l'application, cela ne passera pas inaperçu".

À mesure que la complexité et les risques liés à la création de portefeuilles augmentent, vous devez prendre des mesures pour réduire les risques. Supposons que certaines étapes nécessitent l’appel de plusieurs partenaires. 

À ce stade, vous pouvez introduire une file de messages avec la notion d’utilisateurs et/ou de portefeuilles partiellement construits.

Une stratégie simple et efficace pour vous assurer que vos entités sont finalement bien construites consiste à faire en sorte que les tâches soient réessayées jusqu'à ce qu'elles aboutissent, mais cela dépend en grande partie des cas d'utilisation de votre application.

Je voudrais aussi réfléchir longuement à la raison pour laquelle j'ai eu une étape sujette aux échecs dans mon processus de provisioning.

7
Robert Moskal

Quelles solutions sont disponibles pour éviter ce type d'incohérence dans les données? 

Traditionnellement, les gestionnaires de transactions distribuées sont utilisés. Il y a quelques années, dans le monde Java EE, vous auriez peut-être créé ces services sous la forme de EJB s déployés sur différents nœuds et votre passerelle d'API aurait effectué des appels à distance vers ces EJB. Le serveur d'applications (s'il est configuré correctement) garantit automatiquement, à l'aide de la validation en deux phases, que la transaction est validée ou annulée sur chaque nœud, afin de garantir la cohérence. Mais pour cela, tous les services doivent être déployés sur le même type de serveur d’applications (afin qu’ils soient compatibles) et n’ont en réalité jamais fonctionné qu’avec des services déployés par une seule entreprise.

Existe-t-il des modèles autorisant les transactions à couvrir plusieurs demandes REST?

Pour SOAP (ok, pas REST), il y a la spécification WS-AT mais aucun service que j'ai jamais eu à intégrer n'a pris en charge cela. Pour REST, JBoss a quelque chose en cours . Sinon, le "modèle" consiste à trouver un produit que vous pouvez connecter à votre architecture ou à créer votre propre solution (non recommandé). 

J'ai publié un tel produit pour Java EE: https://github.com/maxant/genericconnector

Selon le document que vous avez référencé, il existe également le modèle Essayer d’annuler/confirmer et le produit associé d’Atomikos.

Les moteurs BPEL gèrent la cohérence entre les services déployés à distance à l'aide de la compensation.

Sinon, je sais que REST pourrait ne pas convenir à ce cas d'utilisation. La bonne façon de gérer cette situation serait-elle d’abandonner complètement REST et d’utiliser un protocole de communication différent, comme un système de file d’attente? 

Il existe de nombreuses manières de "lier" des ressources non transactionnelles à une transaction:

  • Comme vous le suggérez, vous pouvez utiliser une file de messages transactionnelle, mais celle-ci sera asynchrone. Par conséquent, si vous dépendez de la réponse, cela devient désordonné.
  • Vous pouvez écrire le fait que vous devez appeler les services principaux dans votre base de données, puis appeler les services principaux à l'aide d'un lot. Encore une fois, async, peut donc devenir désordonné.
  • Vous pouvez utiliser un moteur de processus métier comme passerelle d'API pour orchestrer les microservices dorsaux.
  • Comme vous l'avez mentionné au début, vous pouvez utiliser un EJB distant, car cela prend en charge les transactions distribuées prêtes à l'emploi.

Ou devrais-je imposer la cohérence dans mon code d'application (par exemple, en ayant un travail d'arrière-plan qui détecte les incohérences et les corrige ou en ayant un attribut "state" sur mon modèle utilisateur avec des valeurs "creation", "created", etc.)?

Jouer au diable comme avocat: pourquoi construire quelque chose comme ça, alors qu'il existe des produits qui le font pour vous (voir ci-dessus), et le font probablement mieux que vous ne pouvez, parce qu'ils ont fait leurs preuves?

3
Ant Kutschera

Personnellement, j'aime bien l'idée de Micro Services, des modules définis par les cas d'utilisation, mais comme votre question le mentionne, ils ont des problèmes d'adaptation pour les entreprises classiques telles que les banques, les assurances, les télécoms, etc.

Les transactions distribuées, comme beaucoup l'ont mentionné, ne constituent pas un bon choix. Les utilisateurs préfèrent désormais des systèmes cohérents, mais je ne suis pas sûr que cela fonctionnera pour les banques, les assurances, etc.

J'ai écrit un blog sur ma solution proposée, peut-être que cela peut vous aider ....

https://mehmetsalgar.wordpress.com/2016/11/05/micro-services-fan-out-transaction-problems-and-solutions-with-spring-bootjboss-and-netflix-eureka/

2
posthumecaver

Une solution simple consiste à créer un utilisateur à l'aide du service utilisateur et à utiliser un bus de messagerie dans lequel le service utilisateur émet ses événements. Le service Wallet s'enregistre sur le bus de messagerie, écoute l'événement créé par l'utilisateur et crée un portefeuille pour l'utilisateur. En attendant, si l'utilisateur va sur l'interface utilisateur du portefeuille pour voir son portefeuille, vérifiez si l'utilisateur vient d'être créé et s'il montre que la création de votre portefeuille est en cours, veuillez vous enregistrer un peu plus tard.

1
techagrammer

La cohérence éventuelle est la clé ici. 

  • L'un des services est choisi pour devenir le gestionnaire principal de l'événement. 
  • Ce service gérera l'événement d'origine avec un seul commit. 
  • Le gestionnaire principal assumera la responsabilité de la communication asynchrone des effets secondaires à d'autres services. 
  • Le gestionnaire principal se chargera de l'orchestration des autres appels de services. 

Le commandant est responsable de la transaction distribuée et en prend le contrôle. Il connaît l'instruction à exécuter et coordonnera son exécution. Dans la plupart des scénarios, il n'y aura que deux instructions, mais il peut gérer plusieurs instructions. 

Le commandant prend la responsabilité de garantir l'exécution de toutes les instructions, ce qui signifie qu'il se retire. Lorsque le commandant de bord essaie d’effectuer la mise à jour à distance et n’obtient pas de réponse, il n’a plus de tentative. Ainsi, le système peut être configuré pour être moins sujet aux défaillances et il se soigne lui-même. 

Comme nous avons de nouvelles tentatives, nous avons idempotence. Idempotence est la propriété de pouvoir faire quelque chose deux fois de telle sorte que les résultats finaux soient les mêmes que si cela avait été fait une seule fois. Nous avons besoin d’idempotence sur le service distant ou la source de données pour que, dans le cas où il reçoit l’instruction plusieurs fois, il ne la traite qu’une seule fois. 

Cohérence éventuelle. Ceci résout la plupart des problèmes liés aux transactions distribuées, mais nous devons examiner quelques points ici. Chaque transaction échouée sera suivie d’une nouvelle tentative, le nombre de tentatives de tentatives dépend du contexte. 

La cohérence est éventuelle, c’est-à-dire que le système n’est plus en état de cohérence lors d’une nouvelle tentative, par exemple si un client a commandé un livre, effectué un paiement puis mis à jour la quantité en stock. Si les opérations de mise à jour du stock échouent et si le dernier stock disponible est le dernier stock disponible, le livre reste disponible jusqu'à la prochaine tentative de mise à jour du stock. Une fois la nouvelle tentative effectuée, votre système sera cohérent. 

0
Viyaan Jhiingade