Bonnes pratiques pour Cloud Datastore

Les bonnes pratiques présentées ici vous offrent un aperçu rapide des points à prendre en compte lors de la création d'une application utilisant Datastore. Si vous débutez avec Datastore, cette page n'est peut-être pas le meilleur endroit pour commencer car vous n'y apprendrez pas les bases de l'utilisation de cet outil. Dans ce cas, nous vous conseillons de commencer par la section Premiers pas avec Datastore.

Général

  • Utilisez toujours des caractères au format UTF-8 pour les espaces de noms, les noms de genres, les noms de propriétés et les noms de clés personnalisées. L'utilisation de caractères non UTF-8 dans ces noms peut interférer avec le fonctionnement de Datastore. Par exemple, la présence d'un caractère non UTF-8 dans un nom de propriété peut empêcher la création d'un index utilisant cette propriété.
  • N'utilisez pas de barre oblique (/) dans les noms de genre ou les noms de clés personnalisées. La présence de barres obliques dans ces noms pourrait interférer avec les fonctionnalités futures.
  • Évitez de stocker des informations sensibles dans un ID de projet Cloud, dans la mesure où il peut être conservé au-delà de la durée de vie de votre projet.
  • Pour respecter les bonnes pratiques de conformité des données, nous vous recommandons de ne pas stocker dans les noms d'entités Datastore ou les noms de propriétés des entités.

Appels d'API

  • Utilisez des opérations par lots pour la lecture, l'écriture et la suppression au lieu d'opérations uniques. Les opérations par lots sont plus efficaces, car elles exécutent plusieurs opérations avec les mêmes coûts généraux qu'une seule opération.
  • Si une transaction échoue, essayez de l'annuler. L'annulation minimise le temps de latence avant la nouvelle tentative pour une requête différente en concurrence pour la ou les mêmes ressources dans une transaction. Sachez que l'annulation peut également échouer et qu'il s'agit d'une tentative d'optimisation uniquement.
  • Utilisez des appels asynchrones, le cas échéant, au lieu d'appels synchrones. Les appels asynchrones minimisent l'impact de la latence. Par exemple, prenons une application qui nécessite le résultat de la commande lookup() synchrone ainsi que les résultats d'une requête avant de pouvoir afficher une réponse. Si l'opération lookup() et la requête ne dépendent pas des données, il n'est pas nécessaire d'attendre de manière synchrone que lookup() se termine avant de lancer la requête.

Entités

  • Regroupez les données étroitement liées dans des groupes d'entités. Les groupes d'entités activent les requêtes ascendantes, qui renvoient des résultats fortement cohérents. Les requêtes ascendantes procèdent également à l'analyse rapide d'un groupe d'entités avec un minimum d'E/S, car les entités d'un groupe d'entités sont stockées à des emplacements physiquement proches sur des serveurs Datastore.
  • Évitez d'écrire dans un groupe d'entités plus d'une fois par seconde. Si vous écrivez à un rythme soutenu au-dessus de cette limite, les lectures cohérentes finiront par devenir plus lentes, ce qui entraînera des délais d'attente pour des lectures fortement cohérentes et ralentira les performances globales de votre application. Une écriture par lot ou une écriture transactionnelle dans un groupe d'entités compte pour une seule écriture par rapport à cette limite.
  • N'incluez pas la même entité (par clé) plusieurs fois dans le même commit. L'inclusion d'une même entité plusieurs fois dans le même commit peut avoir un impact sur la latence de Datastore.

