Partager via


Comment modéliser et partitionner des données à l’aide d’un exemple réel

Cet article s’appuie sur plusieurs concepts d’Azure Cosmos DB tels que la modélisation des données, le partitionnement et le débit provisionné pour montrer comment aborder un exercice de conception de données réel.

Si vous travaillez généralement avec des bases de données relationnelles, vous avez probablement développé des habitudes pour concevoir des modèles de données. En raison des contraintes spécifiques, mais aussi des forces uniques d’Azure Cosmos DB, la plupart de ces meilleures pratiques ne se traduisent pas correctement et peuvent vous faire glisser dans des solutions non optimales. L’objectif de cet article est de vous guider tout au long du processus complet de modélisation d’un cas d’usage réel sur Azure Cosmos DB, de la modélisation d’éléments à la colocation d’entités et au partitionnement de conteneurs.

Pour obtenir un exemple illustrant les concepts de cet article, téléchargez ou affichez ce code source généré par la communauté.

Important

Un contributeur de la communauté a contribué à cet exemple de code. L’équipe Azure Cosmos DB ne prend pas en charge sa maintenance.

Scénario

Pour cet exercice, nous allons envisager le domaine d’une plateforme de blogs où les utilisateurs peuvent créer des articles. Les utilisateurs peuvent également aimer et ajouter des commentaires à ces publications.

Conseil / Astuce

Certains mots sont mis en évidence en italique pour identifier le genre de « choses » que notre modèle manipule.

Ajout d’autres exigences à notre spécification :

  • Une page d’accueil affiche un flux de publications récemment créées.
  • Nous pouvons récupérer tous les billets d’un utilisateur, tous les commentaires pour un billet et tous les likes pour un billet.
  • Les publications sont retournées avec le nom d’utilisateur de leurs auteurs et le nombre de commentaires et de mentions 'J'aime' qu’ils ont.
  • Les commentaires et les mentions ‘j’aime’ sont également retournés avec le nom d’utilisateur des utilisateurs qui les ont créés.
  • Lorsqu’ils sont affichés sous forme de listes, les publications doivent uniquement présenter un résumé tronqué de leur contenu.

Identifier les principaux modèles d’accès

Pour commencer, nous offrons une structure à notre spécification initiale en identifiant les modèles d’accès de notre solution. Lors de la conception d’un modèle de données pour Azure Cosmos DB, il est important de comprendre quelles demandes notre modèle doit servir à s’assurer que le modèle répond efficacement à ces demandes.

Pour faciliter le suivi du processus global, nous catégorisons ces différentes requêtes en tant que commandes ou requêtes, en empruntant un vocabulaire à partir de la séparation des responsabilités des requêtes de commande (CQRS). Dans CQRS, les commandes sont des demandes d’écriture (c’est-à-dire des intentions de mise à jour du système) et les requêtes sont des requêtes en lecture seule.

Voici la liste des demandes exposées par notre plateforme :

  • [C1] Créer ou modifier un utilisateur
  • [Q1] Récupérer un utilisateur
  • [C2] Créer ou modifier un billet
  • [Q2] Récupérer une publication
  • [Q3] Répertorier les billets d’un utilisateur sous forme abrégée
  • [C3] Créer un commentaire
  • [Q4] Répertorier les commentaires d’un billet
  • [C4] Aimer un post
  • [Q5] Répertorier les likes d’un billet
  • [Q6] Répertorier les publications x les plus récentes créées sous forme abrégée (flux)

À ce stade, nous n’avons pas pensé aux détails de ce que contient chaque entité (utilisateur, publication, etc.). Cette étape est généralement parmi les premières étapes à aborder lors de la conception d'un magasin relationnel. Nous commençons d’abord par cette étape, car nous devons déterminer comment ces entités se traduisent en termes de tables, de colonnes, de clés étrangères, et ainsi de suite. Il s’agit beaucoup moins d’une préoccupation avec une base de données de documents qui n’applique aucun schéma lors de l'écriture.

