Comprendre les lectures et les écritures à grande échelle

Lisez ce document pour prendre des décisions éclairées sur la conception de vos applications afin d'assurer 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 sites Web et de serveurs par Firebase et Google Cloud. Il est très facile de se lancer avec Firestore et de développer des applications riches et puissantes.

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

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

Comprendre les composants généraux

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

Composants généraux

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 vers l'API Firestore, les bibliothèques clientes fournissent une couche d'abstraction pour simplifier l'utilisation de l'API et mettre en œuvre 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é (dans ce contexte, le service Firestore). Il offre également d'autres fonctionnalités importantes, y compris la protection contre les attaques par déni de service.

Service Firestore

Le service Firestore effectue des vérifications sur la requête API, y compris l'authentification, l'autorisation, les contrôles de quota 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 les é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. Découvrir comment les données sont organisées peut vous aider à concevoir un modèle de données évolutif et à mieux comprendre les bonnes pratiques de Firestore.

Plages de clés et fractionnements

Firestore est une base de données NoSQL orientée documents. Vous stockez des données dans des documents organisés en hiérarchies de collections. La hiérarchie de la collection et l'ID du document sont traduits en une clé unique pour chaque document. Les documents sont stockés et classés de manière lexicographique de manière logique à l'aide de cette clé unique. Nous utilisons le terme plage de clés pour désigner une plage de clés contiguë d'un point de vue lexicographique.

Une base de données Firestore type est trop volumineuse pour tenir sur une seule machine physique. Il arrive également que la charge de travail pesant 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 pour les diffuser. Ces partitions sont créées sur les tables de base de données dans des blocs de plages de clés appelés divisions.

Réplication synchrone

Il est important de noter que la base de données est toujours répliquée automatiquement et de manière synchrone. Les divisions de données comportent des instances répliquées dans différentes zones pour qu'elles restent disponibles même lorsqu'une zone devient inaccessible. La réplication cohérente sur les différentes copies de la division est gérée par l'algorithme Paxos pour obtenir le consensus. Une instance répliquée de chaque partition est choisie pour agir en tant que variante optimale Paxos, laquelle est responsable de la gestion des écritures sur cette partition. La réplication synchrone vous permet de toujours lire la dernière version des données depuis Firestore.

Au final, nous obtenons un système évolutif et à disponibilité élevée qui offre de faibles latences pour les lectures et les écritures, quelles que soient les charges de travail lourdes et à très grande échelle.

Mise en page des données

Firestore est une base de données de documents sans schéma. Cependant, en interne, il dispose les données principalement dans deux tables de type 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 permettant d'obtenir des résultats efficacement et de les trier par valeur d'index y sont stockées.

Le schéma suivant montre à quoi peuvent ressembler les tables d'une base de données Firestore avec les divisions. Les divisions sont répliquées dans trois zones différentes, et chaque répartition est associée à un leader Paxos.

Mise en page des données

Région unique ou emplacement multirégional

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

Un emplacement régional unique correspond à un emplacement géographique spécifique, par exemple us-west1. 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, comme expliqué précédemment.

Un emplacement multirégional est constitué d'un ensemble défini de régions dans lesquelles sont stockées les instances répliquées de la base de données. Dans un déploiement multirégional de Firestore, deux des régions disposent d'instances répliquées complètes des données entières de la base de données. Une troisième région comporte une instance répliquée témoin qui ne gère pas un ensemble de données complet, mais qui participe à la réplication. La réplication des données entre plusieurs régions permet d'écrire et de lire les données même en cas de perte d'une région entière.

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

Région unique ou emplacement multirégional

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 de mettre à jour le document et ses entrées d'index associées de manière atomique dans la couche de stockage. Firestore est également compatible avec les opérations atomiques constituées de 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 si elles étaient exécutées dans un ordre sérialisé.

Étapes générales 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 sous la forme d'une transaction en lecture/écriture de 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".

