Comprendre les lectures et les écritures à grande échelle

Lisez ce document pour prendre des décisions éclairées sur l'architecture de vos applications afin d'obtenir des performances et une fiabilité élevées. Ce document aborde des sujets avancés concernant Firestore. Si vous débutez avec Firestore, consultez plutôt le guide de démarrage rapide.

Firestore est une base de données flexible et évolutive conçue pour le développement d'appareils mobiles, de serveurs Web et de serveurs à partir de Firebase et de Google Cloud. Il est très facile de se lancer avec Firestore et d'écrire des applications riches et puissantes.

Pour vous assurer que vos applications continuent de fonctionner correctement lorsque la taille de votre base de données et l'augmentation du trafic augmentent, il est utile de comprendre le fonctionnement des lectures et des écritures dans le backend Firestore. Vous devez également comprendre l'interaction de vos lectures et écritures avec la couche de stockage et les contraintes sous-jacentes pouvant affecter les performances.

Consultez les sections suivantes pour connaître les bonnes pratiques avant de concevoir votre application.

Comprendre les composants de haut niveau

Le diagramme suivant présente les composants de haut niveau impliqués dans une requête API Firestore.

Composants de haut niveau

SDK Firestore et bibliothèques clientes

Firestore est compatible avec les SDK et les bibliothèques clientes pour différentes plates-formes. Bien qu'une application puisse effectuer des appels HTTP et RPC directs à l'API Firestore, les bibliothèques clientes fournissent une couche d'abstraction pour simplifier l'utilisation de l'API et implémenter les bonnes pratiques. Ils peuvent également fournir des fonctionnalités supplémentaires telles que l'accès hors connexion, les caches, etc.

Google Front End (GFE)

Il s'agit d'un service d'infrastructure commun à tous les services Google Cloud. Le GFE accepte les requêtes entrantes et les transmet au service Google approprié (service Firestore dans ce contexte). Il fournit également d'autres fonctionnalités importantes, y compris une protection contre les attaques par déni de service.

Service Firestore

Le service Firestore effectue des contrôles sur la requête API, qui incluent l'authentification, l'autorisation, la vérification des quotas et les règles de sécurité, et gère également les transactions. Ce service Firestore inclut un client de stockage qui interagit avec la couche de stockage pour les lectures et écritures de données.

Couche de stockage Firestore

La couche de stockage Firestore est chargée de stocker à la fois les données et les métadonnées, ainsi que les fonctionnalités de base de données associées fournies par Firestore. Les sections suivantes décrivent comment les données sont organisées dans la couche de stockage Firestore et comment le système évolue. Apprendre comment les données sont organisées peut vous aider à concevoir un modèle de données évolutif et à mieux comprendre les bonnes pratiques concernant Firestore.

Plages et divisions de clés

Firestore est une base de données NoSQL orientée documents. Vous stockez les données dans des documents organisés en hiérarchies de collections. La hiérarchie de collection et l'ID de document sont traduits en une seule clé pour chaque document. Les documents sont stockés de manière logique et ordonnés de manière lexicographique en fonction de cette clé unique. Nous utilisons le terme "plage de clés" pour désigner une plage de clés contiguës au niveau lexicographique.

Une base de données Firestore classique est trop volumineuse pour tenir sur une seule machine physique. Il se peut également que la charge de travail sur les données soit trop lourde à gérer pour une seule machine. Pour gérer des charges de travail importantes, Firestore partitionne les données en éléments distincts qui peuvent être stockés sur plusieurs machines ou serveurs de stockage, ainsi que les diffuser. Ces partitions sont créées dans les tables de base de données en blocs de plages de clés appelées "splits".

Réplication synchrone

Notez que la base de données est toujours répliquée automatiquement et de manière synchrone. Les données sont réparties en plusieurs zones, et des réplicas sont créés pour qu'elles restent disponibles même si une zone devient inaccessible. La réplication cohérente sur les différentes copies de la partition est gérée par l'algorithme Paxos pour le consensus. Une instance répliquée de chaque partition est élue pour agir en tant que leader Paxos, responsable de la gestion des écritures sur cette partition. La réplication synchrone vous permet de toujours être en mesure de lire la dernière version des données depuis Firestore.