Il est important d’identifier nos modèles d’accès à partir du début, car cette liste de demandes sera notre suite de tests. Chaque fois que nous itérons sur notre modèle de données, nous parcourons chacune des requêtes et vérifions leurs performances et leur scalabilité. Nous calculons les unités de requête consommées dans chaque modèle et les optimiseons. Tous ces modèles utilisent la stratégie d’indexation par défaut et vous pouvez la remplacer en indexant des propriétés spécifiques, ce qui peut améliorer davantage la consommation et la latence des RU.

V1 : Une première version

Nous commençons par deux conteneurs : users et posts.

Conteneur d’utilisateurs

Ce conteneur stocke uniquement les éléments utilisateur :

{
    "id": "<user-id>",
    "username": "<username>"
}

Nous partitionnez ce conteneur par id, ce qui signifie que chaque partition logique au sein de ce conteneur ne contient qu’un seul élément.

Boîte de messages

Ce conteneur héberge des entités telles que des publications, des commentaires et des likes :

{
    "id": "<post-id>",
    "type": "post",
    "postId": "<post-id>",
    "userId": "<post-author-id>",
    "title": "<post-title>",
    "content": "<post-content>",
    "creationDate": "<post-creation-date>"
}

{
    "id": "<comment-id>",
    "type": "comment",
    "postId": "<post-id>",
    "userId": "<comment-author-id>",
    "content": "<comment-content>",
    "creationDate": "<comment-creation-date>"
}

{
    "id": "<like-id>",
    "type": "like",
    "postId": "<post-id>",
    "userId": "<liker-id>",
    "creationDate": "<like-creation-date>"
}

Nous partitionnons ce conteneur par postId, ce qui signifie que chaque partition logique au sein de ce conteneur contient un billet, tous les commentaires pour ce billet et tous les likes pour ce billet.

Nous avons introduit une type propriété dans les éléments stockés dans ce conteneur pour distinguer les trois types d’entités que ce conteneur héberge.

En outre, nous avons choisi de référencer des données associées au lieu de l’incorporer, car :

  • Il n’existe aucune limite supérieure au nombre de publications qu’un utilisateur peut créer.
  • Les postes peuvent être arbitrairement longs.
  • Il n’y a pas de limite supérieure au nombre de commentaires et de likes qu’une publication peut avoir.
  • Nous voulons pouvoir ajouter un commentaire ou un j'aime à une publication sans avoir à mettre à jour la publication elle-même.

Pour en savoir plus sur ces concepts, consultez La modélisation des données dans Azure Cosmos DB.

Quelle est la performance de notre modèle ?

Il est maintenant temps d’évaluer les performances et la scalabilité de notre première version. Pour chacune des demandes précédemment identifiées, nous mesurons sa latence et le nombre d’unités de requête qu’il consomme. Cette mesure est effectuée par rapport à un jeu de données factice contenant 100 000 utilisateurs avec 5 à 50 publications par utilisateur, et jusqu’à 25 commentaires et 100 likes par publication.

[C1] Créer ou modifier un utilisateur

Cette demande est simple à implémenter, car nous allons simplement créer ou mettre à jour un élément dans le users conteneur. Les requêtes sont bien réparties sur toutes les partitions grâce à la clé de id partition.

Diagramme de l’écriture d’un élément unique dans le conteneur des utilisateurs.

Latency Unités de requête Niveau de performance
7 ms 5.71 RU

[Q1] Récupérer un utilisateur

La récupération d’un utilisateur est effectuée en lisant l’élément correspondant à partir du conteneur users.

Diagramme de récupération d’un élément unique à partir du conteneur utilisateurs.

Latency Unités de requête Niveau de performance
2 ms 1 RU

[C2] Créer ou modifier un billet

De la même façon que [C1], nous devons simplement écrire dans le posts conteneur.

Diagramme de l’écriture d’un seul élément de publication dans le conteneur de publications.

Latency Unités de requête Niveau de performance
9 ms 8.76 RU

[Q2] Récupérer une publication

Nous commençons par récupérer le document correspondant à partir du posts conteneur. Mais ce n’est pas suffisant, conformément à notre spécification, nous devons également agréger le nom d’utilisateur de l’auteur du billet, le nombre de commentaires et les nombres de likes pour le billet. Les agrégations répertoriées nécessitent trois requêtes SQL supplémentaires à émettre.

