Gestion pratique des transactions dans l'architecture de microservices


Photo de profil de l'auteur

@LafernandoAnjana Fernando

Architecte logiciel et évangéliste; Directeur des relations avec les développeurs @ WSO2 Inc.

Il est difficile de gérer les complexités d’un système distribué lors du passage d’une architecture monolithique à une architecture de microservices (MSA). La gestion des transactions est au cœur de ce problème. Une transaction de base de données typique effectuée dans une application Web à l'aide de transactions locales est désormais un problème de transaction distribuée complexe. Dans cet article, nous discuterons des causes de ce problème, des solutions possibles et des meilleures pratiques pour développer des systèmes logiciels transactionnels sûrs à l'aide de MSA.

Si vous êtes déjà familiarisé avec les concepts associés derrière les transactions de base de données et les subtilités de la cohérence des données dans un système distribué, vous pouvez passer à la section sur la modélisation des données dans l'architecture microservice où nous explorons comment modéliser des données avec un cas d'utilisation réel.

Transactions de base de données: une introduction

Dans nos bonnes vieilles applications monolithiques, nous avons effectué des transactions de base de données pour implémenter des opérations de données tout ou rien, tout en gardant la cohérence des données. Nous avons principalement utilisé les transactions ACID, ce que vous trouvez dans les systèmes de bases de données relationnelles. Voici un petit rappel:

  • Atomicité: Toutes les opérations sont exécutées avec succès ou tout échoue ensemble.
  • Cohérence: Les données de la base de données sont conservées dans un état valide, comme indiqué par ses règles, comme l'intégrité référentielle.
  • Isolement: Les transactions séparées exécutées simultanément ne peuvent pas interférer les unes avec les autres. Cela devrait se produire lorsque la transaction s'exécute dans son propre environnement isolé où les autres transactions ne peuvent pas voir les changements qui se produisent pendant cette durée.
  • Durabilité: Une fois la transaction validée, les modifications sont stockées durablement, par ex. persisté dans un disque, donc les pannes temporaires du serveur de base de données ne perdront pas de données.

Nous pouvons imaginer utiliser des transactions ACID comme indiqué ci-dessous, où nous transférons des fonds d'un compte à un autre.

BEGIN TRANSACTION
  UPDATE ACCOUNT SET BALANCE=BALANCE-AMOUNT WHERE ID=1;
  UPDATE ACCOUNT SET BALANCE=BALANCE+AMOUNT WHERE ID=2;
END TRANSACTION

Ici, nous intégrons les opérations de débit et de crédit individuelles dans une transaction ACID. Cela évite les incohérences telles que la perte d'argent du système si les fonds ont été retirés d'un compte mais pas mis sur l'autre compte. Il s'agit d'une solution claire et simple, et nous continuerons à écrire du code comme celui-ci si nécessaire.

Nous sommes habitués à avoir le luxe des transactions ACID chaque fois que nous en avons besoin. Pour la plupart des utilisateurs classiques où les exigences de traitement sont conservées dans un serveur de base de données unique, ce modèle convient. Mais pour les personnes qui ont besoin de faire évoluer leur système avec des exigences croissantes en matière d'accès aux données, de capacité de stockage et de mise à l'échelle en lecture / écriture, ce style d'architecture s'effondre rapidement. Pour ces utilisateurs, il existe deux façons de mettre à l'échelle les magasins de données:

  • Mise à l'échelle verticale: Du matériel plus puissant est placé sur un seul serveur, comme l'augmentation du processeur et de la RAM ou le passage à un ordinateur central. Ceci est généralement plus cher et atteindra une limite lorsque le matériel ne pourra plus être mis à niveau.
  • Mise à l'échelle horizontale: Plus de nœuds de serveur sont ajoutés et tout est connecté via le réseau informatique. C'est souvent la chose la plus pratique à faire.

Dans la mise à l'échelle horizontale, avec un magasin de données comportant plusieurs nœuds dans un cluster, les choses deviennent un peu plus compliquées. Un ensemble de nouveaux défis entre en jeu car les données résident dans des serveurs physiquement séparés. Ceci est expliqué avec le théorème CAP, qui dit que seules deux des propriétés suivantes peuvent être obtenues dans un magasin de données distribué.

  • Cohérence: Ceci concerne la cohérence des données entre les nœuds. Les données auront des répliques pour assurer la redondance en cas d'échec et pour améliorer les performances de lecture. Cela signifie que lorsqu'une mise à jour des données est effectuée en un seul endroit, elle doit être mise à jour simultanément dans toutes les autres répliques sans aucun retard perçu pour les clients. Ceci est plus formellement connu comme ayant une linéarisation. Comme vous pouvez le voir, ce n'est pas le même concept de cohérence dans ACID.
  • Disponibilité: Le magasin de données distribué est hautement disponible, de sorte que la perte d'une instance de serveur n'entrave pas la fonctionnalité globale du magasin de données et les utilisateurs recevront toujours des réponses sans erreur à leurs demandes.
  • Tolérance de partition: Le magasin de données peut gérer les partitions réseau. Une partition réseau est une perte de communication entre certains nœuds du réseau. Ainsi, certains nœuds peuvent ne pas être en mesure de parler avec d'autres nœuds du cluster de stockage de données. Cela partitionne efficacement les nœuds de stockage de données en plusieurs réseaux locaux, dans la vue des nœuds du cluster de bases de données. Un client externe peut toujours avoir accès à tous les nœuds du cluster de base de données.

