Lese- und Schreibvorgänge in großem Maßstab verstehen

Lesen Sie dieses Dokument, um fundierte Entscheidungen zur Architektur Ihrer Anwendungen zu treffen, die eine hohe Leistung und Zuverlässigkeit bieten. Dieses Dokument enthält erweiterte Firestore-Themen. Wenn Sie gerade erst mit Firestore begonnen haben, finden Sie entsprechende Informationen in der Kurzanleitung.

Firestore ist eine flexible, skalierbare Datenbank für die Mobilgeräte-, Web- und Serverentwicklung mit Firebase und Google Cloud. Der Einstieg in Firestore und das Schreiben umfangreicher und leistungsstarker Anwendungen ist ganz einfach.

Damit Ihre Anwendungen weiterhin gut laufen, die Datenbankgröße und der Traffic zunehmen, ist es hilfreich, den Mechanismus von Lese- und Schreibvorgängen im Firestore-Back-End zu verstehen. Außerdem müssen Sie die Interaktion Ihrer Lese- und Schreibvorgänge mit der Speicherebene und die zugrunde liegenden Einschränkungen verstehen, die sich auf die Leistung auswirken können.

In den folgenden Abschnitten finden Sie Best Practices, bevor Sie Ihre Anwendung entwerfen.

Die übergeordneten Komponenten verstehen

Das folgende Diagramm zeigt die übergeordneten Komponenten, die an einer Firestore API-Anfrage beteiligt sind.

Übergeordnete Komponenten

Firestore SDK und Clientbibliotheken

Firestore unterstützt SDKs und Clientbibliotheken für verschiedene Plattformen. Während eine Anwendung direkte HTTP- und RPC-Aufrufe an die Firestore API senden kann, bieten die Clientbibliotheken eine Abstraktionsebene, um die API-Nutzung zu vereinfachen und Best Practices zu implementieren. Sie können auch zusätzliche Funktionen wie Offlinezugriff, Caches usw. bieten.

Google Front End (GFE)

Dies ist ein Infrastrukturdienst, der allen Google Cloud-Diensten gemeinsam ist. Das GFE akzeptiert eingehende Anfragen und leitet sie an den entsprechenden Google-Dienst (in diesem Kontext Firestore-Dienst) weiter. Es bietet auch andere wichtige Funktionen, z. B. Schutz vor Denial-of-Service-Angriffen.

Firestore-Dienst

Der Firestore-Dienst führt Prüfungen der API-Anfrage durch, einschließlich Authentifizierung, Autorisierung, Kontingentprüfungen und Sicherheitsregeln, und verwaltet auch Transaktionen. Dieser Firestore-Dienst umfasst einen Speicherclient, der für die Lese- und Schreibvorgänge von Daten mit der Speicherebene interagiert.

Firestore-Speicherschicht

Auf der Firestore-Speicherebene werden sowohl die Daten als auch die Metadaten sowie die zugehörigen Datenbankfeatures von Firestore gespeichert. In den folgenden Abschnitten wird beschrieben, wie Daten in der Firestore-Speicherebene organisiert sind und wie das System skaliert wird. Wenn Sie wissen, wie Daten organisiert sind, können Sie ein skalierbares Datenmodell entwerfen und die Best Practices in Firestore besser verstehen.

Schlüsselbereiche und Splits

Firestore ist eine dokumentenbasierte NoSQL-Datenbank. Sie speichern Daten in Dokumenten, die in einer Hierarchie von Sammlungen organisiert sind. Die Sammlungshierarchie und die Dokument-ID werden für jedes Dokument in einen einzigen Schlüssel umgewandelt. Dokumente werden nach diesem einzelnen Schlüssel logisch gespeichert und lexikografisch sortiert. Der Begriff Schlüsselbereich bezieht sich auf einen lexikografisch zusammenhängenden Bereich von Schlüsseln.

Eine typische Firestore-Datenbank ist zu groß für einen einzelnen physischen Computer. Es gibt auch Szenarien, in denen die Arbeitslast für die Daten zu stark ist, um von einem einzigen Computer verarbeitet zu werden. Zur Bewältigung großer Arbeitslasten partitioniert Firestore die Daten in separate Teile, die auf mehreren Rechnern oder Speicherservern gespeichert und von diesen bereitgestellt werden können. Diese Partitionen werden in den Datenbanktabellen in Blöcken von Schlüsselbereichen erstellt, die als Splits bezeichnet werden.