Diagramme de la récupération d’une publication et de l’agrégation des données supplémentaires.

Chacune des requêtes filtre sur la clé de partition de son conteneur respectif, ce qui est exactement ce que nous souhaitons pour optimiser les performances et l’évolutivité. Mais nous devons finalement effectuer quatre opérations pour retourner une publication unique, donc nous améliorerons cela dans une prochaine itération.

Latency Unités de requête Niveau de performance
9 ms 19.54 RU

[Q3] Répertorier les billets d’un utilisateur sous forme abrégée

Tout d’abord, nous devons récupérer les publications souhaitées avec une requête SQL qui récupère les publications correspondant à cet utilisateur particulier. Mais nous devons également émettre davantage de requêtes pour agréger le nom d’utilisateur de l’auteur et le nombre de commentaires et de likes.

Diagramme de la récupération de toutes les publications d’un utilisateur et de l’agrégation de ses données supplémentaires.

Cette implémentation présente de nombreux inconvénients :

  • Les requêtes qui additionnent le nombre de commentaires et de mentions "J'aime" sont émises pour chaque publication retournée par la première requête.
  • La requête principale n’effectue pas de filtrage sur la clé de partition du conteneur posts, ce qui conduit à une distribution ramifiée et à une analyse de partition sur le conteneur.
Latency Unités de requête Niveau de performance
130 ms 619.41 RU

[C3] Créer un commentaire

Un commentaire est créé en écrivant l’élément correspondant dans le posts conteneur.

Diagramme de l’écriture d’un seul élément de commentaire dans le conteneur de publications.

Latency Unités de requête Niveau de performance
7 ms 8.57 RU

[Q4] Répertorier les commentaires d’un billet

Nous commençons par une requête qui extrait tous les commentaires de ce billet et une fois de plus, nous devons également agréger des noms d’utilisateur séparément pour chaque commentaire.

Diagramme de la récupération de tous les commentaires d’un billet et de l’agrégation de leurs données supplémentaires.

Bien que la requête principale filtre sur la clé de partition du conteneur, le fait d'agréger séparément les noms d'utilisateur pénalise les performances globales. Nous l’avons amélioré plus tard.

Latency Unités de requête Niveau de performance
23 ms 27.72 RU

[C4] Ajouter une mention « j’aime » à une publication

Tout comme [C3], nous créons l’élément correspondant dans le posts conteneur.

Diagramme de l’écriture d’un seul élément de publication (« J’aime ») dans le conteneur de publications.

Latency Unités de requête Niveau de performance
6 ms 7.05 RU

[Q5] Répertorier les likes d’un billet

Tout comme [Q4], nous interrogeons les likes pour ce billet, puis agrégeons leurs noms d’utilisateur.

Diagramme de la récupération de toutes les likes d’un billet et de l’agrégation de leurs données supplémentaires.

Latency Unités de requête Niveau de performance
59 ms 58.92 RU

[Q6] Lister les x publications les plus récentes créées sous forme abrégée (flux)

Nous récupérons les publications les plus récentes en interrogeant le posts conteneur trié par date de création décroissante, puis agrégeons les noms d’utilisateur et les nombres de commentaires et de likes pour chacune des publications.

Diagramme de la récupération des publications les plus récentes et de l’agrégation de leurs données supplémentaires.

Une fois encore, notre requête initiale n’effectue pas de filtrage sur la clé de partition du conteneur posts, ce qui déclenche une distribution ramifiée coûteuse. La situation est encore pire ici, car nous ciblons un jeu de résultats plus grand et trions les résultats avec une clause ORDER BY, ce qui rend le processus plus onéreux en termes d’unités de requête.

Latency Unités de requête Niveau de performance
306 ms 2063.54 RU

Réfléchir aux performances de V1

En examinant les problèmes de performances rencontrés dans la section précédente, nous pouvons identifier deux classes principales de problèmes :

  • Certaines demandes nécessitent l’émission de plusieurs requêtes afin de collecter toutes les données dont nous avons besoin pour fournir une réponse.
  • Certaines requêtes ne filtrent pas les données sur la clé de partition des conteneurs qu’elles ciblent, ce qui conduit à une distribution ramifiée qui nuit à la scalabilité.

