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 inclut des sujets avancés sur Firestore. Si vous débutez avec Firestore, consultez plutôt le guide de démarrage rapide.

Créé par Firebase et Google Cloud, Firestore est une base de données flexible et évolutive conçue pour le développement mobile, Web et serveur. Il est très facile de se lancer avec Firestore et d'écrire des applications riches et performantes.

Pour vous assurer que vos applications continuent de fonctionner correctement à mesure que la taille de votre base de données et le 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 qui peuvent affecter les performances.

Consultez les sections suivantes pour connaître les bonnes pratiques à suivre avant d'élaborer l'architecture de 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 transfère au service Google approprié (service Firestore dans ce contexte). Il offre é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 vérifications de la requête API, y compris l'authentification, l'autorisation, les vérifications 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. Comprendre comment les données sont organisées peut vous aider à concevoir un modèle de données évolutif et à mieux comprendre les bonnes pratiques dans Firestore.

Plage de clés et fractionnement

Firestore est une base de données NoSQL orientée documents. Vous stockez les données dans des documents, qui sont 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 et triés de manière logique et lexicographique par cette seule clé. 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 typique est trop volumineuse pour être stockée sur une seule machine physique. Il se peut également que la charge de travail sur les données soit trop lourde à traiter pour une seule machine. Pour gérer de lourdes charges de travail, Firestore partitionne les données en éléments distincts qui peuvent être stockés et diffusés à partir de plusieurs machines ou serveurs de stockage. 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

Il est important de noter 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 de consensus Paxos. Une instance dupliquée de chaque partition est élue pour agir en tant que variante optimale Paxos, qui est chargée de gérer les écritures sur cette partition. La réplication synchrone vous permet de toujours lire la dernière version des données de Firestore.

Le résultat global est un système évolutif et disponibilité élevée 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.
  • Tableau Indexes (Index) : les entrées d'index qui permettent d'obtenir des résultats de manière efficace et triés par valeur d'index sont stockées dans ce tableau.

Le diagramme suivant montre à quoi peuvent ressembler les tables d'une base de données Firestore avec les divisions. Les partitions sont répliquées dans trois zones différentes, et une variante optimale Paxos est attribuée à chaque partition.

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. Les divisions de données d'une base de données Firestore ont des répliques dans différentes zones de la région sélectionnée, comme expliqué précédemment.

Un emplacement multirégional se compose d'un ensemble défini de régions dans lesquelles les réplicas de la base de données sont stockés. Dans un déploiement multirégional de Firestore, deux des régions disposent de répliques 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, vous pouvez les écrire et les lire 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 page Emplacements Firestore.

Région unique ou multirégionale

Comprendre la durée 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 fournit également la sérialisabilité, ce qui signifie que toutes les transactions apparaissent comme si elles étaient exécutées dans un ordre séquentiel.

Étapes générales d'une transaction d'écriture