Synchrone Replikation

Beachten Sie, dass die Datenbank immer automatisch und synchron repliziert wird. Die Datenaufteilungen enthalten Replikate in verschiedenen Zonen, damit sie auch dann verfügbar bleiben, wenn eine Zone nicht mehr zugänglich ist. Die konsistente Replikation auf die verschiedenen Kopien des Splits wird vom Paxos-Algorithmus für den Konsens verwaltet. Ein Replikat jedes Splits wird als Paxos-Leader festgelegt, der für die Verarbeitung von Schreibvorgängen in diesen Split verantwortlich ist. Durch die synchrone Replikation können Sie immer die neueste Version der Daten aus Firestore lesen.

Das Gesamtergebnis ist ein skalierbares und hochverfügbares System, das unabhängig von hohen Arbeitslasten und in sehr großem Maßstab niedrige Latenzen sowohl für Lese- als auch für Schreibvorgänge bietet.

Datenlayout

Firestore ist eine schemalose Dokumentdatenbank. Intern werden die Daten jedoch in der Speicherebene hauptsächlich in zwei Tabellen im Stil relationaler Datenbanken so angeordnet:

  • Tabelle Dokumente: Dokumente werden in dieser Tabelle gespeichert.
  • Tabelle Indexe: In dieser Tabelle werden Indexeinträge gespeichert, mit denen Ergebnisse effizient abgerufen werden können und die nach Indexwert sortiert sind.

Das folgende Diagramm zeigt, wie die Tabellen für eine Firestore-Datenbank mit den Aufteilungen aussehen könnten. Die Splits werden in drei verschiedenen Zonen repliziert und jedem Split ist ein Paxos-Leader zugewiesen.

Datenlayout

Einzelne Region oder mehrere Regionen

Beim Erstellen einer Datenbank müssen Sie eine Region oder einen multiregionalen Standort auswählen.

Ein einzelner regionaler Standort ist ein bestimmter geografischer Standort, z. B. us-west1. Die Datenaufteilungen einer Firestore-Datenbank enthalten Replikate in verschiedenen Zonen innerhalb der ausgewählten Region, wie zuvor erläutert.

Ein multiregionaler Standort besteht aus einer definierten Gruppe von Regionen, in denen Replikate der Datenbank gespeichert sind. In einer multiregionalen Bereitstellung von Firestore haben zwei der Regionen vollständige Replikate der gesamten Daten in der Datenbank. Eine dritte Region hat ein Zeugenreplikat, das keinen vollständigen Satz von Daten verwaltet, aber an der Replikation beteiligt ist. Durch die Replikation der Daten zwischen mehreren Regionen können die Daten auch bei dem Verlust einer ganzen Region geschrieben und gelesen werden.

Weitere Informationen zu den Standorten einer Region finden Sie unter Firestore-Standorte.

Einzelne Region oder mehrere Regionen

Lebensdauer eines Schreibvorgangs in Firestore verstehen

Ein Firestore-Client kann Daten schreiben, indem er ein einzelnes Dokument erstellt, aktualisiert oder löscht. Bei einem Schreibvorgang in ein einzelnes Dokument müssen sowohl das Dokument als auch die zugehörigen Indexeinträge auf der Speicherebene in kleinstmöglichen Schritten aktualisiert werden. Firestore unterstützt auch atomare Vorgänge, die aus mehreren Lese- und/oder Schreibvorgängen für ein oder mehrere Dokumente bestehen.

Für alle Arten von Schreibvorgängen bietet Firestore die ACID-Attribute (Atomarität, Konsistenz, Isolation und Langlebigkeit) relationaler Datenbanken. Firestore bietet außerdem Serialisierbarkeit. Das bedeutet, dass alle Transaktionen so aussehen, als wären sie in serieller Reihenfolge ausgeführt worden.

Allgemeine Schritte in einer Schreibtransaktion