Nous allons résoudre chacun de ces problèmes, en commençant par le premier.

V2 : Introduire la dénormalisation pour optimiser les requêtes de lecture

La raison pour laquelle nous devons émettre davantage de demandes dans certains cas est que les résultats de la requête initiale ne contiennent pas toutes les données dont nous avons besoin pour retourner. La dénormalisation des données résout ce type de problème dans notre jeu de données lors de l’utilisation d’un magasin de données non relationnelle comme Azure Cosmos DB.

Dans notre exemple, nous modifions les éléments de publication pour ajouter le nom d’utilisateur de l’auteur du billet, le nombre de commentaires et le nombre de likes.

{
    "id": "<post-id>",
    "type": "post",
    "postId": "<post-id>",
    "userId": "<post-author-id>",
    "userUsername": "<post-author-username>",
    "title": "<post-title>",
    "content": "<post-content>",
    "commentCount": <count-of-comments>,
    "likeCount": <count-of-likes>,
    "creationDate": "<post-creation-date>"
}

Nous modifions également les commentaires et les éléments similaires pour ajouter le nom d’utilisateur de l’utilisateur qui les a créés :

{
    "id": "<comment-id>",
    "type": "comment",
    "postId": "<post-id>",
    "userId": "<comment-author-id>",
    "userUsername": "<comment-author-username>",
    "content": "<comment-content>",
    "creationDate": "<comment-creation-date>"
}

{
    "id": "<like-id>",
    "type": "like",
    "postId": "<post-id>",
    "userId": "<liker-id>",
    "userUsername": "<liker-username>",
    "creationDate": "<like-creation-date>"
}

Dénormaliser les nombres de commentaires et de mentions « j’aime »

Ce que nous voulons obtenir est que chaque fois que nous ajoutons un commentaire ou un tel, nous incrémentons également le commentCount ou le likeCount dans le billet correspondant. Comme postId partitionne notre conteneur posts, le nouvel élément (commentaire ou mention « J’aime ») et la publication correspondante figurent dans la même partition logique. Par conséquent, nous pouvons utiliser une procédure stockée pour effectuer cette opération.

Lorsque vous créez un commentaire ([C3]), au lieu d’ajouter un nouvel élément dans le posts conteneur, nous appelons la procédure stockée suivante sur ce conteneur :

function createComment(postId, comment) {
  var collection = getContext().getCollection();

  collection.readDocument(
    `${collection.getAltLink()}/docs/${postId}`,
    function (err, post) {
      if (err) throw err;

      post.commentCount++;
      collection.replaceDocument(
        post._self,
        post,
        function (err) {
          if (err) throw err;

          comment.postId = postId;
          collection.createDocument(
            collection.getSelfLink(),
            comment
          );
        }
      );
    })
}

Cette procédure stockée prend l’ID du billet et le corps du nouveau commentaire en tant que paramètres, puis :

  • récupère la publication.
  • incrémente le commentCount.
  • remplace la publication.
  • ajoute le nouveau commentaire.

À mesure que les procédures stockées sont exécutées en tant que transactions atomiques, la valeur de commentCount et le nombre réel de commentaires restent toujours synchronisés.

Bien entendu, nous appelons une procédure stockée similaire lors de l’ajout de nouvelles mentions « j’aime » pour incrémenter likeCount.

Dénormaliser les noms d’utilisateur

Les noms d’utilisateur nécessitent une approche différente, car les utilisateurs se trouvent non seulement dans différentes partitions, mais dans un autre conteneur. Lorsque nous devons dénormaliser des données entre des partitions et des conteneurs, nous pouvons utiliser le flux de modification du conteneur source.

Dans notre exemple, nous utilisons le flux de modification du users conteneur pour réagir chaque fois que les utilisateurs mettent à jour leurs noms d’utilisateur. Lorsque cela se produit, nous propageons la modification en appelant une autre procédure stockée sur le posts conteneur :

Diagramme de la dénormalisation des noms d’utilisateur dans le conteneur de publications.