Le résultat global est un système évolutif et hautement disponible qui offre des latences faibles pour les lectures et les écritures, indépendamment des charges de travail importantes et à très grande échelle.

Mise en page des données

Firestore est une base de données de documents sans schéma. Toutefois, en interne, il organise les données principalement dans deux tables de style base de données relationnelle dans sa couche de stockage, comme suit :

  • Table Documents: les documents sont stockés dans cette table.
  • Table Index: les entrées d'index qui permettent d'obtenir des résultats efficacement et de les trier par valeur d'index sont stockées dans cette table.

Le schéma suivant montre à quoi pourraient ressembler les tables d'une base de données Firestore avec les divisions. Les divisions sont répliquées dans trois zones différentes, chacune d'entre elles étant associée à une variante optimale Paxos.

Mise en page des données

Région unique ou multirégionale

Lorsque vous créez une base de données, vous devez sélectionner une région ou une multirégion.

Un emplacement régional correspond à un emplacement géographique spécifique, comme us-west1. Comme expliqué précédemment, les divisions de données d'une base de données Firestore comportent des instances répliquées dans différentes zones de la région sélectionnée.

Un emplacement multirégional correspond à un ensemble défini de régions dans lesquelles des instances répliquées de la base de données sont stockées. Dans un déploiement multirégional de Firestore, deux des régions disposent d'instances répliquées complètes de l'ensemble des données de la base de données. Une troisième région dispose d'un remplacement témoin qui ne conserve pas un ensemble complet de données, mais qui participe à la réplication. En répliquant les données entre plusieurs régions, les données peuvent être écrites et lues, même en cas de perte d'une région entière.

Pour en savoir plus sur les emplacements d'une région, consultez Emplacements Firestore.

Région unique ou multirégion

Comprendre le cycle de vie d'une écriture dans Firestore

Un client Firestore peut écrire des données en créant, en mettant à jour ou en supprimant un seul document. Une écriture dans un seul document nécessite la mise à jour atomique du document et de ses entrées d'index associées dans la couche de stockage. Firestore accepte également les opérations atomiques consistant en plusieurs lectures et/ou écritures sur un ou plusieurs documents.

Pour tous les types d'écritures, Firestore fournit les propriétés ACID (atomicité, cohérence, isolation et durabilité) des bases de données relationnelles. Firestore offre également la sérialisabilité, ce qui signifie que toutes les transactions apparaissent comme exécutées dans un ordre sérialisé.

Principales étapes d'une transaction d'écriture

Lorsque le client Firestore émet une écriture ou valide une transaction, à l'aide de l'une des méthodes mentionnées précédemment, celle-ci est exécutée en interne en tant que transaction en lecture/écriture dans la base de données dans la couche de stockage. La transaction permet à Firestore de fournir les propriétés ACID mentionnées précédemment.

Lors de la première étape d'une transaction, Firestore lit le document existant et détermine les mutations à apporter aux données de la table "Documents".

Vous devez également mettre à jour la table "Indexes" comme suit :

  • Les champs ajoutés aux documents nécessitent des insertions correspondantes dans le tableau des index.
  • Les champs supprimés des documents doivent être supprimés en conséquence dans la table "Indexes".
  • Les champs qui sont modifiés dans les documents nécessitent à la fois des suppressions (pour les anciennes valeurs) et des insertions (pour les nouvelles valeurs) dans le tableau des index.

Pour calculer les mutations mentionnées précédemment, Firestore lit la configuration d'indexation du projet. La configuration d'indexation stocke des informations sur les index d'un projet. Firestore utilise deux types d'index: les index à champ unique et les index composites. Pour en savoir plus sur les index créés dans Firestore, consultez Types d'index dans Firestore.

Une fois les mutations calculées, Firestore les collecte dans une transaction, puis la valide.