Selon cela, il est impossible d’avoir en même temps cohérence, disponibilité et tolérance de partition. Nous pouvons comprendre ce comportement intuitivement si nous pensons aux scénarios possibles qui peuvent se produire.

Pour maintenir la cohérence lorsque nous écrivons des données, nous devons écrire simultanément sur tous les serveurs qui agissent en tant que répliques. Cependant, s'il existe des partitions sur le réseau, nous ne pourrons pas le faire car nous ne pouvons atteindre que certains serveurs à ce moment-là. Dans ce cas, si nous voulons tolérer ces partitions tout en conservant la cohérence, nous ne pouvons pas laisser les utilisateurs lire ces données incohérentes à partir du magasin de données.

Cela signifie que nous devons cesser de répondre aux demandes des utilisateurs, ce qui rend le magasin de données plus disponible (cohérent et tolérant aux partitions). Un autre scénario serait de laisser le magasin de données continuer à fonctionner, c'est-à-dire de le garder disponible, ce qui le rendrait plus cohérent (disponible et tolérant aux partitions).

Le dernier scénario est celui où le système ne tolère pas les partitions, ce qui lui permet d’être à la fois cohérent et disponible. Dans ce cas, pour avoir une forte cohérence (linéarisation), nous devrons toujours utiliser un protocole de transaction tel que la validation en deux phases (2PC) pour exécuter les opérations de données entre les nœuds du serveur de base de données répliquée. Dans 2PC, les opérations des bases de données participantes sont exécutées par un coordinateur de transactions dans les deux phases suivantes:

  • Préparer: Le participant est invité à vérifier si son opération de données peut être effectuée et à fournir une promesse au coordinateur de transaction que plus tard, si nécessaire, il peut valider l'opération ou l'annulation. Ceci est fait pour tous les participants.
  • Commettre: Si tous les participants ont dit d'accord dans la phase de préparation, chaque participant reçoit une opération de validation pour valider l'opération mentionnée précédemment. À ce stade, le participant ne peut pas refuser l'opération, en raison de la promesse faite au coordinateur de transaction plus tôt. Si l'un des participants a précédemment refusé son opération de données locales pour une raison quelconque, la coordonnée enverra des commandes de restauration à tous les participants.

Outre le scénario de réplication de base de données ci-dessus pour l'évolutivité, 2PC est également utilisé lors de l'exécution de transactions entre différents types de systèmes tels que les serveurs de base de données et les courtiers de messages. Cependant, nous évitons généralement 2PC en raison de l'augmentation des conflits de verrouillage chez les participants distribués, ce qui entraîne de mauvaises performances et entrave l'évolutivité.

En pratique, les réseaux informatiques ne sont pas fiables et nous devons nous attendre à ce qu’ils aient des partitions réseau à un moment ou à un autre. Nous voyons donc généralement des systèmes de base de données qui donnent la priorité à la disponibilité ou à la cohérence, c'est-à-dire AP ou CP. Certains systèmes permettent à l'utilisateur d'ajuster ces paramètres pour les rendre hautement disponibles ou pour sélectionner le niveau de cohérence.

Ceci est fourni dans les systèmes de base de données tels que DynamoDB d'Amazon et Apache Cassandra. Cependant, ils sont généralement classés comme des systèmes AP car ils n'ont pas une cohérence stricte au niveau du CAP. La prise en charge des transactions légères de Cassandra utilise le protocole de consensus Paxos pour implémenter la cohérence linéarisable, mais cela est rarement utilisé car son mécanisme entraîne de très faibles performances, ce qui est attendu.

Effets sur la cohérence des données avec l'évolutivité

Lorsque nous avons examiné les compromis dans le théorème CAP, si nous valorisons la haute disponibilité avec des performances et une évolutivité élevées, nous devons faire des compromis dans la cohérence des données. Cette propriété de cohérence des données dans CAP affecte la propriété d'isolation d'ACID. Si les données ne sont pas cohérentes entre les nœuds d'un système distribué, cela signifie qu'il y a un problème d'isolation de transaction et que nous pouvons voir des données sales. Donc, dans cette situation, nous devons accepter la réalité et trouver un moyen de vivre sans transactions ACID et sans cohérence avec la PAC.