Wenn der Firestore-Client mit einer der oben genannten Methoden einen Schreibvorgang ausgibt oder eine Transaktion mit Commits ausführt, wird dies intern auf der Speicherebene als Lese-Schreib-Transaktion für Datenbanken ausgeführt. Durch die Transaktion kann Firestore die zuvor erwähnten ACID-Attribute bereitstellen.

Als ersten Schritt einer Transaktion liest Firestore das vorhandene Dokument und bestimmt die Mutationen, die an den Daten in der Dokumenttabelle vorgenommen werden sollen.

Dazu gehört auch die Durchführung der folgenden erforderlichen Aktualisierungen an der Indextabelle:

  • Für Felder, die den Dokumenten hinzugefügt werden, müssen entsprechende Einfügungen in die Tabelle „Indexe“ eingefügt werden.
  • Für Felder, die aus den Dokumenten entfernt werden, sind entsprechende Löschvorgänge in der Tabelle „Indexe“ erforderlich.
  • Für Felder, die in den Dokumenten geändert werden, müssen sowohl Löschvorgänge (für alte Werte) als auch Einfügungen (für neue Werte) in die Tabelle „Indexe“ eingefügt werden.

Firestore liest die Indexierungskonfiguration für das Projekt, um die zuvor erwähnten Mutationen zu berechnen. In der Indexierungskonfiguration werden Informationen zu den Indexen für ein Projekt gespeichert. Firestore verwendet zwei Arten von Indexen: Einzelfeld- und zusammengesetzte Indexe. Ausführliche Informationen zu den in Firestore erstellten Indexen finden Sie unter Indextypen in Firestore.

Nachdem die Mutationen berechnet wurden, erfasst Firestore sie innerhalb einer Transaktion und führt dann einen Commit durch.

Schreibtransaktion auf Speicherebene verstehen

Wie bereits erwähnt, beinhaltet ein Schreibvorgang in Firestore eine Lese-Schreib-Transaktion auf Speicherebene. Je nach Layout der Daten kann ein Schreibvorgang einen oder mehrere Splits umfassen, wie im Datenlayout dargestellt.

Im folgenden Diagramm hat die Firestore-Datenbank acht Splits (mit 1–8 gekennzeichnet), die auf drei verschiedenen Speicherservern in einer einzigen Zone gehostet werden, und jeder Split wird in 3(oder mehr) verschiedenen Zonen repliziert. Für jeden Split gibt es einen Paxos-Leader, der sich für verschiedene Splits in einer anderen Zone befinden kann.

Firestore-Datenbankaufteilung

Betrachten Sie eine Firestore-Datenbank mit der Sammlung Restaurants so:

Restaurantsammlung

Der Firestore-Client fordert die folgende Änderung an einem Dokument in der Sammlung Restaurant an, indem er den Wert des Felds priceCategory aktualisiert.

Zu einem Dokument in einer Sammlung wechseln

In den folgenden allgemeinen Schritten wird beschrieben, was als Teil des Schreibvorgangs geschieht:

  1. Erstellen Sie eine Lese-Schreib-Transaktion.
  2. Lesen Sie das restaurant1-Dokument in der Sammlung Restaurants aus der Tabelle Dokumente auf der Speicherebene.
  3. Lesen Sie die Indexe für das Dokument aus der Tabelle Indexe.
  4. Berechnen Sie die Mutationen, die an den Daten vorgenommen werden sollen. In diesem Fall gibt es fünf Mutationen:
    • M1: Aktualisieren Sie die Zeile für restaurant1 in der Tabelle Dokumente, um die Änderung des Werts im Feld priceCategory widerzuspiegeln.
    • M2 und M3: Löschen Sie die Zeilen für den alten Wert von priceCategory in der Tabelle Indexe für absteigende und aufsteigende Indexe.
    • M4 und M5: Fügen Sie die Zeilen für den neuen Wert von priceCategory in die Tabelle Indexe für absteigende und aufsteigende Indexe ein.
  5. Führen Sie für diese Mutationen ein Commit durch.

