Modèles de données cloud natifs
Conseil
Ce contenu est un extrait du livre électronique, Cloud Native .NET apps for Azure (Architecture d’applications .NET natives cloud pour Azure), disponible dans la documentation .NET ou au format PDF à télécharger gratuitement pour le lire hors connexion.
Comme nous l’avons vu dans ce livre, une approche native cloud change la façon dont vous concevez, déployez et gérez des applications. Elle modifie également la façon dont vous gérez et stockez des données.
La figure 5-1 contraste les différences.
Figure 5-1. Gestion des données dans les applications natives cloud
Les développeurs expérimentés reconnaîtront facilement l’architecture sur le côté gauche de la figure 5-1. Dans cette application monolithique, les composants de service métier sont colocalisés dans un niveau de services partagés, en partageant des données à partir d’une base de données relationnelle unique.
De bien des façons, une base de données unique permet de simplifier la gestion des données. L’interrogation de données sur plusieurs tables est simple. Modifications apportées aux données ensemble, ou restauration de toutes les données. Les transactions ACID garantissent une cohérence forte et immédiate.
En concevant pour le cloud natif, nous adoptons une approche différente. Sur le côté droit de la figure 5-1, notez comment les fonctionnalités métier se séparent en microservices petits et indépendants. Chaque microservice encapsule une fonctionnalité métier spécifique et ses propres données. La base de données monolithique se décompose en modèle de données distribué avec de nombreuses bases de données plus petites, chacune s’alignant avec un microservice. Lorsque la fumée se dissipe, nous émergeons avec une conception qui expose une base de données par microservice.
Pourquoi une base de données par microservice ?
Cette base de données par microservice offre de nombreux avantages, en particulier pour les systèmes qui doivent évoluer rapidement et prendre en charge une grande échelle. Avec ce modèle...
- Les données de domaine sont encapsulées dans le service
- Le schéma de données peut évoluer sans impact direct sur les autres services
- Chaque magasin de données peut effectuer une mise à l’échelle indépendante
- Une défaillance du magasin de données dans un service n’aura pas d’impact direct sur les autres services
Le fait de séparer les données permet également à chaque microservice d’implémenter le type de magasin de données le mieux adapté à sa charge de travail, ses besoins de stockage et ses modèles de lecture/écriture. Les choix incluent les magasins de données relationnels, les documents, les clés-valeurs et même les magasins de données basés sur des graphiques.
La figure 5-2 présente le principe de persistance polyglotte dans un système natif cloud.
Figure 5-2. Persistance des données polyglotte
Notez dans la figure précédente comment chaque microservice prend en charge un autre type de magasin de données.
- Le microservice du catalogue de produits consomme une base de données relationnelle pour prendre en charge la structure relationnelle riche de ses données sous-jacentes.
- Le microservice de panier d’achat consomme un cache distribué qui prend en charge son magasin de données simple à clé-valeur.
- Le microservice de commande consomme à la fois une base de données de documents NoSql pour les opérations d’écriture, ainsi qu’un magasin de clés/valeurs hautement dénormalisé pour prendre en charge les volumes élevés d’opérations de lecture.
Bien que les bases de données relationnelles restent pertinentes pour les microservices avec des données complexes, les bases de données NoSQL ont acquis une popularité considérable. Elles fournissent une grande mise à l’échelle et une haute disponibilité. Leur nature sans schéma permet aux développeurs de s’éloigner d’une architecture de classes de données typées et d’ORM qui rendent les modifications coûteuses et fastidieuses. Nous aborderons les bases de données NoSQL plus loin dans ce chapitre.
Bien que l’encapsulation de données dans des microservices distincts puisse augmenter l’agilité, les performances et l’extensibilité, elle présente également de nombreux défis. Dans la section suivante, nous discutons de ces défis avec les modèles et les pratiques pour mieux les surmonter.
Requêtes interservices
Bien que les microservices soient indépendants et se concentrent sur des fonctionnalités spécifiques, comme l’inventaire, l’expédition ou la commande, ils nécessitent fréquemment une intégration avec d’autres microservices. Souvent, l’intégration implique qu’un microservice en interroge un autre pour obtenir des données. La figure 5-3 illustre ce scénario.
Figure 5-3. Interrogation entre microservices
Dans la figure précédente, nous voyons un microservice de panier d’achat qui ajoute un article au panier d’achat d’un utilisateur. Bien que le magasin de données de ce microservice contient des données d’article de panier et de ligne, il ne conserve pas les données de produit ou de tarification. Ces éléments de données appartiennent plutôt aux microservices de catalogue et de tarifs. Cet aspect présente un problème. Comment le microservice de panier d’achat peut-il ajouter un produit au panier d’achat de l’utilisateur lorsqu’il n’a pas de données de produit ni de tarification dans sa base de données ?
Une option décrite dans le chapitre 4 est un appel HTTP direct du microservice de panier d’achat vers ceux de catalogue et de tarifs. Toutefois, dans le chapitre 4, nous avons dit que les appels HTTP synchrones associent étroitement les microservices, réduisant leur autonomie et leurs avantages architecturaux.
Nous pouvons également implémenter un modèle de réponse aux requêtes avec des files d’attente entrantes et sortantes distinctes pour chaque service. Toutefois, ce modèle est compliqué et nécessite une structure pour mettre en corrélation les messages de requête et de réponse. Bien qu’il découple les appels de microservice du back-end, le service appelant doit toujours attendre de façon synchrone que l’appel soit terminé. De la congestion réseau, des erreurs temporaires ou une surcharge de microservice peuvent entraîner des opérations de longue durée, voire des échecs.
Au lieu de cela, un modèle largement accepté pour la suppression des dépendances entre services est le modèle de vue matérialisée, illustré dans la figure 5-4.
Figure 5-4. Modèle de vue matérialisée
Avec ce modèle, vous placez une table de données locale (appelée modèle de lecture) dans le service de panier d’achat. Ce tableau contient une copie dénormalisée des données nécessaires à partir des microservices de produit et de tarification. La copie des données directement dans le microservice de panier d’achat élimine le besoin d’appels interservices coûteux. Avec les données locales au service, vous améliorez le temps de réponse et la fiabilité du service. En outre, avoir sa propre copie des données rend le service de panier d’achat plus résilient. Si le service de catalogue doit devenir indisponible, il n’y aurait pas d’impact direct sur le service de panier d’achat. Le panier d’achat peut continuer à fonctionner avec les données de son propre magasin.
Le problème avec cette approche est que vous avez désormais des données en double dans votre système. Toutefois, la duplication stratégique des données dans les systèmes natifs cloud est une pratique établie et n’est pas considérée comme un anti-modèle ou une mauvaise pratique. Gardez à l’esprit qu’un seul service peut posséder un jeu de données et avoir l’autorité sur celui-ci. Vous devez synchroniser les modèles de lecture lorsque le système d’enregistrements est mis à jour. La synchronisation est généralement implémentée via la messagerie asynchrone avec un modèle publication/abonnement, comme illustré dans la figure 5.4.
Transactions distribuées
Bien que l’interrogation de données entre microservices soit difficile, l’implémentation d’une transaction entre plusieurs microservices est encore plus complexe. Le défi inhérent de maintenir la cohérence des données entre des sources de données indépendantes dans différents microservices ne peut pas être sous-estimé. L’absence de transactions distribuées dans des applications natives cloud signifie que vous devez gérer les transactions distribuées par programmation. Vous passez d’un monde de cohérence immédiate à une cohérence à terme.
La figure 5-5 illustre ce problème.
Figure 5-5. Implémentation d’une transaction entre microservices
Dans la figure précédente, cinq microservices indépendants participent à une transaction distribuée qui crée une commande. Chaque microservice gère son propre magasin de données et implémente une transaction locale pour son magasin. Pour créer la commande, la transaction locale pour chaque microservice individuel doit réussir, ou chacun doit abandonner et restaurer l’opération. Bien que la prise en charge transactionnelle intégrée soit disponible à l’intérieur de chacun des microservices, il n’existe aucune prise en charge d’une transaction distribuée qui s’étend sur les cinq services pour maintenir la cohérence des données.
Au lieu de cela, vous devez construire cette transaction distribuée par programmation.
Un modèle populaire pour l’ajout d’une prise en charge transactionnelle distribuée est le modèle de saga. Il est implémenté en regroupant des transactions locales en appelant par programmation et séquentiellement chacune d’elles. Si l’une des transactions locales échoue, la saga abandonne l’opération et appelle un ensemble de transactions de compensation. Les transactions de compensation annulent les modifications apportées par les transactions locales précédentes et restaurent la cohérence des données. La figure 5-6 montre une transaction ayant échoué avec le modèle de saga.
Figure 5-6. Restauration d’une transaction
Dans la figure précédente, l’opération Mettre à jour l’inventaire a échoué dans le microservice d’inventaire. La saga appelle un ensemble de transactions de compensation (en rouge) pour ajuster les chiffres de l’inventaire, annuler le paiement et la commande, et retourner les données de chaque microservice à un état cohérent.
Les modèles de saga sont généralement chorégraphiés sous la forme d’une série d’événements connexes ou orchestrés comme un ensemble de commandes connexes. Dans le chapitre 4, nous avons discuté du modèle d’agrégateur de services qui serait le fondement d’une implémentation de saga orchestrée. Nous avons également discuté d’événements avec les rubriques Azure Service Bus et Azure Event Grid qui constitueraient une base pour une implémentation orchestrée de saga.
Données volumineuses
Les applications natives cloud volumineuses prennent souvent en charge des besoins en données à volume élevé. Dans ces scénarios, les techniques de stockage de données traditionnelles peuvent entraîner des goulots d’étranglement. Pour les systèmes complexes qui se déploient à grande échelle, la séparation des responsabilités en matière de commande et de requête (CQRS) et l’approvisionnement en événements peuvent améliorer les performances des applications.
CQRS
CQRS est un modèle architectural qui peut vous aider à optimiser les performances, l’extensibilité et la sécurité. Le modèle sépare les opérations qui lisent les données des opérations qui écrivent des données.
Pour les scénarios normaux, les mêmes modèle d’entité et objet de référentiel de données sont utilisés à la fois pour les opérations de lecture et d’écriture.
Toutefois, un scénario de données à volume élevé peut tirer parti de modèles et de tables de données distincts pour les lectures et les écritures. Pour améliorer les performances, l’opération de lecture peut interroger une représentation hautement dénormalisée des données afin d’éviter les jointures de tables répétitives coûteuses et les verrous de table. L’opération d’écriture, connue sous le nom de commande, effectuerait la mise à jour par rapport à une représentation entièrement normalisée des données qui garantirait la cohérence. Vous devez ensuite implémenter un mécanisme pour conserver les deux représentations synchronisées. En règle générale, chaque fois que la table d’écriture est modifiée, elle publie un événement qui réplique la modification dans la table de lecture.
La figure 5-7 montre une implémentation du modèle CQRS.
Figure 5-7. Implémentation de CQRS
Dans la figure précédente, des modèles de commande et de requête distincts sont implémentés. Chaque opération d’écriture de données est enregistrée dans le magasin d’écriture, puis propagée au magasin de lecture. Portez une attention particulière à la façon dont le processus de propagation des données fonctionne sur le principe de la cohérence à terme. Le modèle de lecture finit par se synchroniser avec le modèle d’écriture, mais il peut y avoir un certain décalage en cours de route. Nous aborderons la cohérence à terme dans la section suivante.
Cette séparation permet aux lectures et aux écritures de se mettre à l’échelle indépendamment. Les opérations de lecture utilisent un schéma optimisé pour les requêtes, tandis que les écritures utilisent un schéma optimisé pour les mises à jour. Les requêtes de lecture sont appliquées aux données dénormalisées, tandis qu’une logique métier complexe peut être appliquée au modèle d’écriture. De plus, vous pouvez imposer une sécurité plus stricte sur les opérations d’écriture que sur celles qui exposent des lectures.
L’implémentation de CQRS peut améliorer les performances des applications pour les services cloud natifs. Toutefois, cela entraîne une conception plus complexe. Appliquez ce principe avec soin et stratégiquement aux sections de votre application native cloud qui en bénéficieront. Pour plus d’informations sur CQRS, consultez le livre Microsoft Microservices .NET : architecture pour les applications .NET conteneurisées.
Provisionnement en événements
Une autre approche de l’optimisation des scénarios de données à volume élevé implique l’approvisionnement en événements.
Un système stocke généralement l’état actuel d’une entité de données. Si un utilisateur modifie son numéro de téléphone, par exemple, l’enregistrement client est mis à jour avec le nouveau numéro. Nous connaissons toujours l’état actuel d’une entité de données, mais chaque mise à jour remplace l’état précédent.
Dans la plupart des cas, ce modèle fonctionne correctement. Toutefois, dans les systèmes à volume élevé, la surcharge liée au verrouillage transactionnel et aux opérations de mise à jour fréquentes peut avoir un impact sur les performances, la réactivité et la limitation de l’extensibilité de la base de données.
L’approvisionnement en événements adopte une approche différente pour capturer des données. Chaque opération qui affecte les données est conservée dans un magasin d’événements. Au lieu de mettre à jour l’état d’un enregistrement de données, nous ajoutons chaque modification à une liste séquentielle d’événements passés, semblable au grand livre d’un comptable. Le magasin d’événements devient le système d’enregistrement des données. Il est utilisé pour propager différentes vues matérialisées dans le contexte délimité d’un microservice. La figure 5.8 illustre ce modèle.
Figure 5-8. Approvisionnement en événements
Dans la figure précédente, notez comment chaque entrée (en bleu) pour le panier d’achat d’un utilisateur est ajoutée à un magasin d’événements sous-jacent. Dans la vue matérialisée adjacente, le système projette l’état actuel en relisant tous les événements associés à chaque panier d’achat. Cette vue (ou modèle de lecture) est ensuite exposée à l’interface utilisateur. Les événements peuvent également être intégrés à des systèmes et applications externes ou interrogés pour déterminer l’état actuel d’une entité. Avec cette approche, vous conservez l’historique. Vous connaissez non seulement l’état actuel d’une entité, mais aussi la façon dont vous avez atteint cet état.
Mécaniquement parlant, l’approvisionnement en événements simplifie le modèle d’écriture. Il n’existe aucune mise à jour ni suppression. L’ajout de chaque entrée de données en tant qu’événement immuable réduit la contention, le verrouillage et les conflits d’accès concurrentiel associés aux bases de données relationnelles. La création de modèles de lecture avec le modèle de vue matérialisée vous permet de dissocier la vue du modèle d’écriture et de choisir le meilleur magasin de données pour optimiser les besoins de l’interface utilisateur de votre application.
Pour ce modèle, envisagez un magasin de données qui prend directement en charge l’approvisionnement en événements. Azure Cosmos DB, MongoDB, Cassandra, CouchDB et RavenDB sont de bons candidats.
Comme avec tous les modèles et technologies, implémentez cette solution stratégiquement et seulement quand cela est nécessaire. Bien que l’approvisionnement en événements puisse fournir des performances et une scalabilité accrues, cela se fait au prix d’une complexité accrue et d’une certaine courbe d’apprentissage.