Ce modèle de cohérence des transactions qui englobe la cohérence éventuelle est également appelé BASE (Basically Available, Soft State et Eventually Consistent), ce qui favorise la disponibilité avec une cohérence éventuelle. L'état souple signifie que les données peuvent changer plus tard en raison de la cohérence éventuelle. La plupart des bases de données NoSQL suivent cette approche, où elles ne fournissent aucune fonctionnalité de transaction ACID, mais se concentrent plutôt sur l'évolutivité.

Il existe de nombreux cas d'utilisation où la cohérence éventuelle est acceptable car une cohérence stricte des données n'est pas requise. Par exemple, DNS (Domain Name System) est basé sur un modèle de cohérence à terme. Plusieurs caches intermédiaires contiennent les entrées DNS. Si quelqu'un met à jour une entrée DNS, celles-ci ne sont pas immédiatement mises à jour, mais plutôt, les requêtes DNS sont effectuées après un délai d'expiration du cache sur les entrées locales.

Étant donné qu'une mise à jour sur une entrée DNS n'est pas fréquente, effectuer une nouvelle requête DNS pour chaque résolution de nom est un peu exagéré et constituera un goulot d'étranglement majeur pour les performances du réseau. Il est donc tolérable d'avoir une entrée obsolète dans DNS pour un utilisateur. De la même manière, il existe de nombreuses autres situations du monde réel où nous ferions cela.

Nous mettrons en œuvre d'autres solutions de contournement pour détecter ce type de données obsolètes ou d'incohérences et prendrons les mesures appropriées à ce stade, plutôt que d'être pessimiste et de rendre toutes les opérations totalement cohérentes et de subir un impact considérable sur les performances.

Dans notre scénario de magasin de données distribué, le niveau de cohérence dépend du cas d'utilisation que nous implémentons. Examinons plus en détail les différents niveaux de cohérence qui existent, du niveau de cohérence le plus élevé au moins fort.

Sérialisation stricte

Dans le cadre d'une sérialisation stricte, les opérations sur plusieurs objets doivent se produire de manière atomique dans tous les réplicas, tout en conservant l'ordre en temps réel. L'ordre en temps réel signifie que c'est le même ordre que les opérations ont été exécutées par les clients en ce qui concerne une horloge globale que tout le monde partage. Ces opérations d'objets groupés représentent des transactions individuelles.

Ceci est nécessaire pour implémenter l'aspect d'isolation des transactions ACID. Ainsi, si nous avons besoin d'un comportement typique que nous trouvons dans les transactions ACID pour la charge de travail, une sérialisabilité stricte est requise dans notre magasin de données distribué.

Linéarisation

Dans ce modèle de cohérence, les opérations d’un seul objet dans les répliques doivent être atomiques. Lorsqu'un seul client voit une opération effectuée sur un objet dans un réplica, tout autre client connecté à un autre réplica doit également voir la même opération. En outre, l'ordre dans lequel les opérations se produisent sur l'objet doit être le même que celui vu par tous les clients et ressembler au même ordre en temps réel.

Quand est-ce nécessaire? Disons que nous avons trois clients / processus. Le processus A écrit une valeur d'objet dans le magasin de données. Le processus B reçoit un événement externe qui l'invite à lire la valeur d'objet susmentionnée. Après avoir lu la valeur, il envoie un message au processus C pour lire également la valeur de l'objet et prendre une décision.

La compréhension du processus B est que la valeur d'objet qu'il lit sera au moins la dernière valeur qu'il a et non une valeur plus ancienne. Pour s'assurer de ce comportement, le magasin de données distribué doit fournir des garanties de cohérence de linéarisation pour s'assurer que les mises à jour effectuées par le processus A sont immédiatement vues par tous les autres réplicas en même temps.

Même si nous configurons la base de données Cassandra pour avoir une cohérence forte en utilisant une approche telle que la lecture / écriture basée sur le quorum, cela ne fournirait pas de garanties de linéarisation, car les mises à jour ne se produisent pas de manière atomique dans les réplicas. Pour avoir la linéarisation, nous devons utiliser la prise en charge des transactions légères dans Cassandra.

Cohérence séquentielle

En cohérence séquentielle, un processus effectuant des opérations sur un magasin de données apparaîtra dans le même ordre pour les autres processus. De plus, cet ordre de toutes les opérations sera cohérent avec l’ordre relatif des opérations de chaque processus. Fondamentalement, il préserve l'ordre des niveaux de processus dans l'ordre global des opérations.

Cohérence causale

Dans la cohérence causale, toute opération causale potentielle doit être visible dans le même ordre pour tous les processus. En termes simples, si vous effectuez une opération basée sur une opération séparée observée précédemment, l'ordre de ces opérations doit également être le même pour les autres processus.