Der Speicherclient im Firestore-Dienst sucht die Splits, denen die Schlüssel der zu ändernden Zeilen gehören. Nehmen wir einen Fall, bei dem Split 3 auf M1 und Split 6 auf M2–M5 stellt. Es gibt eine verteilte Transaktion, bei der alle diese Aufteilungen als Teilnehmer beteiligt sind. Die Teilnehmer-Splits können auch andere Splits umfassen, aus denen Daten zuvor im Rahmen der Lese-Schreib-Transaktion gelesen wurden.

In den folgenden Schritten wird beschrieben, was als Teil des Commits geschieht:

  1. Der Speicherclient gibt einen Commit aus. Der Commit enthält die Mutationen M1 bis M5.
  2. Splits 3 und 6 sind die Teilnehmer dieser Transaktion. Einer der Teilnehmer wird zum Koordinator ausgewählt, z. B. Split 3. Die Aufgabe des Koordinators besteht darin, sicherzustellen, dass die Transaktion entweder für alle Teilnehmer übernommen oder in kleinstmöglichen Schritten abgebrochen wird.
    • Die Leader-Replikate dieser Splits sind für die Arbeit der Teilnehmenden und Koordinatoren verantwortlich.
  3. Alle Teilnehmenden und Koordinatoren führen mit ihren jeweiligen Replikaten einen Paxos-Algorithmus aus.
    • Der Leader führt einen Paxos-Algorithmus mit den Replikaten aus. Das Quorum wird erreicht, wenn die meisten Replikate mit einer ok to commit-Antwort an den Leader antworten.
    • Jeder Teilnehmer benachrichtigt dann den Koordinator, wenn er vorbereitet ist (erste Phase des zweiphasigen Commits). Wenn ein Teilnehmer die Transaktion nicht mit Commit durchführen kann, ist die gesamte Transaktion aborts.
  4. Sobald der Koordinator weiß, dass alle Teilnehmer, einschließlich sich selbst, vorbereitet sind, kommuniziert er das accept-Transaktionsergebnis an alle Teilnehmer (zweite Phase des zweiphasigen Commits). In dieser Phase zeichnet jeder Teilnehmer die Commit-Entscheidung in stabilen Speicher auf und die Transaktion wird durchgeführt.
  5. Der Koordinator antwortet dem Speicherclient in Firestore, dass für die Transaktion ein Commit durchgeführt wurde. Parallel dazu wenden der Koordinator und alle Teilnehmenden die Mutationen auf die Daten an.

Commit-Lebenszyklus

Wenn die Firestore-Datenbank klein ist, kann es vorkommen, dass ein einzelner Split alle Schlüssel in den Mutationen M1–M5 besitzt. In einem solchen Fall gibt es nur einen Teilnehmer an der Transaktion und der oben erwähnte zweiphasige Commit ist nicht erforderlich, wodurch die Schreibvorgänge schneller werden.

Schreibvorgänge an mehreren Regionen

In einer multiregionalen Bereitstellung erhöht die Verteilung von Replikaten über die Regionen die Verfügbarkeit, geht aber mit Leistungskosten einher. Die Kommunikation zwischen Replikaten in verschiedenen Regionen dauert länger. Daher ist die Basislatenz für Firestore-Vorgänge im Vergleich zu Bereitstellungen in einer Region etwas höher.

Wir konfigurieren die Replikate so, dass die Führungsebene für Splits immer in der primären Region bleibt. Die primäre Region ist die Region, von der der Traffic auf den Firestore-Server eingeht. Durch diese Entscheidung der Führungsebene wird die Umlaufverzögerung bei der Kommunikation zwischen dem Speicherclient in Firestore und dem Replikat-Leader (oder Koordinator bei Transaktionen mit mehreren Splits) reduziert.

Jeder Schreibvorgang in Firestore beinhaltet auch eine Interaktion mit der Echtzeit-Engine in Firestore. Weitere Informationen zu Echtzeitabfragen finden Sie unter Echtzeitabfragen im großen Maßstab verstehen.

Das Leben eines Lesevorgangs in Firestore verstehen