Clés

  • Les noms de clé sont générés automatiquement s'ils ne sont pas fournis lors de la création de l'entité. Ils sont alloués de manière à être répartis uniformément dans l'espace de clés.
  • Pour une clé qui utilise un nom personnalisé, utilisez toujours des caractères au format UTF-8, à l'exception de la barre oblique (/). Les caractères non UTF-8 interfèrent avec différents processus, tels que l'importation d'une sauvegarde Datastore dans Google BigQuery. L'utilisation de barres obliques pourrait interférer avec les fonctionnalités futures.
  • Pour une clé avec un ID numérique :
    • N'utilisez pas de nombre négatif pour l'ID. Un ID négatif pourrait interférer avec le tri.
    • N'attribuez pas la valeur 0 (zéro) à l'ID. Le cas échéant, vous obtiendrez un ID alloué automatiquement.
    • Si vous souhaitez attribuer manuellement vos propres ID numériques aux entités que vous créez, demandez à votre application d'obtenir un bloc d'ID avec la méthode allocateIds(). Cette opération empêche Datastore d'attribuer l'un de vos ID numériques manuels à une autre entité.
  • Si vous attribuez votre propre ID numérique manuel ou un nom personnalisé aux entités que vous créez, n'utilisez pas de valeurs qui augmentent de façon monotone, telles que :

    1, 2, 3, ,
    "Customer1", "Customer2", "Customer3", .
    "Product 1", "Product 2", "Product 3", .
    

    Si une application génère un trafic important, cette numérotation séquentielle peut entraîner des hotspots ayant une incidence sur la latence de Datastore. Pour éviter le problème lié aux ID numériques séquentiels, procurez-vous des ID numériques à l'aide de la méthode allocateIds(). La méthode allocateIds() génère des séquences d'ID numériques réparties de manière homogène.

  • En spécifiant une clé ou en stockant le nom généré, vous pouvez effectuer plus tard une commande lookup() cohérente sur cette entité sans avoir à émettre de requête pour rechercher l'entité.

Index

  • Si une propriété est destinée à ne jamais être utilisée pour une requête, excluez-la des index. L'indexation inutile d'une propriété risque d'allonger le délai nécessaire pour assurer la cohérence et d'augmenter les coûts de stockage pour les entrées d'index.
  • Évitez d'avoir trop d'index composites. Une utilisation excessive d'index composites risque d'allonger le délai nécessaire pour assurer la cohérence et d'augmenter les coûts de stockage pour les entrées d'index. Si vous devez exécuter des requêtes ponctuelles sur des ensembles de données volumineux sans index préalablement définis, utilisez Google BigQuery.
  • N'indexez pas les propriétés avec des valeurs monotones croissantes (telles que l'horodatage NOW()). La maintenance d'un tel index peut entraîner des hotspots qui ont une incidence sur la latence de Datastore pour les applications ayant des taux de lecture et d'écriture élevés. Pour en savoir plus sur la gestion des propriétés monotones, consultez la section Taux de lecture/écriture élevés pour une plage de clés étroite ci-dessous.

Propriétés

  • Utilisez toujours des caractères au format UTF-8 pour les propriétés de type chaîne. Un caractère non UTF-8 dans une propriété de type chaîne risque d'interférer avec les requêtes. Si vous devez enregistrer des données avec des caractères non UTF-8, utilisez une chaîne d'octets.
  • N'utilisez pas de points dans les noms de propriétés. Ceux-ci interfèrent avec l'indexation des propriétés d'entités intégrées.

Requêtes

  • Si vous ne devez accéder à la clé qu'à partir des résultats de la requête, utilisez une requête de type "keys-only". Une requête ne contenant que des clés renvoie des résultats avec une latence et un coût inférieurs à ceux liés à la récupération d'entités entières.
  • Si vous ne devez accéder qu'à des propriétés spécifiques d'une entité, utilisez une requête de projection. Une requête de projection renvoie des résultats avec une latence et un coût inférieurs à ceux liés à la récupération d'entités entières.
  • De même, si vous ne devez accéder qu'aux propriétés incluses dans le filtre de requête (par exemple, celles répertoriées dans une clause order by), utilisez une requête de projection.
  • N'utilisez pas de décalages. Employez plutôt des curseurs. Un décalage permet seulement d'éviter de renvoyer les entités ignorées à l'application, mais ces entités sont toujours récupérées en interne. Les entités ignorées ont une incidence sur la latence de la requête et l'application est facturée pour les opérations de lecture nécessaires à leur récupération.
  • Si vous avez besoin d'une cohérence forte pour vos requêtes, utilisez une requête ascendante. (Pour utiliser des requêtes ascendantes, vous devez d'abord structurer les données de façon à assurer une cohérence forte). Une requête ascendante renvoie des résultats fortement cohérents. Notez qu'une requête de type keys-only non ascendante suivie d'une requête lookup() ne renvoie pas des résultats fiables, car elle est susceptible de les obtenir à partir d'un index non cohérent au moment de la requête.