Comprendre une transaction d'écriture dans la couche de stockage

Comme indiqué précédemment, une écriture dans Firestore implique une transaction en lecture-écriture dans la couche de stockage. En fonction de la disposition des données, une écriture peut impliquer une ou plusieurs divisions, comme illustré dans la mise en page des données.

Dans le diagramme suivant, la base de données Firestore comporte huit divisions (1 à 8) hébergées sur trois serveurs de stockage différents dans une même zone. Chaque division est répliquée dans trois zones différentes (ou plus). Chaque partition a un leader Paxos, qui peut se trouver dans une zone différente pour chaque partition.

Division de la base de données Firestore

Prenons l'exemple d'une base de données Firestore dont la collection Restaurants se présente comme suit :

Collection de restaurants

Le client Firestore demande la modification suivante d'un document de la collection Restaurant en mettant à jour la valeur du champ priceCategory.

Passer à un document de la collection

Les étapes générales suivantes décrivent ce qui se passe lors de l'écriture :

  1. Créez une transaction en lecture/écriture.
  2. Lisez le document restaurant1 de la collection Restaurants à partir du tableau Documents à partir de la couche de stockage.
  3. Lisez les index du document à partir de la table Index.
  4. Calculez les mutations à apporter aux données. Dans ce cas, il existe cinq mutations:
    • M1 : Mettez à jour la ligne de restaurant1 dans le tableau Documents pour refléter la modification de la valeur du champ priceCategory.
    • M2 et M3 : supprimez les lignes correspondant à l'ancienne valeur priceCategory dans le tableau Indexes (Index) pour les index ascendants et descendants.
    • M4 et M5: insère les lignes correspondant à la nouvelle valeur de priceCategory dans la table Index pour les index décroissants et croissants.
  5. Effectuez un commit sur ces mutations.

Le client de stockage du service Firestore recherche les partitions propriétaires des clés des lignes à modifier. Prenons l'exemple où la partition 3 sert M1 et la partition 6 sert M2 à M5. Une transaction distribuée implique toutes ces divisions en tant que participants. Les fractionnements des participants peuvent également inclure tout autre fractionnement à partir duquel des données ont été lues précédemment dans le cadre de la transaction en lecture-écriture.

Les étapes suivantes décrivent ce qui se passe lors du commit :

  1. Le client de stockage émet une validation. Le commit contient les mutations M1 à M5.
  2. Les parties 3 et 6 participent à cette transaction. L'un des participants doit être désigné comme coordonnateur (par exemple, la partie 3). Celui-ci a pour tâche de s'assurer que la transaction est validée ou annulée de manière atomique pour tous les participants.
    • Les répliques principales de ces divisions sont responsables du travail effectué par les participants et les coordinateurs.
  3. Chaque participant et coordinateur exécute un algorithme Paxos ainsi que ses instances répliquées respectives.
    • La variante optimale exécute un algorithme Paxos avec les instances répliquées. Le quorum est atteint si la plupart des réplicas répondent à la variante optimale avec une réponse ok to commit.
    • Chaque participant informe ensuite le coordinateur lorsqu'il est préparé (première phase de validation en deux phases). Si un participant ne peut pas valider la transaction, l'ensemble de la transaction est aborts.
  4. Une fois que le coordinateur sait que tous les participants, y compris lui-même, sont prêts, il communique le résultat de la transaction accept à tous les participants (deuxième phase de la validation en deux phases). Au cours de cette phase, chaque participant enregistre la décision de validation dans un stockage stable, et la transaction est validée.
  5. Le coordinateur répond au client de stockage dans Firestore que la transaction a été validée. En parallèle, le coordinateur et tous les participants appliquent les mutations aux données.

Cycle de vie des commits

Lorsque la base de données Firestore est petite, il peut arriver qu'une seule division possède toutes les clés des mutations M1 à M5. Dans ce cas, il n'y a qu'un seul participant à la transaction et la validation en deux phases mentionnée précédemment n'est pas requise, ce qui accélère les écritures.