Lorsque le client Firestore effectue une écriture ou valide une transaction à l'aide de l'une des méthodes mentionnées précédemment, cette opération est exécutée en interne en tant que transaction de base de données en lecture-écriture 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 doivent être insérés dans le tableau "Indexes".
  • Les champs supprimés des documents doivent être supprimés de 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 les 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 mise en page 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 (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(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 à un document de la collection Restaurant en mettant à jour la valeur du champ priceCategory.

Passer à un document d'une 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 dans la table Documents de la couche de stockage.
  3. Lisez les index du document dans la table Indexes (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érez les lignes pour la nouvelle valeur de priceCategory dans le tableau Index pour les index ascendants et descendants.
  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 d'une situation 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 partitions 3 et 6 sont les participants à cette transaction. L'un des participants est choisi comme coordinateur, par exemple le groupe 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éplicas principaux de ces partitions sont responsables du travail effectué par les participants et les coordinateurs.
  3. Chaque participant et coordinateur exécute un algorithme Paxos avec ses réplications respectives.
    • Le leader exécute un algorithme Paxos avec les répliques. Le quorum est atteint si la plupart des réplicas répondent à la variante optimale avec une réponse ok to commit.
    • Chaque participant avertit ensuite le coordinateur lorsqu'il est prêt (première phase de la validation en deux phases). Si l'un des participants ne peut pas valider la transaction, l'intégralité de la transaction 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. Parallèlement, 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 est possible qu'un seul fractionnement possède toutes les clés des mutations M1 à M5. Dans ce cas, il n'y a qu'un seul participant à la transaction et le commit en deux phases mentionné précédemment n'est pas nécessaire, ce qui accélère les écritures.

Écritures dans plusieurs régions

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 pour les opérations Firestore est légèrement supérieure par rapport aux déploiements dans une seule région.

Nous configurons les réplicas de manière à ce que la responsabilité des répartitions reste toujours dans la région principale. La région principale est celle d'où provient le trafic 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 présente les lectures autonomes hors temps réel dans Firestore. En interne, le serveur Firestore gère la plupart de ces requêtes en deux grandes étapes:

  1. Analyse de plage unique sur la table Indexes
  2. Recherches ponctuelles dans le tableau 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 à partir de la couche de stockage sont effectuées en interne à l'aide d'une transaction de base de données pour assurer 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, elles choisissent un code temporel, puis exécutent 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 de lecture.

Comprendre une transaction de lecture dans la couche de stockage

Cette section décrit les types de lectures et la façon dont elles sont traitées dans la couche de stockage de Firestore.

Lectures fortes

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 qui ont été effectué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 du réplica choisi:

  • La requête de lecture est envoyée à une instance dupliqué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 dupliquée non principale (par exemple, la zone B).
    • La scission 3 peut savoir par son état interne qu'elle dispose de suffisamment d'informations pour diffuser la lecture, et elle le fait.
    • La division 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

Lorsque 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 à partir de 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. Votre application Firestore n'a souvent 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 obsolètes à 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 obtenir des performances nettement meilleures, 15 secondes est une valeur d'obsolescence raisonnable. Même pour les lectures obsolètes, les lignes générées sont cohérentes entre elles.

Éviter les zones à forte activité

Les divisions dans Firestore sont automatiquement divisées en parties plus petites pour répartir la distribution du trafic vers davantage de serveurs de stockage si nécessaire ou lorsque l'espace de clés augmente. Les répartitions 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 fractionnements sont maintenus et d'autres sont introduits si nécessaire. Ces mécanismes permettent aux bases de données Firestore de s'adapter automatiquement à une augmentation de la charge de trafic ou de la taille de la base de données. Toutefois, certaines limites s'appliquent, comme indiqué ci-dessous.

La répartition du stockage et de la charge prend du temps. Si vous augmentez trop rapidement le trafic, vous risquez de rencontrer des erreurs de latence élevée ou de délai dépassé (appelées communément points chauds), pendant que le service s'ajuste. Il est recommandé de 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 est appelé règle 500/50/5. Il permet de positionner la base de données de manière optimale pour qu'elle s'adapte à votre charge de travail.

Bien que des divisions soient créées automatiquement à mesure que la charge augmente, Firestore ne peut diviser une plage de clés que jusqu'à ce qu'il ne serve qu'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 seul document peuvent entraîner un point chaud 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 point chaud se produit lorsqu'une clé à incrément/décrément séquentiel est utilisée comme ID de document dans Firestore et qu'un nombre particulièrement élevé d'opérations par seconde est effectué. Créer d'autres répartitions ne vous aidera pas, car le pic de trafic sera simplement redirigé vers la nouvelle répartition. Comme Firestore indexe automatiquement tous les champs du document par défaut, de tels hotspots mobiles peuvent également être créés dans l'espace d'index pour un champ de document contenant une valeur séquentielle croissante/décroissante, comme un code temporel.

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