Pour satisfaire la cohérence causale, les comportements suivants doivent être pris en charge:

  • Lisez vos écrits: Un processus doit être capable de lire immédiatement les modifications qu'il a effectuées avec les opérations d'écriture précédentes.
  • Lectures monotones: Un processus effectuant une opération de lecture sur un objet doit toujours voir la même valeur ou une valeur plus récente. Fondamentalement, l'opération de lecture ne peut pas revenir en arrière et voir une valeur plus ancienne.
  • Écrits monotones: Un processus effectuant des opérations d'écriture doit s'assurer que les autres processus doivent voir ces opérations d'écriture dans leur ordre relatif.
  • Écrit suivre les lectures: Un processus écrit une nouvelle valeur v2 sur un objet en lisant une valeur antérieure v1 basée sur une opération d'écriture antérieure. Dans ce cas, tout le monde devrait toujours voir les valeurs de l'objet v1 avant v2.

Il s'agit d'un modèle de cohérence particulièrement utile avec de nombreuses applications pratiques de la vie réelle. Par exemple, prenons un site Web de réseau social soutenu par un magasin de données distribué. Nous avons des utilisateurs simultanés qui interagissent avec le site Web, qu'il s'agisse de mettre à jour l'état de leur profil ou de commenter l'état du profil d'une personne. L'utilisateur Anne, qui vient d'avoir un petit accident, met le statut «A eu un petit accident, en attendant les résultats de la radiographie!». Juste après avoir mis ce statut, elle obtient ses résultats et il n'y a pas de fracture. Elle met donc à jour son statut en «Bonne nouvelle: pas de fracture!». Bob voit ce dernier message d'Anne et répond à son statut en disant «Content d'entendre! 🙂 ».

Dans le scénario ci-dessus, si notre magasin de données fournit au moins une cohérence causale, tous les autres utilisateurs verront les messages d'Anne et de Bob dans le bon ordre. Mais si le magasin de données ne fournit pas de cohérence causale, il est possible que d'autres utilisateurs voient le premier message d'Anne puis le message de Bob sans voir la deuxième mise à jour d'Anne. Cette situation devient un peu étrange car il semble que Bob ait exprimé son bonheur pour le malheur d'Anne, ce qui n'était pas le cas. Nous avons donc besoin d'une cohérence causale dans une situation comme celle-ci.

Donc, à condition que nous ayons un magasin de données cohérent causal, dans la même situation, disons que Tom met à jour son statut pour dire "Je viens de recevoir ma première voiture!" juste avant qu'Anne ne mette sa première mise à jour. Certains utilisateurs du site Web voient son statut après le premier message d'Anne. Cette situation est bonne car peu importe si la mise à jour de Tom a eu lieu avant ou après la mise à jour d'Anne dans la vie réelle. Il n'y a aucun lien entre eux, c'est-à-dire que les actions de Tom n'ont pas été causées par les actions d'Anne. D'autres opérations qui n'ont pas de relation causale fonctionnent de manière finalement cohérente.

MongoDB est un magasin de données distribué qui prend spécifiquement en charge la cohérence causale. Il a implémenté cela sur la base de l'horloge logique Lamport.

Cohérence éventuelle

En clair, s'il n'y a plus d'écritures dans un magasin de données, tous les réplicas du magasin de données convergent et finissent par s'entendre sur une valeur finale. Il ne fournira aucune autre garantie, telle que la cohérence causale avant de se stabiliser sur la valeur finale.

En pratique, ce modèle de cohérence convient aux cas d'utilisation où il ne s'agit que de mises à jour de valeurs simultanées et il n'y a pas de connexions entre les valeurs lorsqu'elles sont mises à jour; les utilisateurs ne se soucient pas des valeurs intermédiaires, mais uniquement de la valeur stable finale avec laquelle il aboutit. Par exemple, prenons un site Web qui affiche la température actuelle pour chaque ville. Ces valeurs changent de temps en temps. À un moment donné, certains utilisateurs peuvent voir la dernière valeur de température, tandis que d'autres n'ont toujours pas été mis à jour. Cependant, à terme, la mise à jour des données sera rattrapée par tous les utilisateurs du site Web. Donc, ce délai de propagation possible d'une base de données distribuée qui stocke ces valeurs n'est pas un gros problème tant qu'à la fin, tous les utilisateurs verront finalement les mêmes valeurs de température.

Pour plus d'informations sur les modèles de cohérence des transactions, veuillez consulter la section des ressources à la fin de l'article.

Nous avons maintenant une compréhension générale des aspects liés au traitement des transactions et aux modèles de cohérence. Ces connaissances sont utiles lorsque vous travaillez dans un environnement de traitement distribué, tel que MSA. Les mêmes concepts dont nous avons parlé s'appliqueront là aussi. Voyons maintenant comment modéliser des données dans MSA.

Modélisation des données dans l'architecture de microservices

Une exigence fondamentale d'un microservice est d'être hautement cohésif et faiblement couplé. Ceci est naturellement nécessaire car la structure organisationnelle d'une équipe de développement sera également construite autour de ce concept. Il y aura des équipes distinctes responsables des microservices, et elles auront besoin de flexibilité et de liberté pour être indépendantes des autres. Cela signifie qu'ils évitent toute synchronisation indésirable avec d'autres équipes sur la conception et les détails internes des implémentations.