Concevoir des solutions évolutives

Mises à jour d'un seul groupe d'entités

Évitez de mettre à jour trop rapidement un seul groupe d'entités dans Cloud Datastore.

Si vous utilisez Datastore, nous vous recommandons de concevoir votre application de façon à éviter de mettre à jour un groupe d'entités plus d'une fois par seconde. N'oubliez pas qu'une entité sans parent ni enfants constitue son propre groupe d'entités. Si vous mettez à jour un groupe d'entités trop rapidement, vos opérations d'écriture Datastore risquent de générer une latence accrue, des délais d'attente et d'autres types d'erreur. On peut ainsi parler de conflit.

Les taux d'écriture Datastore dans un seul groupe d'entités peuvent parfois dépasser la limite d'une seconde. Ainsi, il est possible que les tests de charge n'identifient pas ce problème.

Taux de lecture/écriture élevés pour une plage de clés étroite

Évitez les taux de lecture ou d'écriture élevés sur les clés Datastore qui sont proches d'un point de vue lexicographique.

Datastore est construit sur la base de données NoSQL de Google, Bigtable, et est soumis aux caractéristiques de performance de Bigtable. Bigtable évolue en segmentant des lignes sur des tablets séparés, les lignes étant triées par clé de façon lexicographique.

Si vous utilisez Datastore, les opérations d'écriture peuvent être ralenties en raison d'un tablet très sollicité dans le cas où vous constatez une augmentation soudaine du taux d'écriture sur un petit nombre de clés dépassant la capacité d'un serveur à tablet unique. Bigtable finira par séparer l'espace clé de manière à accepter des charges importantes.

La limite pour les opérations de lecture est généralement beaucoup plus élevée que pour les opérations d'écriture, à moins que la lecture ne s'effectue qu'à partir d'une seule clé et à un taux élevé. Bigtable ne peut pas fractionner une seule clé sur plusieurs tablets.

Les cas de tablets très sollicités peuvent s'appliquer aux plages de clés utilisées à la fois par les clés d'entité et par les index.

Dans certains cas, un hotspot Datastore peut avoir un impact plus important sur une application. En d'autres termes, le hotspot ne se limite pas à empêcher les opérations de lecture ou d'écriture sur une petite plage de clés. Par exemple, les clés très sollicitées peuvent être lues ou écrites lors du démarrage de l'instance, ce qui entraîne l'échec des requêtes de chargement.

Par défaut, Datastore attribue des clés à l'aide d'un algorithme dispersé. Ainsi, vous ne rencontrerez normalement pas de problème de hotspotting sur les opérations d'écriture Datastore si vous créez des entités avec un taux d'écriture élevé à l'aide de la règle d'allocation d'ID par défaut. Vous pouvez rencontrer ce problème dans certains cas particuliers :

  • Si vous créez des entités à un taux très élevé à l'aide de l'ancienne règle d'allocation d'ID séquentielle.

  • Si vous créez des entités à un taux très élevé et que vous attribuez vos propres ID de type monotone croissant.

  • Si vous créez des entités à un taux très élevé pour un genre qui possédait auparavant très peu d'entités. Bigtable démarrera avec toutes les entités sur le même serveur de tablet et mettra un certain temps à segmenter la plage de clés sur des serveurs de tablet distincts.

  • Vous rencontrerez également ce problème si vous créez des entités à un taux élevé avec une propriété indexée monotone croissante, telle qu'un horodatage, car ces propriétés représentent les clés des lignes de tables d'index dans Bigtable.

  • Datastore ajoute à la clé de ligne Bigtable un préfixe constitué de l'espace de noms et du genre du groupe d'entités racine. Vous pouvez rencontrer un hotspot si vous commencez à écrire dans un nouvel espace de noms ou genre sans augmenter progressivement le trafic.

Si vous avez une clé ou une propriété indexée qui va augmenter de manière monotone, vous pouvez ajouter un hachage aléatoire en tant que préfixe, pour vous assurer que les clés sont partagées sur plusieurs tablets.