function updateUsernames(userId, username) {
  var collection = getContext().getCollection();
  
  collection.queryDocuments(
    collection.getSelfLink(),
    `SELECT * FROM p WHERE p.userId = '${userId}'`,
    function (err, results) {
      if (err) throw err;

      for (var i in results) {
        var doc = results[i];
        doc.userUsername = username;

        collection.upsertDocument(
          collection.getSelfLink(),
          doc);
      }
    });
}

Cette procédure stockée prend l’ID de l’utilisateur et le nouveau nom d’utilisateur de l’utilisateur comme paramètres, puis :

  • récupère tous les éléments correspondant à l’élément userId (qui peut être des publications, des commentaires ou des likes).
  • pour chacun de ces éléments :
    • remplace le userUsername.
    • remplace l’élément.

Important

Cette opération est coûteuse, car elle nécessite que cette procédure stockée soit exécutée sur chaque partition du posts conteneur. Nous partons du principe que la plupart des utilisateurs choisissent un nom d’utilisateur approprié lors de l’inscription et ne le modifieront jamais, de sorte que cette mise à jour s’exécute très rarement.

Quels sont les gains de performances de V2 ?

Parlons de certains des gains de performances de V2.

[Q2] Récupérer une publication

Maintenant que notre dénormalisation est en place, nous n’avons qu’à récupérer un seul élément pour gérer cette demande.

Diagramme de la récupération d’un élément unique à partir du conteneur de publications dénormalisées.

Latency Unités de requête Niveau de performance
2 ms 1 RU

[Q4] Répertorier les commentaires d’un billet

Ici encore, nous pouvons réduire les demandes supplémentaires qui ont récolté les noms d'utilisateur et obtenons une requête unique qui effectue un filtrage sur la clé de partition.

Diagramme de la récupération de tous les commentaires pour un billet dénormalisé.

Latency Unités de requête Niveau de performance
4 ms 7.72 RU

[Q5] Répertorier les likes d’un billet

La situation est exactement la même lors de l’énumération des mentions « j’aime ».

Diagramme de la récupération de toutes les likes pour un billet dénormalisé.

Latency Unités de requête Niveau de performance
4 ms 8.92 RU

V3 : Vérifiez que toutes les requêtes sont évolutives

Il existe toujours deux demandes que nous n’avons pas entièrement optimisées lors de l’analyse de nos améliorations globales des performances. Ces requêtes sont [Q3] et [Q6].. Ce sont les demandes impliquant des requêtes qui ne filtrent pas sur la clé de partition des conteneurs ciblés.

[Q3] Répertorier les billets d’un utilisateur sous forme abrégée

Cette demande bénéficie déjà des améliorations introduites dans V2, qui épargne davantage de requêtes.

Diagramme montrant la requête permettant de répertorier les billets dénormalisés d’un utilisateur sous forme abrégée.

Mais la requête restante n’est toujours pas filtrée sur la clé de partition du posts conteneur.

La façon de réfléchir à cette situation est simple :

  • Cette demande doit effectuer un filtrage sur userId, car nous voulons extraire toutes les publications d’un utilisateur particulier.
  • Elle n'a pas de bonnes performances parce qu'elle est exécutée sur le posts conteneur, qui n'a pas de partitionnement userId.
  • En indiquant l’évidence, nous allons résoudre notre problème de performances en exécutant cette requête sur un conteneur partitionné avec userId.
  • Il s’avère que nous avons déjà un tel conteneur : le users conteneur !

Nous introduisons donc un deuxième niveau de dénormalisation en dupliquant des publications entières dans le conteneur users. En procédant ainsi, nous obtenons effectivement une copie de nos publications, désormais partitionnées selon une dimension différente qui améliore considérablement l’efficacité de leur récupération par leur userId.

Le users conteneur contient maintenant deux types d’éléments :

{
    "id": "<user-id>",
    "type": "user",
    "userId": "<user-id>",
    "username": "<username>"
}

{
    "id": "<post-id>",
    "type": "post",
    "postId": "<post-id>",
    "userId": "<post-author-id>",
    "userUsername": "<post-author-username>",
    "title": "<post-title>",
    "content": "<post-content>",
    "commentCount": <count-of-comments>,
    "likeCount": <count-of-likes>,
    "creationDate": "<post-creation-date>"
}