Cela inclut également les mises à jour nécessaires du tableau des index, comme suit:

  • Les champs en cours d'ajout aux documents nécessitent des insertions correspondantes dans la table Index.
  • Les champs supprimés des documents doivent être supprimés de la table des index.
  • Les champs en cours de modification dans les documents doivent être supprimés (pour les anciennes valeurs) et insérés (pour les nouvelles valeurs) dans la table d'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: à champ unique et composite. 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. Selon 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 schéma suivant, la base de données Firestore comporte huit divisions (de 1 à 8) hébergées sur trois serveurs de stockage différents dans une même zone, et chaque division est répliquée dans au moins trois zones différentes. Chaque division dispose d'une variante optimale Paxos, qui peut se trouver dans une zone différente en fonction des répartitions.

Fractionnement de la base de données Firestore

Prenons l'exemple d'une base de données Firestore contenant la collection Restaurants comme suit:

Collection de restaurants

Le client Firestore demande la modification suivante à 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 dans la collection Restaurants à partir de la table Documents 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: mettre à jour la ligne 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 de priceCategory dans la table Index des index croissants et décroissants.
    • M4 et M5: insérez les lignes correspondant à la nouvelle valeur de priceCategory dans la table Index pour les index croissants et décroissants.
  5. Effectuez un commit de ces mutations.

Le client de stockage du service Firestore recherche les divisions qui possèdent les clés des lignes à modifier. Imaginons un cas où la partition 3 dessert M1, et la partition 6 diffuse M2-M5. Il y a une transaction distribuée, impliquant toutes ces divisions en tant que participants. Les divisions des participants peuvent également inclure toute autre division à partir de laquelle 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 dans le cadre du commit:

  1. Le client de stockage émet un commit. Le commit contient les mutations M1 à M5.
  2. Les divisions 3 et 6 sont les participants de cette transaction. L'un des participants est choisi en tant que coordonnateur, par exemple pour la répartition 3. Le travail du coordinateur est 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 avec 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 instances répliquées répondent par une réponse ok to commit à la variante optimale.
    • 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éparés, il communique le résultat de la transaction accept à tous les participants (deuxième phase de validation en deux phases). Au cours de cette phase, chaque participant enregistre la décision de commit dans un espace de 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 d'un commit

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 dans la transaction et le commit en deux phases mentionné précédemment n'est pas requis, ce qui accélère les écritures.

Écriture dans un emplacement multirégional

Dans un déploiement multirégional, la répartition des instances répliquées entre les régions augmente la disponibilité, mais engendre un coût en termes de performances. La communication entre les instances répliquées de différentes régions prend plus de temps. Par conséquent, la latence de référence pour les opérations Firestore est légèrement supérieure à celle des déploiements à région unique.

Nous configurons les instances répliquées de sorte que la direction des écrans fractionnés reste toujours dans la région principale. La région principale est celle depuis laquelle le trafic arrive au serveur Firestore. Cette décision de direction réduit le délai aller-retour de la communication entre le client de stockage dans Firestore et l'instance répliquée principale (ou le coordinateur pour les transactions multi-partitions).

Chaque écriture dans Firestore implique également une interaction avec le moteur en temps réel dans 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 lecture dans Firestore

Cette section explore les lectures autonomes et en différé dans Firestore. En interne, le serveur Firestore gère la plupart de ces requêtes en deux grandes étapes:

  1. Une analyse d'une seule plage sur la table Index
  2. Recherches ponctuelles dans la table Documents basées sur le résultat de l'analyse précédente
Certaines requêtes peuvent nécessiter moins de traitement (par exemple, des requêtes keys-only pour le mode Datastore) ou plus de traitement (par exemple, des requêtes IN) dans Firestore.

Les lectures de données à partir de la couche de stockage sont effectuées en interne à l'aide d'une transaction de base de données pour garantir la cohérence des lectures. Toutefois, contrairement aux transactions utilisées pour les écritures, ces transactions ne sont pas verrouillées. Au lieu de cela, ils choisissent un code temporel, puis exécute toutes les lectures à ce code temporel. Comme elles n'acquièrent pas de verrous, elles 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 Read.

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 extrêmes

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 de fractionnement simple