De même, si vous avez besoin d'interroger une propriété monotone croissante (ou décroissante) à l'aide d'un tri ou d'un filtre, vous pouvez choisir d'indexer une nouvelle propriété, pour laquelle vous préfixez la valeur monotone avec une valeur à forte cardinalité dans l'ensemble de données, commune à toutes les entités comprises dans le champ d'application de la requête que vous souhaitez exécuter. Par exemple, si vous souhaitez interroger des entrées par horodatage, mais que vous ne devez renvoyer les résultats que pour un seul utilisateur à la fois, vous pouvez préfixer l'horodatage avec l'ID utilisateur et indexer cette nouvelle propriété. Cela permettra de continuer à autoriser les requêtes et les résultats triés pour cet utilisateur, mais la présence de l'ID utilisateur garantira que l'index est lui-même correctement segmenté.

Pour une explication plus détaillée de ce problème, consultez l'article du blog d'Ikai Lan sur la sauvegarde des valeurs monotones croissantes dans Datastore.

Augmenter le trafic

Augmentez progressivement le trafic vers de nouveaux genres Cloud Datastore ou de nouvelles parties de l'espace de clés.

Vous devez augmenter progressivement le trafic vers de nouveaux genres Datastore afin de donner à Bigtable le temps nécessaire pour segmenter les tablets à mesure que le trafic augmente. Nous recommandons d'effectuer un maximum de 500 opérations par seconde vers un nouveau genre Datastore, puis d'augmenter le trafic de 50 % toutes les 5 minutes. En théorie, vous pouvez atteindre 740 000 opérations par seconde après 90 minutes en utilisant ce programme de montée en puissance. Assurez-vous que les opérations d'écriture sont réparties de manière relativement uniforme sur la plage de clés. Nos ingénieurs SRE (Site Reliability Engineer) appellent cette méthode la règle "500/50/5".

Ce mode de montée en charge progressive est particulièrement important si vous modifiez votre code pour cesser d'utiliser le genre A afin de le remplacer par le genre B. Une manière simple de gérer cette migration consiste à modifier votre code pour qu'il lise le genre B, et s'il n'existe pas, le genre A. Toutefois, cela peut entraîner une augmentation soudaine du trafic vers un nouveau genre avec une très petite partie de l'espace de clés. Bigtable ne parviendra peut-être pas à fractionner les tablets de manière efficace si l'espace de clés est creux.

Le même problème peut également se produire si vous effectuez une migration de vos entités afin qu'elles utilisent une plage de clés différente au sein du même genre.

La stratégie à utiliser pour la migration des entités vers un nouveau genre ou une nouvelle clé dépend de votre modèle de données. Vous trouverez ci-dessous un exemple de stratégie, connue sous le nom de "Lectures parallèles". Vous devrez déterminer si cette stratégie est efficace pour vos données. L'impact financier des opérations parallèles pendant la migration sera un aspect important à prendre en compte.

Commencez par lire l'ancienne entité ou clé. Si elle est absente, vous pouvez alors lire la nouvelle entité ou clé. Un taux élevé de lectures d'entités inexistantes peut engendrer un hotspotting. Vous devez faire en sorte d'augmenter progressivement la charge. Une meilleure stratégie consiste à copier l'ancienne entité dans la nouvelle, puis à supprimer l'ancienne. Augmentez progressivement les lectures parallèles pour vous assurer que le nouvel espace clé est réparti de manière homogène.

Une stratégie possible pour l'augmentation progressive des opérations de lecture ou d'écriture vers un nouveau genre consiste à utiliser un hachage déterministe de l'ID utilisateur afin d'obtenir un pourcentage aléatoire d'utilisateurs qui écrivent de nouvelles entités. Assurez-vous que le résultat du hachage de l'ID utilisateur n'est pas faussé par votre fonction aléatoire ou par le comportement des utilisateurs.

Pendant ce temps, exécutez une tâche Dataflow afin de copier toutes les données des anciennes entités ou clés vers les nouvelles. Votre tâche par lot doit éviter les opérations d'écriture sur des clés séquentielles afin d'éviter les hotspots Bigtable. Lorsque la tâche par lot est terminée, vous ne pouvez effectuer des opérations de lecture qu'à partir du nouvel emplacement.