Dans cet exemple :

  • Nous avons introduit un type champ dans l’élément utilisateur pour distinguer les utilisateurs des publications.
  • Nous avons également ajouté un userId champ dans l’élément utilisateur, qui est redondant avec le id champ, mais est requis, car le users conteneur est maintenant partitionné avec userId (et non comme id précédemment).

Pour atteindre cette dénormalisation, nous utilisons à nouveau le flux de modification. Cette fois, nous réagissons sur le flux de modification du posts conteneur pour distribuer tout billet nouveau ou mis à jour vers le users conteneur. Et comme la fourniture d’une liste des publications ne nécessite pas de retourner leur contenu complet, nous pouvons les tronquer dans ce processus.

Diagramme de la dénormalisation des publications dans le conteneur des utilisateurs.

Nous pouvons maintenant acheminer notre requête vers le users conteneur, en filtrant sur la clé de partition du conteneur.

Diagramme de la récupération de toutes les publications pour un utilisateur dénormalisé.

Latency Unités de requête Niveau de performance
4 ms 6.46 RU

[Q6] Lister les x publications les plus récentes créées sous forme abrégée (flux)

Nous devons nous occuper d'une situation similaire ici : même après avoir éliminé les requêtes devenues inutiles par la dénormalisation introduite dans V2, la requête restante ne filtre pas sur la clé de partition du conteneur.

Diagramme montrant la requête permettant de répertorier les publications x les plus récentes créées sous forme abrégée.

En suivant la même approche, l’optimisation des performances et de l’extensibilité de cette requête nécessite qu’elle n’atteigne qu’une seule partition. Il est concevable d’atteindre une seule partition, car nous n’avons qu’à retourner un nombre limité d’éléments. Pour remplir la page d’accueil de notre plateforme de blogs, nous devons simplement obtenir les 100 publications les plus récentes, sans avoir à paginer l’ensemble du jeu de données.

Ainsi, pour optimiser cette dernière demande, nous introduisons un troisième conteneur à notre conception, entièrement dédié à la prestation de cette demande. Nous dénormalisons nos publications dans ce nouveau feed conteneur :

{
    "id": "<post-id>",
    "type": "post",
    "postId": "<post-id>",
    "userId": "<post-author-id>",
    "userUsername": "<post-author-username>",
    "title": "<post-title>",
    "content": "<post-content>",
    "commentCount": <count-of-comments>,
    "likeCount": <count-of-likes>,
    "creationDate": "<post-creation-date>"
}

Le champ type partitionne ce conteneur, qui est toujours post dans nos éléments. Cela garantit que tous les éléments de ce conteneur se trouvent dans la même partition.

Pour réaliser cette dénormalisation, il nous suffit de raccorder le pipeline de flux de modification que nous avons précédemment introduit pour distribuer les publications vers ce nouveau conteneur. Une chose importante à garder à l’esprit est que nous devons nous assurer que nous ne stockons que les 100 postes les plus récents ; sinon, le contenu du conteneur peut augmenter au-delà de la taille maximale d’une partition. Cette limitation peut être implémentée en appelant un post-déclencheur chaque fois qu’un document est ajouté dans le conteneur :

Diagramme de dénormalisation des publications dans le conteneur de flux.

Voici le corps du post-déclencheur qui tronque la collection :

function truncateFeed() {
  const maxDocs = 100;
  var context = getContext();
  var collection = context.getCollection();

  collection.queryDocuments(
    collection.getSelfLink(),
    "SELECT VALUE COUNT(1) FROM f",
    function (err, results) {
      if (err) throw err;

      processCountResults(results);
    });

  function processCountResults(results) {
    // + 1 because the query didn't count the newly inserted doc
    if ((results[0] + 1) > maxDocs) {
      var docsToRemove = results[0] + 1 - maxDocs;
      collection.queryDocuments(
        collection.getSelfLink(),
        `SELECT TOP ${docsToRemove} * FROM f ORDER BY f.creationDate`,
        function (err, results) {
          if (err) throw err;

          processDocsToRemove(results, 0);
        });
    }
  }

  function processDocsToRemove(results, index) {
    var doc = results[index];
    if (doc) {
      collection.deleteDocument(
        doc._self,
        function (err) {
          if (err) throw err;

          processDocsToRemove(results, index + 1);
        });
    }
  }
}