Écriture dans un emplacement multirégional

Dans un déploiement multirégional, la répartition des réplicas entre les régions augmente la disponibilité, mais a un impact sur les performances. La communication entre les réplicas dans différentes régions prend plus de temps. Par conséquent, la latence de référence des opérations Firestore est légèrement supérieure à celle des déploiements dans une seule région.

Nous configurons les instances répliquées de sorte que le leadership des divisions reste toujours dans la région principale. La région principale est celle à partir de laquelle le trafic entrant vers le serveur Firestore. Cette décision de leadership réduit le délai aller-retour de la communication entre le client de stockage dans Firestore et le leader du réplica (ou coordinateur pour les transactions multi-fractionnées).

Chaque écriture dans Firestore implique également une interaction avec le moteur en temps réel de Firestore. Pour en savoir plus sur les requêtes en temps réel, consultez Comprendre les requêtes en temps réel à grande échelle.

Comprendre le cycle de vie d'une lecture dans Firestore

Cette section explore les lectures autonomes non en temps réel dans Firestore. En interne, le serveur Firestore traite la plupart de ces requêtes en deux étapes principales:

  1. Analyse de plage unique sur la table Indexes
  2. Recherches de points dans la table Documents en fonction du résultat de l'analyse précédente
Certaines requêtes peuvent nécessiter moins de traitement (par exemple, les requêtes keys-only pour le mode Datastore) ou plus de traitement (par exemple, les requêtes IN) dans Firestore.

Les lectures de données depuis la couche de stockage sont effectuées en interne à l'aide d'une transaction de base de données afin d'assurer des lectures cohérentes. Toutefois, contrairement aux transactions utilisées pour les écritures, ces transactions ne sont pas verrouillées. Au lieu de cela, elles fonctionnent en choisissant un horodatage, puis en exécutant toutes les lectures à cet horodatage. Étant donné qu'ils n'acquièrent pas de verrous, ils ne bloquent pas les transactions en lecture-écriture simultanées. Pour exécuter cette transaction, le client de stockage dans Firestore spécifie une limite d'horodatage, qui indique à la couche de stockage comment choisir un code temporel de lecture. Le type de limite d'horodatage choisi par le client de stockage dans Firestore est déterminé par les options de lecture de la requête de lecture.

Comprendre une transaction de lecture dans la couche de stockage

Cette section décrit les types de lectures et leur traitement dans la couche de stockage de Firestore.

Lectures intensives

Par défaut, les lectures Firestore sont fortement cohérentes. Cette cohérence forte signifie qu'une lecture Firestore renvoie la dernière version des données, qui reflète toutes les écritures validées jusqu'au début de la lecture.

Lecture avec division unique

Le client de stockage de Firestore recherche les partitions propriétaires des clés des lignes à lire. Supposons qu'il doit effectuer une lecture à partir de la section 3 de la section précédente. Le client envoie la requête de lecture au réplica le plus proche pour réduire la latence aller-retour.

À ce stade, les cas suivants peuvent se produire en fonction de l'instance répliquée choisie:

  • La requête de lecture est transmise à une instance répliquée principale (zone A).
    • Cette dernière étant toujours à jour, la lecture peut se dérouler directement.
  • La requête de lecture est envoyée à une instance répliquée non principale (telle que la zone B)
    • La partition 3 peut savoir, grâce à son état interne, qu'elle dispose de suffisamment d'informations pour diffuser la lecture, et la division le fait.
    • La partition 3 n'est pas sûre d'avoir vu les dernières données. Il envoie un message au leader pour lui demander l'horodatage de la dernière transaction à appliquer pour diffuser la lecture. Une fois que cette transaction est appliquée, la lecture peut avoir lieu.

Firestore renvoie ensuite la réponse à son client.

Lecture multi-partitionnée

Si les lectures doivent être effectuées à partir de plusieurs divisions, le même mécanisme s'applique à toutes les divisions. Une fois les données renvoyées par toutes les divisions, le client de stockage de Firestore combine les résultats. Firestore répond ensuite à son client avec ces données.