Vous pouvez peaufiner cette stratégie en faisant migrer simultanément des petits lots d'utilisateurs. Ajoutez à l'entité utilisateur un champ qui suit l'état de la migration de cet utilisateur. Sélectionnez un lot d'utilisateurs à migrer en fonction du hachage de l'ID utilisateur. Une tâche Mapreduce ou Dataflow effectuera la migration des clés de ce lot d'utilisateurs. Les utilisateurs avec une migration en cours utiliseront des lectures parallèles.

Veuillez prendre en compte qu'il n'est pas facile de revenir à la configuration précédente sans effectuer d'opérations d'écriture en double des anciennes et des nouvelles entités pendant la phase de migration, Ceci aurait pour effet d'augmenter les coûts de Cloud Datastore.

Suppressions

Évitez de supprimer un grand nombre d'entités Cloud Datastore sur une petite plage de clés.

Bigtable réécrit régulièrement les tables pour éliminer les entrées supprimées et pour réorganiser les données afin d'accroître l'efficacité des opérations de lecture et d'écriture. Il s'agit d'une opération de compactage.

Si vous supprimez un grand nombre d'entités Datastore sur une petite plage de clés, les requêtes sur cette partie de l'index seront plus lentes jusqu'à la fin de l'opération de compactage. Dans les cas extrêmes, vos requêtes peuvent expirer avant de renvoyer les résultats.

Un antipatron consiste à utiliser une valeur d'horodatage pour un champ indexé afin de représenter le délai d'expiration d'une entité. Afin de récupérer les entités arrivées à expiration, vous devez interroger ce champ indexé, qui se trouve probablement dans une partie chevauchant l'espace de clés avec des entrées d'index pour les dernières entités supprimées.

Vous pouvez améliorer les performances à l'aide de "requêtes segmentées", qui ajoutent, en tant que préfixe, une chaîne de longueur fixe à l'horodatage d'expiration. L'index est trié sur la chaîne complète afin que les entités avec le même horodatage soient situées à travers la plage de clés de l'index. Exécutez plusieurs requêtes en parallèle pour récupérer les résultats de chaque segment.

Une solution plus complète liée à l'horodatage d'expiration consiste à utiliser un "numéro de génération", qui est un compteur global mis à jour régulièrement. Le numéro de génération est ajouté en tant que préfixe à l'horodatage d'expiration afin que les requêtes soient triées par numéro de génération, puis par fragment et enfin par horodatage. La suppression d'anciennes entités survient lors d'une génération précédente. Le numéro de génération de chaque entité non supprimée doit être incrémenté. Une fois la suppression terminée, passez à la génération suivante. Les requêtes sur une génération plus ancienne donneront des résultats médiocres jusqu'à la fin de l'opération de compactage. Vous devrez peut-être attendre plusieurs générations avant d'interroger l'index pour obtenir la liste des entités à supprimer, afin de réduire le risque de résultats manquants dus à la cohérence à terme.

Partition et réplication

Utilisez la partition ou la réplication pour les clés Cloud Datastore très sollicitées.

Vous pouvez utiliser la réplication pour lire une partie de la plage de clés à un taux supérieur à celui autorisé par Bigtable. À l'aide de cette stratégie, vous pouvez stocker N copies de la même entité, permettant un taux de lectures N fois supérieur à celui accepté par une seule entité.

Vous pouvez utiliser la partition pour écrire sur une partie de la plage de clés à un taux supérieur à celui autorisé par Bigtable. La partition divise une entité en parties plus petites.

Voici quelques erreurs courantes liées à la partition :

  • Segmenter à l'aide d'un préfixe de temps. Lorsque le temps passe au préfixe suivant, la nouvelle partie non segmentée devient un hotspot. Vous devriez plutôt faire passer une partie de vos écritures vers le nouveau préfixe de façon progressive.

  • Ne segmenter que les entités les plus sollicitées. Si vous segmentez une petite partie du nombre total d'entités, il se peut que le nombre de lignes entre les entités très sollicitées ne soit pas suffisant pour garantir leur maintien dans des répartitions différentes.

Étape suivante