La dernière étape consiste à rediriger notre requête vers notre nouveau feed conteneur :

Diagramme de la récupération des publications les plus récentes.

Latency Unités de requête Niveau de performance
9 ms 16.97 RU

Conclusion

Examinons les améliorations globales des performances et de la scalabilité que nous avons introduites sur les différentes versions de notre conception.

V1 V2 V3
[C1] 7 ms / 5.71 RU 7 ms / 5.71 RU 7 ms / 5.71 RU
[Q1] 2 ms / 1 RU 2 ms / 1 RU 2 ms / 1 RU
[C2] 9 ms / 8.76 RU 9 ms / 8.76 RU 9 ms / 8.76 RU
[Q2] 9 ms / 19.54 RU 2 ms / 1 RU 2 ms / 1 RU
[Q3] 130 ms / 619.41 RU 28 ms / 201.54 RU 4 ms / 6.46 RU
[C3] 7 ms / 8.57 RU 7 ms / 15.27 RU 7 ms / 15.27 RU
[Q4] 23 ms / 27.72 RU 4 ms / 7.72 RU 4 ms / 7.72 RU
[C4] 6 ms / 7.05 RU 7 ms / 14.67 RU 7 ms / 14.67 RU
[Q5] 59 ms / 58.92 RU 4 ms / 8.92 RU 4 ms / 8.92 RU
[Q6] 306 ms / 2063.54 RU 83 ms / 532.33 RU 9 ms / 16.97 RU

Nous avons optimisé un scénario à forte charge de lecture.

Vous remarquerez peut-être que nous avons concentré nos efforts pour améliorer les performances des demandes de lecture (requêtes) au détriment des demandes d’écriture (commandes). Dans de nombreux cas, les opérations d’écriture déclenchent désormais la dénormalisation ultérieure par le biais de flux de modification, ce qui les rend plus coûteuses et plus longues à matérialiser.

Nous justifions cette attention sur les performances en lecture par le fait qu’une plateforme de blogs, comme la plupart des applications sociales, est consacrée principalement à la lecture. Une charge de travail intensive en lecture indique que la quantité de demandes de lecture qu’il doit traiter est généralement supérieure au nombre de demandes d’écriture. Il est donc judicieux de rendre les demandes d’écriture plus coûteuses à exécuter afin de laisser les demandes de lecture être moins coûteuses et plus performantes.

Si nous examinons l’optimisation la plus extrême que nous avons effectuée, [Q6] est passé de plus de 2000 RU à seulement 17 RU ; nous l'avons réalisé en dénormalisant les messages à raison d’environ 10 RU par article. Comme nous servirions beaucoup plus de demandes de flux que pour la création ou la mise à jour des publications, le coût de cette dénormalisation est négligeable compte tenu des économies globales réalisées.

La dénormalisation peut être appliquée de manière incrémentielle

Les améliorations de scalabilité que nous avons explorées dans cet article impliquent la dénormalisation et la duplication des données dans le jeu de données. Il convient de noter que ces optimisations n’ont pas besoin d’être mises en place le jour 1. Les requêtes qui filtrent sur les clés de partition fonctionnent mieux à grande échelle, mais les requêtes entre partitions peuvent être acceptables si elles sont appelées rarement ou par rapport à un jeu de données limité. Si vous créez simplement un prototype ou que vous lancez un produit avec une petite base d’utilisateurs contrôlée, vous pouvez probablement économiser ces améliorations ultérieurement. Ce qui est important, c’est de surveiller les performances de votre modèle afin que vous puissiez décider si et quand il est temps de les amener.

Le flux de modification que nous utilisons pour distribuer des mises à jour à d’autres conteneurs stocke toutes ces mises à jour de manière permanente. Cette persistance permet de demander toutes les mises à jour depuis la création du conteneur et des vues dénormalisées de démarrage en tant qu’opération de rattrapage unique, même si votre système a déjà de nombreuses données.

Étapes suivantes

Après cette introduction à la modélisation et au partitionnement pratiques des données, vous pouvez consulter les articles suivants pour passer en revue les concepts suivants :