Bonnes pratiques

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 Firestore en mode Datastore. Si vous débutez avec le mode Datastore, cette page ne constitue peut-être pas le meilleur point de départ, car elle ne vous apprend pas les bases de l'utilisation du mode Datastore. Pour vous familiariser avec le mode Datastore, nous vous suggérons de commencer par la section Premiers pas avec Firestore en mode 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 du mode 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. Un ID de projet Cloud peut être conservé au-delà de la durée de vie de votre projet.

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.

Entities

  • 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.

  • Consultez la section sur les mises à jour d'une entité.

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é avec 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 divers processus, tels que l'importation d'un fichier d'exportation en mode Datastore dans 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 cet identifiant. Un identifiant négatif pourrait interférer avec le tri.
    • N'utilisez pas la valeur 0(zéro) pour l'ID. Si vous le faites, 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 le mode 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 du mode 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 exécuter plus tard une commande lookup() 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'accroître la latence 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 peut entraîner une augmentation de la latence en écriture et des 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 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 ayant une incidence sur la latence du mode Datastore pour les applications avec 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 cursors. 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.

Concevoir des solutions évolutives

Les bonnes pratiques suivantes décrivent comment éviter les situations qui créent des conflits.

Mises à jour d'une entité

Lorsque vous concevez votre application, tenez compte de la rapidité avec laquelle elle met à jour des entités uniques. Le meilleur moyen de caractériser les performances de votre charge de travail consiste à effectuer des tests de charge. Le débit maximal exact qu'une application peut mettre à jour pour une seule entité dépend fortement de la charge de travail. Les facteurs incluent le taux d'écriture, les conflits entre les requêtes et le nombre d'index concernés.

Une opération d'écriture d'entité met à jour l'entité et tous les index associés, et Firestore en mode Datastore applique l'opération d'écriture de manière synchrone sur un quorum d'instances répliquées. À des taux d'écriture suffisamment élevés, la base de données commence à rencontrer des conflits, une latence plus élevée ou d'autres erreurs.

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 documents qui sont proches d'un point de vue lexicographique, ou votre application rencontrera des erreurs liées à un conflit. Ce problème, appelé "hotspotting", peut se produire si votre application effectue l'une des opérations suivantes :

  • Elle crée des entités à un taux très élevé et attribue ses propres ID monotones croissants.

    Le mode Datastore attribue les clés à l'aide d'un algorithme dispersé. Vous ne devriez pas rencontrer de problème de hotspotting sur les opérations d'écriture si vous créez des entités à l'aide de la règle d'allocation d'ID d'entité par défaut.

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

  • Elle crée des entités à un taux élevé pour un genre qui possède très peu d'entités.

  • Elle crée des entités avec une valeur de propriété indexée augmentant de manière monotone (telle qu'un horodatage), à un taux très élevé.

  • Elle supprime les entités d'un genre à un taux élevé.

  • Elle effectue des opérations d'écriture dans la base de données à un taux très élevé sans augmenter progressivement le trafic.

Si vous constatez une augmentation soudaine du taux d'écriture sur une petite plage de clés, les écritures peuvent être lentes en raison d'un point d'accès. Le mode Datastore finira par diviser 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é.

Les hotspots 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 peut avoir un impact plus important sur une application. En d'autres termes, il 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.

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é.

Augmenter le trafic

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

Vous devez augmenter progressivement le trafic vers de nouveaux genres afin de donner à Firestore en mode Datastore le temps nécessaire pour se préparer à l'augmentation du trafic. Nous recommandons d'effectuer un maximum de 500 opérations par seconde vers un nouveau genre, 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.

Le même problème peut également se produire si vous migrez 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'empêcher les hotspots. 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, auquel cas, les coûts relatifs au mode Datastore augmenteraient.

Suppressions

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

Firestore en mode Datastore 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 lectures et des écritures. Il s'agit d'une opération de compactage.

Si vous supprimez un grand nombre d'entités du mode 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 gérer les hotspots.

Vous pouvez utiliser la réplication si vous avez besoin de lire une partie de la plage de clés à un taux supérieur à celui autorisé par Firestore en mode Datastore. À 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 segmentation si vous avez besoin d'écrire une partie de la plage de clés à un taux supérieur à celui autorisé par Firestore en mode Datastore. 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.

Étapes suivantes