In diesem Abschnitt werden eigenständige Lesevorgänge in Firestore beschrieben, die nicht in Echtzeit erfasst werden. Intern verarbeitet der Firestore-Server die meisten dieser Abfragen in zwei Hauptphasen:

  1. Einzelner Bereichsscan für die Tabelle Indexes
  2. Punktsuche in der Tabelle Dokumente basierend auf dem Ergebnis des vorherigen Scans
Bestimmte Abfragen erfordern möglicherweise weniger Verarbeitung (z. B. keys-only Abfragen im Datastore-Modus) oder mehr Verarbeitung (z. B. IN-Abfragen) in Firestore.

Die Daten werden intern aus der Speicherschicht gelesen, indem eine Datenbanktransaktion verwendet wird, um konsistente Lesevorgänge zu gewährleisten. Im Gegensatz zu den für Schreibvorgänge verwendeten Transaktionen erhalten diese Transaktionen jedoch keine Sperren. Stattdessen wählen sie einen Zeitstempel aus und führen dann alle Lesevorgänge an diesem Zeitstempel aus. Da sie keine Sperren erhalten, blockieren sie keine gleichzeitigen Lese-Schreib-Transaktionen. Zum Ausführen dieser Transaktion gibt der Speicherclient in Firestore eine Zeitstempelgrenze an, die der Speicherebene mitteilt, wie ein Lesezeitstempel ausgewählt werden soll. Der Typ der Zeitstempelgrenze, der vom Speicherclient in Firestore ausgewählt wird, wird durch die Leseoptionen für die Leseanfrage bestimmt.

Lesetransaktion auf Speicherebene verstehen

In diesem Abschnitt werden die Arten von Lesevorgängen und ihre Verarbeitung auf der Speicherebene in Firestore beschrieben.

Starke Lektüre

Standardmäßig sind Firestore-Lesevorgänge strikt konsistent. Diese Strong Consistency bedeutet, dass ein Firestore-Lesevorgang die neueste Version der Daten zurückgibt, die alle Schreibvorgänge darstellt, für die bis zum Beginn des Lesevorgangs ein Commit durchgeführt wurde.

Einfach geteilter Lesevorgang

Der Speicherclient in Firestore sucht die Splits, denen die Schlüssel der zu lesenden Zeilen gehören. Nehmen wir an, dass ein Lesevorgang aus Split 3 aus dem vorherigen Abschnitt durchgeführt werden muss. Der Client sendet die Leseanfrage an das nächste Replikat, um die Umlauflatenz zu verringern.

An dieser Stelle können je nach ausgewähltem Replikat folgende Fälle auftreten:

  • Die Leseanfrage geht an ein Leader-Replikat (Zone A).
    • Da der Leader immer auf dem neuesten Stand ist, kann der Lesevorgang direkt fortgesetzt werden.
  • Leseanfrage geht an ein Replikat, das nicht als Leader fungiert (z. B. Zone B)
    • Split 3 weiß aufgrund seines internen Status möglicherweise, dass er über genügend Informationen verfügt, um den Lesevorgang zu verarbeiten, und der Split tut dies.
    • Split 3 ist nicht sicher, ob die neuesten Daten erfasst wurden. Er sendet eine Nachricht an den Leader, um nach dem Zeitstempel der letzten Transaktion zu fragen, die für den Lesevorgang ausgeführt werden muss. Sobald diese Transaktion angewendet wurde, kann der Lesevorgang fortgesetzt werden.

Firestore gibt die Antwort dann an seinen Client zurück.

Lesevorgänge in mehreren Teilen

In der Situation, in der die Lesevorgänge aus mehreren Splits ausgeführt werden müssen, findet für alle Splits derselbe Mechanismus statt. Sobald die Daten aus allen Aufteilungen zurückgegeben wurden, kombiniert der Speicherclient in Firestore die Ergebnisse. Firestore antwortet dem Client dann mit diesen Daten.

Veraltete Lesevorgänge

Starke Lesevorgänge sind der Standardmodus in Firestore. Allerdings kann die Latenz aufgrund der erforderlichen Kommunikation mit dem Leader höher sein. Oft muss Ihre Firestore-Anwendung nicht die neueste Version der Daten lesen und die Funktionalität funktioniert gut mit Daten, die einige Sekunden veraltet sind.