Le client de stockage dans Firestore recherche les divisions qui possèdent les clés des lignes à lire. Supposons qu'elle doive effectuer une lecture à partir de la partition 3 de la section précédente. Le client envoie la requête de lecture à l'instance répliquée la 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 envoyée à une instance répliquée principale (zone A).
    • Comme la variante optimale est toujours à jour, la lecture peut se poursuivre directement.
  • La requête de lecture est envoyée à une instance répliquée non principale (par exemple, la zone B).
    • La division 3 peut savoir par son état interne qu'elle dispose de suffisamment d'informations pour diffuser la lecture, et la division le fait.
    • La division 3 n'est pas certaine d'avoir vu les données les plus récentes. Il envoie un message à la variante optimale pour demander l'horodatage de la dernière transaction à appliquer pour diffuser la lecture. Une fois cette transaction appliquée, la lecture peut se poursuivre.

Firestore renvoie ensuite la réponse à son client.

Lecture multi-split

Dans le cas où les lectures doivent être effectuées à partir de plusieurs divisions, le même mécanisme est appliqué à toutes les divisions. Une fois que les données ont été renvoyées par toutes les divisions, le client de stockage de Firestore combine les résultats. Firestore fournit ensuite ces données à son client.

Lectures obsolètes

Les lectures fortes sont le mode par défaut dans Firestore. Cependant, cela a un coût, une latence potentielle plus élevée en raison de la communication qui peut être requise avec la variante optimale. Souvent, votre application Firestore n'a pas besoin de lire la dernière version des données et la 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 car les données étaient à l'emplacement read_time. Il est fort probable que l'instance répliquée la plus proche ait déjà vérifié qu'elle disposait de données à l'emplacement read_time spécifié. Pour améliorer sensiblement les performances, 15 secondes est une valeur d'obsolescence raisonnable. Même pour les lectures non actualisées, les lignes générées sont cohérentes entre elles.

Éviter les points d'accès

Dans Firestore, les divisions sont automatiquement divisées en plusieurs petites parties pour répartir le trafic de diffusion vers un plus grand nombre de serveurs de stockage en cas de besoin ou lorsque l'espace clé augmente. 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 conservées et d'autres divisions sont ajoutées chaque fois que nécessaire. Ces mécanismes aident les bases de données Firestore à s'adapter automatiquement en cas de charge de trafic ou de taille de base de données croissante. Il existe toutefois certaines limites à connaître, comme expliqué ci-dessous.

La répartition du stockage et de la charge prend du temps. Une augmentation trop rapide du trafic peut entraîner une latence élevée ou des erreurs de dépassement de délai, communément appelées hotspots, pendant 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 d'une base de données avec 500 opérations par seconde. Après cette montée en puissance progressive, augmentez le trafic jusqu'à 50% toutes les cinq minutes. Ce processus, appelé règle 500/50/5, permet de positionner la base de données de manière à s'adapter 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'un seul document soit diffusé à l'aide d'un ensemble dédié de serveurs de stockage répliqués. Par conséquent, des volumes importants et continus d'opérations simultanées sur un même document peuvent créer un hotspot sur ce document. Si vous rencontrez des latences élevées sur un seul document, vous devriez envisager de modifier votre modèle de données pour diviser ou répliquer les données sur plusieurs documents.

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

Un autre cas particulier de hotspotting se produit lorsqu'une clé qui augmente ou diminue séquentiellement 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'un plus grand nombre de divisions n'est pas d'une grande utilité, 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 telles zones cliquables en mouvement peuvent également être créées sur l'espace d'index d'un champ de document contenant une valeur qui augmente ou diminue séquentiellement, comme un horodatage.

Notez qu'en suivant les pratiques décrites ci-dessus, Firestore peut s'adapter pour diffuser des charges de travail arbitrairement volumineuses sans que vous ayez à ajuster une configuration.

Dépannage

Firestore fournit Key Visualizer en tant qu'outil de diagnostic spécialement conçu pour analyser les schémas d'utilisation et résoudre les problèmes de point d'accès.

Étape suivante