Avec ces exigences, les microservices ne doivent strictement pas partager de bases de données. Si chaque microservice ne peut pas avoir sa propre base de données, c'est un bon indicateur que ces microservices doivent être fusionnés.

Ce qui suit montre une conception de microservices possible d'un backend de commerce électronique.

Ici, nous gérons chaque aspect du système avec son propre microservice. Cela semble bon jusqu'à ce que nous arrivions au traitement des transactions. Une opération typique consisterait à créer une commande utilisateur avec un ensemble de produits. La disponibilité de ces produits est vérifiée à l'aide du service d'inventaire, et une fois la commande finalisée, l'inventaire est mis à jour pour réduire le stock disponible de ces produits. Dans une application monolithique classique, vous pouvez effectuer les opérations suivantes en une seule transaction ACID.

BEGIN TRANSACTION
  CHECK INVENTORY OF PRODUCT 1 FOR 5 ITEMS
  CHECK INVENTORY OF PRODUCT 2 FOR 10 ITEMS
  CREATE ORDER
  ADD 5 ITEMS OF PRODUCT 1 TO ORDER
  ADD 10 ITEMS OF PRODUCT 2 TO ORDER
  DECREMENT INVENTORY OF PRODUCT 1 BY 5 ITEMS
  DECREMENT INVENTORY OF PRODUCT 2 BY 10 ITEMS
  INSERT PAYMENT 
END TRANSACTION

Dans cette approche, nous sommes convaincus que le magasin de données se retrouve dans un état cohérent après les opérations de données. Maintenant, comment modéliser les opérations ci-dessus à l'aide de nos microservices? Une solution possible qui peut venir à l'esprit est l'approche ci-dessous.

Ici, un service de coordination "Admin" crée une orchestration de service en appelant l'opération de chaque service. Cela fonctionnerait si toutes les opérations s'exécutaient sans aucun problème. Mais il y a de fortes chances qu'une étape du flux échoue, par exemple si une erreur d'application se produit lorsqu'un utilisateur ne dispose pas de suffisamment de crédit ou que la communication réseau échoue. Par exemple, si le flux échoue en raison de l'indisponibilité du service de gestion des utilisateurs pour le service de traitement des paiements, l'étape 4 échouera. Mais à ce stade, nous avons déjà créé une commande et mis à jour l'inventaire. Alors maintenant, nous avons un état incohérent dans notre système, où notre inventaire rapporte un plus petit nombre de marchandises sans que personne ne les achète! Le problème clair ici est que nous ne faisions pas ces opérations en une seule transaction où si une étape échoue, toutes les opérations seraient annulées, nous laissant avec un système cohérent.

Quelles sont les solutions possibles à notre problème? La solution la plus simple serait de revenir à une solution monolithique en rassemblant toutes ces opérations dans un seul service et une seule base de données et en effectuant toutes les opérations dans une transaction locale. Mais dans cette situation, supposons que nous ayons pris la décision de ne pas mettre à l'échelle la grande application monolithique et que nous devons la diviser en microservices individuels.

Dans ce cas, pour notre problème de transaction, nous nous retrouvons avec une solution basée sur 2PC. Nous pouvons utiliser une solution telle que WS-TX ou la fonctionnalité de transaction distribuée de Ballerina pour exécuter une transaction globale basée sur 2PC entre les services réseau. Des approches similaires à celles-ci sont la seule option si vous avez besoin de garanties ACID dans votre transaction. Cependant, cela doit être utilisé avec précaution, car les inconvénients typiques de 2PC tels que l'augmentation des temps de verrouillage dans les bases de données principales sont toujours présents. Cette nature ne fait qu'augmenter dans un environnement de microservices en raison de sauts de communication réseau supplémentaires.

Cependant, la plupart des workflows réels n'ont pas besoin de garanties ACID car les actions incorrectes peuvent être annulées en utilisant le contraire de l'action. Ainsi, dans notre workflow de traitement des commandes, si quelque chose ne va pas, nous pouvons exécuter des opérations de compensation pour les opérations déjà effectuées et annuler la transaction complète. Ces actions consisteront par exemple à créditer un paiement sur la carte de crédit de l'utilisateur, à mettre à jour l'inventaire de produits en rajoutant le nombre de produits de la commande et à mettre à jour l'enregistrement de commande comme annulé.

Notre scénario backend de commerce électronique ne peut en fait pas être modélisé uniquement comme une transaction de base de données unique car l'action de traitement des paiements est effectuée à l'aide d'une passerelle de paiement externe, qui ne peut pas faire partie d'une transaction de base de données locale ou globale (2PC). Cependant, il existe une exception à cette situation dans le cas d'une transaction globale basée sur 2PC. Dans le scénario 2PC, le tout dernier participant de la transaction globale n'a pas à implémenter à la fois les phases de préparation et de validation, mais une seule opération de préparation suffit pour exécuter son opération. C'est ce qu'on appelle la dernière optimisation de validation des ressources. Là encore, cela n'est possible qu'avec ce scénario spécifique, où ce type de participant à tout autre endroit du workflow ne permettra pas d'avoir une transaction globale.