Lectures non actualisées

Les lectures strictes sont le mode par défaut dans Firestore. Cependant, cela entraîne une latence potentiellement plus élevée en raison de la communication qui peut être requise avec le leader. Souvent, votre application Firestore n'a pas besoin de lire la dernière version des données, et cette fonctionnalité fonctionne bien avec des données qui peuvent être obsolètes de quelques secondes.

Dans ce cas, le client peut choisir de recevoir des lectures non actualisées à l'aide des options de lecture read_time. Dans ce cas, les lectures sont effectuées comme si les données se trouvaient à read_time, et il est très probable que le réplica le plus proche ait déjà vérifié qu'il dispose de données à l'read_time spécifié. Pour des performances nettement supérieures, une valeur d'obsolescence raisonnable est de 15 secondes. Même pour les lectures obsolètes, les lignes générées sont cohérentes entre elles.

Éviter les zones à forte activité

Dans Firestore, les divisions sont automatiquement divisées en plus petits éléments afin de répartir le travail de diffusion du trafic vers davantage de serveurs de stockage en cas de besoin ou lorsque l'espace clé s'accroît. Les divisions créées pour gérer le trafic excédentaire sont conservées pendant environ 24 heures, même si le trafic disparaît. Ainsi, en cas de pics de trafic récurrents, les divisions sont maintenues et d'autres divisions sont introduites chaque fois que nécessaire. Ces mécanismes aident les bases de données Firestore à s'adapter automatiquement lorsque la charge du trafic ou la taille de la base de données augmente. Toutefois, il existe certaines limites à prendre en compte, comme expliqué ci-dessous.

La division du stockage et du chargement prend du temps. Une augmentation trop rapide du trafic peut entraîner des erreurs de latence élevée ou de dépassement du délai, communément appelées hotspots, le temps que le service s'ajuste. La bonne pratique consiste à répartir les opérations sur la plage de clés, tout en augmentant le trafic sur une collection dans une base de données avec 500 opérations par seconde. Après cette augmentation progressive, augmentez le trafic jusqu'à 50 % toutes les cinq minutes. Ce processus, appelé règle 500/50/5, positionne la base de données de manière à ce qu'elle s'adapte de manière optimale à votre charge de travail.

Bien que les divisions soient créées automatiquement avec une charge croissante, Firestore ne peut diviser une plage de clés que jusqu'à ce qu'il diffuse un seul document à l'aide d'un ensemble dédié de serveurs de stockage répliqués. Par conséquent, des volumes élevés et soutenus d'opérations simultanées sur un même document peuvent entraîner un hotspot sur ce document. Si vous rencontrez des latences élevées et durables sur un seul document, envisagez de modifier votre modèle de données pour diviser ou répliquer les données sur plusieurs documents.

Les erreurs de contention se produisent lorsque plusieurs opérations tentent de lire et/ou d'écrire le même document simultanément.

Un autre cas particulier de hotspotting se produit lorsqu'une clé qui augmente ou diminue de manière séquentielle est utilisée comme ID de document dans Firestore, et que le nombre d'opérations par seconde est considérablement élevé. La création d'autres divisions n'est d'aucune aide dans ce cas, car la hausse de trafic se déplace simplement vers la nouvelle répartition. Étant donné que Firestore indexe automatiquement tous les champs du document par défaut, de tels hotspots mobiles peuvent également être créés sur l'espace d'index d'un champ de document contenant une valeur qui augmente ou diminue de manière séquentielle, comme un horodatage.

Notez que si vous suivez les pratiques décrites ci-dessus, Firestore peut évoluer pour répondre à des charges de travail arbitrairement importantes sans que vous ayez à ajuster la configuration.

Dépannage

Firestore fournit Key Visualizer, un outil de diagnostic conçu pour analyser les schémas d'utilisation et résoudre les problèmes de hotspots.

Étape suivante