In einem solchen Fall kann der Client mithilfe der read_time-Leseoptionen festlegen, dass veraltete Lesevorgänge empfangen werden. In diesem Fall werden Lesevorgänge wie bei read_time durchgeführt. Das nächstgelegene Replikat hat mit hoher Wahrscheinlichkeit bereits bestätigt, dass es Daten am angegebenen read_time enthält. Für eine deutlich bessere Leistung sind 15 Sekunden ein angemessener Wert für die Veralterung. Auch bei veralteten Lesevorgängen sind die ausgegebenen Zeilen konsistent.

Hotspots vermeiden

Die Splits in Firestore werden automatisch in kleinere Teile zerlegt, um die Arbeit der Traffic-Bereitstellung auf mehr Speicherserver bei Bedarf oder bei Erweiterung des Schlüsselbereichs zu verteilen. Aufteilungen, die zum Verarbeiten von zusätzlichem Traffic erstellt wurden, werden ca. 24 Stunden aufbewahrt, selbst wenn der Traffic entfernt wird. Wenn es also wiederkehrende Zugriffsspitzen gibt, werden die Aufteilungen beibehalten und bei Bedarf weitere Aufteilungen eingeführt. Diese Mechanismen helfen Firestore-Datenbanken, bei zunehmender Trafficlast oder Datenbankgröße automatisch zu skalieren. Es gibt jedoch einige Einschränkungen, die Sie kennen sollten, wie unten erläutert.

Das Aufteilen von Speicher und Last nimmt Zeit in Anspruch und ein zu schneller Anstieg des Traffics kann zu hohen Latenz- oder Zeitüberschreitungsfehlern führen, die üblicherweise als Hotspots bezeichnet werden, während der Dienst sich anpasst. Es empfiehlt sich, Vorgänge über den Schlüsselbereich zu verteilen und gleichzeitig den Traffic für eine Sammlung in einer Datenbank mit 500 Vorgängen pro Sekunde zu erhöhen. Nach dieser allmählichen Anlaufzeit können Sie den Traffic alle fünf Minuten um bis zu 50% erhöhen. Dieser Prozess wird als 500/50/5-Regel bezeichnet und positioniert die Datenbank für eine optimale Skalierung entsprechend Ihrer Arbeitslast.

Obwohl Splits bei zunehmender Last automatisch erstellt werden, kann Firestore einen Schlüsselbereich nur so lange aufteilen, bis ein einzelnes Dokument über einen dedizierten Satz replizierter Speicherserver bereitgestellt wird. Daher kann eine hohe und anhaltende Anzahl gleichzeitiger Vorgänge für ein einzelnes Dokument zu einem Engpass bei diesem Dokument führen. Wenn bei einem einzelnen Dokument dauerhaft hohe Latenzen auftreten, sollten Sie Ihr Datenmodell ändern, um die Daten auf mehrere Dokumente aufzuteilen oder zu replizieren.

Konfliktfehler treten auf, wenn mehrere Vorgänge versuchen, dasselbe Dokument gleichzeitig zu lesen und/oder zu schreiben.

Ein weiterer Sonderfall des Heißlaufens tritt auf, wenn ein Schlüssel mit sequenzieller Erhöhung/Abnahme als Dokument-ID in Firestore verwendet wird und es eine beträchtliche Anzahl von Vorgängen pro Sekunde gibt. In diesem Fall hilft es nicht, weitere Aufteilungen zu erstellen, da der Anstieg des Traffics einfach auf die neu erstellte Aufteilung verschoben wird. Da Firestore standardmäßig alle Felder im Dokument indexiert, können solche verschiebbaren Hotspots auch im Indexbereich eines Dokumentfelds erstellt werden, das einen sequenziell zunehmenden/abnehmenden Wert wie einen Zeitstempel enthält.

Beachten Sie, dass Firestore mithilfe der oben beschriebenen Praktiken skaliert werden kann, um beliebig große Arbeitslasten zu bedienen, ohne dass Sie eine Konfiguration anpassen müssen.

Fehlerbehebung

Firestore bietet Key Visualizer als Diagnosetool, mit dem Sie Nutzungsmuster analysieren und Heißlaufende Probleme beheben können.

Weitere Informationen