Alors maintenant, nous avons décidé que nous n'avons pas besoin de la stricte cohérence des données que nous obtenons avec 2PC, et nous sommes d'accord pour corriger les problèmes plus tard. Voyons une possible exécution de ce workflow.

Ici, le flux de travail échoue à l'étape 4. À partir de là, le service d'administration doit exécuter un ensemble d'opérations de compensation dans l'ordre inverse des services appelés précédemment. Mais il y a un problème potentiel ici. Que faire si le service d'administration rencontre une erreur telle qu'un problème de réseau temporaire lors de la restauration des opérations? Nous avons à nouveau un problème d'incohérence des données où la restauration totale n'est pas effectuée, et nous n'avons aucun moyen de connaître les dernières opérations que nous avons effectuées et comment y remédier plus tard.

Une façon de gérer ce problème serait de conserver un journal des opérations effectuées par le service d'administration. Cela pourrait être similaire à ce qui suit:

TX1: CHECK INVENTORY
TX1: CREATE ORDER
TX1: UPDATE INVENTORY
TX1: PROCESS PAYMENT - FAILED
TX1: MARK ORDER AS CANCELLED
TX1: UPDATE INVENTORY - INCREMENT STOCK COUNTS

Ainsi, le service d'administration peut suivre les opérations qui ont déjà été exécutées et ce qui ne l'a pas été. Mais encore une fois, nous devons être attentifs aux cas extrêmes qui peuvent survenir même lors du traitement d'un journal d'événements comme celui-ci. Le service d'administration et son journal sont séparés des autres opérations de service à distance, donc ces interactions elles-mêmes ne fonctionnent pas de manière transactionnelle. Il y a des changements comme les suivants:

  • Le service d'administration exécute le service d'inventaire pour annuler les comptages d'inventaire (par incrémentation)
  • Le service d'administration met à jour son journal avec "TX1: UPDATE INVENTORY – INCREMENT STOCK COUNTS"

Que faire si la première opération ci-dessus s'exécute, puis le service se bloque avant la deuxième opération? Lorsque le service d'administration continuera ses opérations, il pensera qu'il n'a pas effectué l'opération de restauration de l'inventaire et exécutera à nouveau la première opération. Cela laissera les données d'inventaire avec des valeurs non valides, car il a effectué deux fois des incréments de numéro de stock, ce qui n'est pas bon! Il s'agit d'une situation de livraison au moins une fois, comme on le verrait souvent dans un système distribué. Une approche courante pour gérer cela serait de modéliser nos opérations pour qu'elles soient idempotentes. Autrement dit, même si la même opération est effectuée plusieurs fois, elle ne causera aucun dommage et l’état du système cible serait le même.

Mais notre opération de restauration d'inventaire n'est pas idempotente, car elle ne définit pas une valeur spécifique, mais elle incrémente une valeur déjà existante dans le système cible. Vous ne pouvez donc pas dupliquer ces opérations. Nous pouvons en faire une opération idempotente en définissant directement le décompte des stocks qui existait avant de passer notre commande. Mais là encore, en raison de la façon dont notre transaction est modélisée dans l'architecture de microservice, elle ne fournit aucun type de propriétés d'isolation comme vous le trouveriez dans les transactions ACID, c'est-à-dire un niveau de cohérence de sérialisation strict. Autrement dit, lorsque notre transaction est exécutée, un autre utilisateur peut également créer une autre commande impliquant les mêmes produits, ce qui modifiera les mêmes enregistrements d'inventaire. Pour cette raison, les actions de ces deux transactions peuvent se chevaucher et conduire à des scénarios incohérents.

Donc en fait, non seulement dans le scénario d'une annulation de transaction, même dans deux transactions réussies simultanées, en raison du manque d'isolation, une opération idempotente peut avoir pour effet de perdre la mise à jour des données. Voyons ci-dessous la chronologie de deux opérations de transaction.

Ici, nous avons deux transactions TX1 et TX2. Ils passent tous les deux des commandes pour le produit «P1», qui a un inventaire de départ de 100. TX1 va créer une commande avec 10 articles, puis TX2 va créer une commande avec 20 articles. Comme nous pouvons le voir dans le diagramme de séquence ci-dessus, la vérification de l'inventaire et la mise à jour de l'inventaire ne se produisent pas de manière atomique dans les deux transactions, mais plutôt, elles sont entrelacées et la mise à jour de l'inventaire finale de TX2 éclipse la mise à jour de l'inventaire de TX1. Ainsi, le nombre final de produits de P1 est de 80 alors qu'il aurait dû être de 70 en raison de 30 produits achetés par deux transactions. Alors maintenant, les données de la base de données d'inventaire signalent de manière incorrecte l'inventaire disponible réel.

Nous avons donc une condition de concurrence due au fait que deux processus concurrents ne sont pas correctement isolés. L'approche pour résoudre cette situation consiste à faire de l'opération de mise à jour d'inventaire une opération de décrémentation de valeur, c'est-à-dire

decrementInventoryStockCount(product, offset)

, par rapport à la valeur existante dans la base de données. Ainsi, cette opération peut être transformée en une opération atomique dans le service cible à l'aide d'une seule opération SQL ou d'une seule transaction locale en cours d'exécution.

Donc, avec cette mise à jour, l'interaction ci-dessus peut être réécrite de la manière suivante.

Comme nous pouvons le voir maintenant, avec les nouvelles opérations pour décrémenter les décomptes dans l'inventaire, nos décomptes finaux vont être cohérents et corrects.

Remarque: Nous aurions toujours une situation d'erreur différente, où après vérification des inventaires initiaux, au moment de la commande, l'inventaire du produit peut être vide là où d'autres transactions ont déjà ramené le stock complet. Cela serait simplement traité comme une erreur de processus métier dans laquelle nous pouvons annuler les opérations et les données resteront cohérentes.

Parfois, il n'est pas possible de rendre les opérations de données idempotentes en raison de problèmes d'isolement des transactions que nous rencontrons dans une communication de microservices. Cela signifie que nous ne pouvons pas réessayer aveuglément des opérations dans une transaction si nous ne savons pas si une opération a été exécutée dans un microservice distant. Cela peut être résolu en ayant un identifiant unique tel qu'un ID de transaction pour l'opération de l'appel de microservice, de sorte que le microservice cible créera un historique des transactions exécutées contre lui. De cette manière, pour chaque appel d'opération de microservice, il peut effectuer une transaction locale pour vérifier l'historique et voir si cette transaction a déjà été exécutée. Sinon, il exécutera l'opération de base de données et, toujours dans la transaction locale, mettra également à jour la table d'historique des transactions. Le code ci-dessous montre une implémentation possible pour le

decrementInventoryStockCount

opération dans le service d'inventaire en utilisant la stratégie ci-dessus.

function decrementInventoryStockCount(txid, pid, offset) 
   transaction 
       tx_executed = check transaction table record where id=txid
       if not tx_executed 
           prod_count = select count from inventory where product=pid
           prod_count += offset
           set count=prod_count in inventory where product=pid
           insert to transaction table with id=txid
       
   

Microservices et messagerie

Alors maintenant, nous avons trouvé un moyen cohérent d'exécuter nos transactions à travers un ensemble de microservices, avec d'éventuelles garanties tout ou rien. Dans ce flux, nous devons toujours maintenir notre journal des événements et le mettre à jour dans un stockage persistant fiable. Si le service de coordination qui exécute l'orchestration tombe en panne, une autre entité devra le déclencher à nouveau pour vérifier le journal des événements et effectuer des actions de récupération. Ce serait bien si nous disposions déjà d'un middleware qui pourrait être utilisé pour fournir une communication fiable entre les services pour vous aider dans cette tâche.

C'est là qu'une architecture événementielle (EDA) est utile. Cela peut être créé à l'aide d'un courtier de messages pour la communication entre les microservices. En utilisant ce modèle, nous pouvons nous assurer que si nous émettons avec succès un message à un courtier de messages destiné à cibler un service, il sera à un moment donné envoyé avec succès au destinataire prévu. Cette garantie rend nos autres processus beaucoup plus faciles à modéliser. En outre, le modèle de communication asynchrone du courtier de messages, où il permet la lecture et l’écriture simultanées, offre de bien meilleures performances en raison de la réduction des frais généraux et des temps d’attente pour les appels aller-retour. La gestion des erreurs est également plus simple car même si le service cible est en panne, le courtier de messages conservera les messages et les remettra lorsque le point de terminaison cible est disponible. En outre, il peut effectuer d'autres opérations telles que des tentatives d'échec et des demandes d'équilibrage de charge avec plusieurs instances de service. Ce modèle encourage également un couplage lâche entre les services. La communication se fait via la file d'attente / les sujets, et les producteurs et les consommateurs n'ont pas besoin de se connaître explicitement. Le modèle Saga suit ces directives générales lors de la mise en œuvre de transactions dans des microservices, basées sur des opérations de compensation.

Il existe deux stratégies de coordination pour mettre en œuvre ce modèle: la chorégraphie et l'orchestration.

Chorégraphie

Dans cette approche, les services eux-mêmes sont conscients du flux des opérations. Une fois qu'un message initial est envoyé à une opération de service, il génère le message suivant à envoyer à l'opération de service suivante. Le service doit avoir une connaissance explicite du flux de la transaction conduisant à davantage de couplage entre les services. Le diagramme ci-dessous montre les interactions typiques entre les services et les files d'attente lors de la mise en œuvre d'un flux de travail transactionnel en tant que chorégraphie.

Ici, nous pouvons voir que le flux démarre à partir du client qui envoie le message initial au premier service via sa file d'attente de messages d'entrée. Dans sa logique métier, il peut effectuer ses opérations liées aux données dans une transaction locale définie au sein du service. Une fois les opérations terminées, le flux de travail global ajoute un message à la file d'attente des demandes du service suivant dans la chorégraphie. De cette manière, le contexte de transaction global sera propagé à travers ces messages vers chacun des services jusqu'à son achèvement.

En cas d'échec d'un service dans le workflow, nous devons annuler la transaction globale. Pour cela, à partir du service à l'origine de la panne, il nettoiera ses ressources, et enverra un message via une file d'attente de compensation au service qui a été exécuté juste avant. Cela déplace l'exécution de l'étape précédente, effectue toutes les actions de compensation pour annuler les modifications effectuées par sa transaction locale et répète l'opération de contact avec son service précédent pour les opérations de compensation. En cela, la chaîne de gestion des erreurs atteindra le premier service qui enverra ensuite un message à la file d'attente de réponses. This is connected to the client to notify that an error has occurred and the total transaction has been rolled back successfully using compensation actions.

As seen in the synchronous service invocation approach, when executing the local transactions in their respective services, we should maintain a transaction history table to ensure we don’t repeat the local operations in case the service receives duplicate messages. Also, in order to not lose the continuity of the workflow, a service should acknowledge the message from its request queue only after the database transaction is done and the next message is added to the request queue of the following service. This flow makes sure we don’t lose any messages and the overall transaction will finish executing either by succeeding or rolling back all the operations.

Orchestration

In this coordination approach, we have a dedicated coordinator service that calls other services in succession. The communication between the coordinator services and other services will be done via request and response queues. This pattern is similar to the “Admin” service in our e-commerce scenario. The only change is to use messaging for communication.

The asynchronous communication between the coordinator service and the other services allows it to model the transactional process as a state machine, wherein each of the steps completed with the services can update the state machine. The state machine should be persisted in a database to recover from any failures of the coordination service. The diagram below shows how the orchestration coordination approach is designed using a message-driven strategy.

Compared to choreography, orchestration has lesser coupling between the services. This is because the workflow is driven by the coordination service, and the full state at a given moment is retained in that service itself. But still, the services here are not totally independent since their requests and responses are bound to specific queues, where a fixed producer and a fixed consumer are using those queues. So it’s now harder to use these services as generic services.

Choreography based coordination probably is feasible when we have a fewer number of operations. For complicated operations, the orchestration based approach will have more flexibility when modeling the operations.

When implementing this strategy, it is important to abstract out the finer details of communication, state machine’s persistence, and so on in a developer framework. Or else, a developer will end up writing more code for implementing the transaction handling rather than the core business logic. Also, it would be much more error-prone if a typical developer always implemented this pattern from scratch.

Selecting a Transaction Model for Microservices

In any technology we use when implementing transactions, we need to be explicit on the data consistency guarantees that each approach will give. Then we have to cross-check with our business needs to see what is most suitable for us. The following can be used as a general guideline.

  • 2PC: In the case where the microservices are created using different programming languages/frameworks/databases, and probably development teams from different companies, it is not possible to combine all operations to a single service. Also, it would require strict data consistency, where any data isolation issues, such as dirty reads cannot be tolerated in relation to the business needs.
  • Compensation-based transactions: This is using a transaction coordination mechanism to track the individual steps in a transaction, and in the case of a failure, to execute compensation operations to roll back the actions. This should generally involve the usage of idempotent data operations or commutative updates in order to handle message duplicate scenarios. Your business requirements should be able to handle eventual consistency behavior such as dirty reads.
  • Consolidating services: Use this in case 2PC is not tolerated due to performance issues and scalability, but strict data consistency is required for business needs. In this case, we should group related functionality to their own single services and use local transactions.

Sommaire

In this article we have looked at the basics of transaction handling from ACID guarantees to loosening data consistency guarantees with BASE, and how the CAP theorem defines data store tradeoffs in a distributed system. We then analyzed the different data consistency levels that can be supported in distributed data stores and in general distributed processes. These data consistency concerns are directly applicable to modeling transactions in an MSA, where we need to bring together individual independent services to carry out a global transaction.

We looked at the benefits and tradeoffs of each approach and checked the general guidelines when selecting an option in relation to the business requirements.

Resources

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.



Source link

Pourquoi composer une vitrine sur la toile ?

Il n’a des fois été aussi facile de projeter un site web e-commerce de à nous jours, il assez de voir le nombre de websites e-commerce en France pour s’en rembourser compte. En effet, 204 000 plateformes web actifs en 2016. En 10 ans, le nombre de plateformes web est fois 9. Avec l’évolution des technologies, média à grand coup d’histoire de succès story, (si dans l’hypothèse ou je vous assure, mon nom c’est aussi tombé dans le panneau) le commerce électronique est longuement été vu comme un eldorado. Du coup, une concurrence accrue a vu le jour dans thématiques.