Lese- und Schreibvorgänge im großen Maßstab verstehen
In diesem Dokument finden Sie Informationen, die Ihnen helfen, fundierte Entscheidungen bei der Entwicklung Ihrer Anwendungen für hohe Leistung und Zuverlässigkeit zu treffen. Dieses Dokument enthält erweiterte Firestore-Themen. Wenn Sie gerade erst mit Firestore beginnen, lesen Sie stattdessen die Kurzanleitung.
Firestore ist eine flexible, skalierbare Datenbank für die Mobil-, Web- und Serverentwicklung über Firebase und Google Cloud. Der Einstieg in Firestore und das Schreiben leistungsstarker Anwendungen ist sehr einfach.
Damit Ihre Anwendungen auch bei zunehmender Datenbankgröße und steigendem Traffic weiterhin gut funktionieren, ist es hilfreich, die Mechanismen von Lese- und Schreibvorgängen im Firestore-Backend 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, die Sie bei der Entwicklung Ihrer Anwendung berücksichtigen sollten.
Allgemeine Komponenten
Das folgende Diagramm zeigt die übergeordneten Komponenten, die an einer Firestore API-Anfrage beteiligt sind.
Firestore-SDK und -Clientbibliotheken
Firestore unterstützt SDKs und Clientbibliotheken für verschiedene Plattformen. Eine App kann zwar direkte HTTP- und RPC-Aufrufe an die Firestore API senden, die Clientbibliotheken bieten jedoch eine Abstraktionsebene, um die API-Nutzung zu vereinfachen und Best Practices zu implementieren. Sie bieten möglicherweise auch zusätzliche Funktionen wie Offlinezugriff, Caches usw.
Google Front End (GFE)
Dieser Infrastrukturdienst gilt für alle Google Cloud-Dienste. Das GFE akzeptiert eingehende Anfragen und leitet sie an den entsprechenden Google-Dienst weiter (in diesem Fall den Firestore-Dienst). Es bietet auch andere wichtige Funktionen, darunter 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 enthält einen Speicherclient, der mit der Speicherebene für das Lesen und Schreiben von Daten interagiert.
Firestore-Speicherschicht
Die Firestore-Speicherebene ist für das Speichern von Daten und Metadaten sowie der zugehörigen Datenbankfunktionen von Firestore verantwortlich. 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 nachvollziehen.
Schlüsselbereiche und Aufteilungen
Firestore ist eine dokumentenbasierte NoSQL-Datenbank. Sie speichern Daten in Dokumenten, die in Hierarchien von Sammlungen organisiert sind. Die Sammlungshierarchie und die Dokument-ID werden für jedes Dokument in einen einzelnen Schlüssel übersetzt. Dokumente werden logisch gespeichert und lexikografisch nach diesem einzelnen Schlüssel sortiert. Der Begriff „Schlüsselbereich“ bezieht sich auf einen lexikografisch zusammenhängenden Bereich von Schlüsseln.
Eine typische Firestore-Datenbank ist zu groß, um auf einen einzelnen physischen Computer zu passen. Es gibt auch Szenarien, in denen die Arbeitslast für die Daten zu hoch ist, um von einem einzelnen Rechner verarbeitet zu werden. Zur Verarbeitung großer Arbeitslasten partitioniert Firestore die Daten in separate Teile, die auf mehreren Maschinen oder Speicherservern gespeichert und von dort bereitgestellt werden können. Diese Partitionen werden in den Datenbanktabellen in Blöcken von Schlüsselbereichen erstellt, die als Splits bezeichnet werden.
Synchrone Replikation
Die Datenbank wird immer automatisch und synchron repliziert. Die Datensplits haben Replikate in verschiedenen Zonen, damit sie auch dann verfügbar sind, wenn eine Zone nicht mehr zugänglich ist. Die konsistente Replikation in den verschiedenen Kopien des Splits wird vom Paxos-Algorithmus für den Konsens verwaltet. Ein Replikat jedes Splits wird zum Paxos-Leader bestimmt, der für die Verarbeitung von Schreibvorgängen für diesen Split verantwortlich ist. Durch die synchrone Replikation können Sie immer die aktuelle Version der Daten aus Firestore lesen.
Das Ergebnis ist ein skalierbares und hochverfügbares System, das niedrige Latenz sowohl für Lese- als auch für Schreibvorgänge bietet, unabhängig von starker Auslastung und in großem Umfang.
Datenlayout
Firestore ist eine schemalose Dokumentdatenbank. Intern werden die Daten jedoch hauptsächlich in zwei Tabellen im Stil relationaler Datenbanken in der Speicherebene angeordnet:
- Tabelle Documents: In dieser Tabelle werden Dokumente gespeichert.
- Tabelle Indexe: In dieser Tabelle werden Indexeinträge gespeichert, mit denen Ergebnisse effizient und nach Indexwert sortiert abgerufen werden können.
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.
Eine Region im Vergleich zu mehreren Regionen
Wenn Sie eine Datenbank erstellen, müssen Sie eine Region oder Multiregion auswählen.
Ein einzelner regionaler Standort ist ein bestimmter geografischer Ort, z. B. us-west1. Die Datenaufteilungen einer Firestore-Datenbank haben Replikate in verschiedenen Zonen innerhalb der ausgewählten Region, wie bereits beschrieben.
Ein multiregionaler Standort besteht aus einer definierten Gruppe von Regionen, in denen Replikate der Datenbank gespeichert werden. Bei einer multiregionalen Bereitstellung von Firestore haben zwei der Regionen vollständige Replikate aller Daten in der Datenbank. In einer dritten Region gibt es ein Zeugenreplikat, das keine vollständigen Daten enthält, aber an der Replikation teilnimmt. Durch die Replikation der Daten zwischen mehreren Regionen können Daten auch bei Verlust einer ganzen Region geschrieben und gelesen werden.
Weitere Informationen zu den Standorten einer Region finden Sie unter Firestore-Standorte.
Lebenszyklus eines Schreibvorgangs in Firestore
Ein Firestore-Client kann Daten schreiben, indem er ein einzelnes Dokument erstellt, aktualisiert oder löscht. Für einen Schreibvorgang in ein einzelnes Dokument müssen sowohl das Dokument als auch die zugehörigen Indexeinträge in der Speicherebene atomar 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-Eigenschaften (Atomarität, Konsistenz, Isolation und Langlebigkeit) von relationalen Datenbanken. Firestore bietet auch Serialisierbarkeit. Das bedeutet, dass alle Transaktionen so erscheinen, als ob sie in einer seriellen Reihenfolge ausgeführt würden.
Allgemeine Schritte bei einer Schreibtransaktion
Wenn der Firestore-Client einen Schreibvorgang ausgibt oder einen Commit einer Transaktion mit einer der oben genannten Methoden durchführt, wird dies intern als Lese-/Schreibtransaktion der Datenbank in der Speicherebene ausgeführt. Die Transaktion ermöglicht es Firestore, die oben genannten ACID-Eigenschaften bereitzustellen.
Als erster Schritt einer Transaktion liest Firestore das vorhandene Dokument und bestimmt die Änderungen, die an den Daten in der Tabelle „Documents“ vorgenommen werden sollen.
Dazu gehört auch, dass Sie die Tabelle „Indizes“ wie folgt aktualisieren:
- Für Felder, die den Dokumenten hinzugefügt werden, sind entsprechende Einfügungen in der Tabelle „Indexes“ erforderlich.
- Felder, die aus den Dokumenten entfernt werden, müssen in der Tabelle „Indexes“ entsprechend gelöscht werden.
- Für Felder, die in den Dokumenten geändert werden, sind sowohl Löschvorgänge (für alte Werte) als auch Einfügevorgänge (für neue Werte) in der Tabelle „Indexes“ erforderlich.
Zum Berechnen der oben genannten Mutationen liest Firestore die Indexierungskonfiguration für das Projekt. In der Indexierungskonfiguration werden Informationen zu den Indexen für ein Projekt gespeichert. Firestore verwendet zwei Arten von Indexen: Einzelfeldindexe und zusammengesetzte Indexe. Eine detaillierte Beschreibung der in Firestore erstellten Indexe finden Sie unter Indextypen in Firestore.
Sobald die Mutationen berechnet wurden, werden sie von Firestore in einer Transaktion zusammengefasst und dann per Commit übergeben.
Schreibtransaktion in der Speicherebene verstehen
Wie bereits erwähnt, beinhaltet ein Schreibvorgang in Firestore eine Lese-/Schreibtransaktion in der Speicherebene. Je nach Datenlayout kann ein Schreibvorgang einen oder mehrere Splits umfassen, wie im Datenlayout zu sehen ist.
Im folgenden Diagramm hat die Firestore-Datenbank acht Splits (mit 1–8 gekennzeichnet), die auf drei verschiedenen Speicherservern in einer einzelnen Zone gehostet werden. Jeder Split wird in drei(oder mehr) verschiedenen Zonen repliziert. Jeder Split hat einen Paxos-Leader, der sich für verschiedene Splits in einer anderen Zone befinden kann.
Betrachten Sie eine Firestore-Datenbank mit der Sammlung Restaurants
:
Der Firestore-Client fordert die folgende Änderung an einem Dokument in der Sammlung Restaurant
an, indem er den Wert des Felds priceCategory
aktualisiert.
Im Folgenden wird beschrieben, was beim Schreiben passiert:
- Erstellen Sie eine Lese-/Schreibtransaktion.
- Lesen Sie das Dokument
restaurant1
in der SammlungRestaurants
aus der Tabelle Documents der Speicherebene. - Lesen Sie die Indexe für das Dokument aus der Tabelle Indexe.
- Berechnen Sie die Änderungen, 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 Documents, um die Änderung des Werts des FeldspriceCategory
widerzuspiegeln. - M2 und M3: Löschen Sie die Zeilen für den alten Wert von
priceCategory
in der Tabelle Indexes 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.
- M1: Aktualisieren Sie die Zeile für
- Führen Sie ein Commit für diese Mutationen durch.
Der Speicherdienst im Firestore-Dienst sucht nach den Splits, die die Schlüssel der zu ändernden Zeilen enthalten. Nehmen wir an, Split 3 stellt M1 bereit und Split 6 stellt M2 bis M5 bereit. Es gibt eine verteilte Transaktion, an der alle diese Splits als Teilnehmer beteiligt sind. Die Teilnehmeraufteilungen können auch alle anderen Aufteilungen enthalten, aus denen zuvor im Rahmen der Lese-/Schreibtransaktion Daten gelesen wurden.
In den folgenden Schritten wird beschrieben, was im Rahmen des Commits passiert:
- Der Speicherclient gibt einen Commit aus. Der Commit enthält die Mutationen M1 bis M5.
- Split 3 und Split 6 sind die Teilnehmer an dieser Transaktion. Einer der Teilnehmer wird als Koordinator ausgewählt, z. B. Split 3. Die Aufgabe des Koordinators besteht darin, sicherzustellen, dass die Transaktion entweder für alle Teilnehmer übergeben oder individuell abgebrochen wird.
- Die Leader-Replikate dieser Splits sind für die von den Teilnehmern und Koordinatoren geleistete Arbeit verantwortlich.
- Jeder Teilnehmer und Koordinator führt einen Paxos-Algorithmus mit seinen jeweiligen Replikaten aus.
- Der Leader führt einen Paxos-Algorithmus mit den Replikaten aus. Ein Quorum wird erreicht, wenn die meisten Replikate dem Leader mit einer
ok to commit
-Antwort antworten. - Jeder Teilnehmer benachrichtigt dann den Koordinator, wenn er bereit ist (erste Phase des zweiphasigen Commit). Wenn ein Teilnehmer den Commit der Transaktion nicht durchführen kann, wird die gesamte Transaktion
aborts
.
- Der Leader führt einen Paxos-Algorithmus mit den Replikaten aus. Ein Quorum wird erreicht, wenn die meisten Replikate dem Leader mit einer
- Sobald der Koordinator weiß, dass alle Teilnehmer, einschließlich er selbst, bereit sind, teilt er allen Teilnehmern das Transaktionsergebnis
accept
mit (zweite Phase des Two-Phase-Commit). In dieser Phase zeichnet jeder Teilnehmer die Commit-Entscheidung an einem stabilen Speicherplatz auf und die Transaktion wird übergeben. - Der Coordinator antwortet dem Speicherclient in Firestore, dass die Transaktion übergeben wurde. Parallel dazu wenden der Koordinator und alle Teilnehmer die Mutationen auf die Daten an.
Wenn die Firestore-Datenbank klein ist, kann es vorkommen, dass ein einzelner Split alle Schlüssel in den Mutationen M1–M5 enthält. In diesem 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 in mehreren Regionen
Bei einer Bereitstellung in mehreren Regionen erhöht die Verteilung von Replikaten auf Regionen die Verfügbarkeit, geht aber mit Leistungseinbußen einher. Die Kommunikation zwischen Replikaten in verschiedenen Regionen dauert länger. Daher ist die Baseline-Latenz für Firestore-Vorgänge im Vergleich zu Bereitstellungen in einer einzelnen Region etwas höher.
Wir konfigurieren die Replikate so, dass die Führung für Splits immer in der primären Region bleibt. Die primäre Region ist die Region, aus 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 für 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.
Lebenszyklus eines Lesevorgangs in Firestore
In diesem Abschnitt werden eigenständige, nicht in Echtzeit ausgeführte Lesevorgänge in Firestore behandelt. Intern verarbeitet der Firestore-Server die meisten dieser Anfragen in zwei Hauptphasen:
- Ein einzelner Bereichsscan der Tabelle Indexes
- Punkt-Lookups in der Tabelle Dokumente basierend auf dem Ergebnis des vorherigen Scans
Die Datenlesevorgänge aus der Speicherebene werden intern mit einer Datenbanktransaktion ausgeführt, um konsistente Lesevorgänge zu gewährleisten. Im Gegensatz zu den für Schreibvorgänge verwendeten Transaktionen werden für diese Transaktionen jedoch keine Sperren gesetzt. Stattdessen wird ein Zeitstempel ausgewählt und alle Lesevorgänge werden zu diesem Zeitstempel ausgeführt. Da sie keine Sperren erhalten, blockieren sie keine gleichzeitigen Lese-Schreib-Transaktionen. Um diese Transaktion auszuführen, gibt der Speicherclient in Firestore eine Zeitstempelgrenze an, die der Speicherschicht mitteilt, wie ein Lesezeitstempel ausgewählt werden soll. Der Typ der Zeitstempelgrenze, die vom Speicherclient in Firestore ausgewählt wird, wird durch die Leseoptionen für die Leseanforderung bestimmt.
Lesetransaktion in der Speicherebene verstehen
In diesem Abschnitt werden die verschiedenen Arten von Lesevorgängen und deren Verarbeitung in der Speicherebene in Firestore beschrieben.
Starke Lesevorgänge
Standardmäßig sind Firestore-Lesevorgänge strikt konsistent. Diese starke Konsistenz bedeutet, dass ein Firestore-Lesevorgang die aktuelle Version der Daten zurückgibt, die alle Schreibvorgänge widerspiegelt, die bis zum Beginn des Lesevorgangs mit einem Commit festgeschrieben wurden.
Lesevorgang mit einer Aufteilung
Der Speicherclient in Firestore sucht nach den Splits, die die Schlüssel der zu lesenden Zeilen enthalten. Angenommen, es muss einen Lesevorgang aus Split 3 aus dem vorherigen Abschnitt durchführen. Der Client sendet die Leseanfrage an das nächstgelegene Replikat, um die Roundtrip-Latenz zu verringern.
An diesem Punkt können je nach ausgewähltem Replikat die folgenden Fälle eintreten:
- Die Leseanfrage wird an ein Leader-Replikat gesendet (Zone A).
- Da der Leader immer aktuell ist, kann der Lesevorgang direkt ausgeführt werden.
- Die Leseanforderung wird an ein Replikat gesendet, das nicht als Leader fungiert (z. B. Zone B).
- Split 3 weiß möglicherweise anhand seines internen Status, dass er genügend Informationen hat, um den Lesevorgang zu verarbeiten, und tut dies.
- Split 3 ist nicht sicher, ob es die aktuellen Daten gesehen hat. Es sendet eine Nachricht an den Leader, um nach dem Zeitstempel der letzten Transaktion zu fragen, die es zum Verarbeiten des Lesevorgangs ausführen muss. Sobald diese Transaktion angewendet wurde, kann der Lesevorgang fortgesetzt werden.
Firestore gibt die Antwort dann an den Client zurück.
Lesevorgänge mit mehreren Splits
Wenn die Lesevorgänge aus mehreren Splits erfolgen müssen, wird derselbe Mechanismus für alle Splits angewendet. Sobald die Daten aus allen Splits zurückgegeben wurden, werden die Ergebnisse vom Speicherclient in Firestore zusammengeführt. Firestore antwortet dann mit diesen Daten auf den Client.
Veraltete Lesevorgänge
Strikte Lesevorgänge sind der Standardmodus in Firestore. Dies kann jedoch zu einer potenziell höheren Latenz führen, da möglicherweise eine Kommunikation mit dem Leader erforderlich ist. Oft muss Ihre Firestore-Anwendung nicht die neueste Version der Daten lesen und die Funktionalität funktioniert auch mit Daten, die einige Sekunden alt sind.
In diesem Fall kann der Client mit den read_time
-Leseoptionen veraltete Lesevorgänge erhalten. In diesem Fall werden Lesevorgänge so ausgeführt, wie die Daten am read_time
waren. Das nächste Replikat hat die Daten am angegebenen read_time
höchstwahrscheinlich bereits überprüft.
Für eine deutlich bessere Leistung sind 15 Sekunden ein angemessener Veralterungswert. Auch bei veralteten Lesevorgängen sind die ausgegebenen Zeilen miteinander konsistent.
Hotspots vermeiden
Die Splits in Firestore werden automatisch in kleinere Teile zerlegt, um die Arbeit der Bereitstellung von Traffic bei Bedarf oder bei einer Erweiterung des Schlüsselbereichs auf mehr Speicherserver zu verteilen. Splits, die zur Bewältigung von übermäßigem Traffic erstellt wurden, werden etwa 24 Stunden lang beibehalten, auch wenn der Traffic nachlässt. Bei wiederkehrenden Traffic-Spitzen bleiben die Aufteilungen also erhalten und es werden bei Bedarf weitere eingeführt. Diese Mechanismen helfen Firestore-Datenbanken, bei steigender Traffic-Last oder Datenbankgröße automatisch zu skalieren. Es gibt jedoch einige Einschränkungen, die unten erläutert werden.
Das Aufteilen von Speicher und Last dauert eine Weile. Wenn Sie den Traffic zu schnell steigern, kann es zu einer hohen Latenz oder zu Fehlern wegen überschrittener Frist kommen, die häufig als Hotspots bezeichnet werden, während der Dienst sich anpasst. Als Best Practice wird empfohlen, Vorgänge über den Schlüsselbereich zu verteilen und den Traffic für eine Sammlung in einer Datenbank mit 500 Vorgängen pro Sekunde zu erhöhen. Erhöhen Sie nach dieser schrittweisen Steigerung den Traffic alle fünf Minuten um bis zu 50 %. Dieser Prozess wird als 500/50/5-Regel bezeichnet und sorgt dafür, dass die Datenbank optimal skaliert werden kann, um Ihre Arbeitslast zu bewältigen.
Splits werden zwar automatisch bei steigender Last erstellt, Firestore kann einen Schlüsselbereich jedoch nur so lange aufteilen, bis ein einzelnes Dokument über einen dedizierten Satz replizierter Speicherserver bereitgestellt wird. Daher kann es bei einer großen und anhaltenden Anzahl gleichzeitiger Vorgänge für ein einzelnes Dokument zu einem Hotspot für dieses Dokument kommen. Wenn Sie bei einem einzelnen Dokument dauerhaft hohe Latenzen feststellen, sollten Sie Ihr Datenmodell so ändern, dass die Daten auf mehrere Dokumente aufgeteilt oder repliziert werden.
Konfliktfehler treten auf, wenn mehrere Vorgänge gleichzeitig versuchen, dasselbe Dokument zu lesen und/oder zu schreiben.
Ein weiterer Sonderfall von Hotspotting tritt auf, wenn ein sequenziell ansteigender/absteigender Schlüssel als Dokument-ID in Firestore verwendet wird und eine beträchtlich hohe Anzahl von Vorgängen pro Sekunde erfolgt. Das Erstellen weiterer Splits hilft hier nicht, da die Trafficspitze einfach in den neu erstellten Split verschoben wird. Da Firestore standardmäßig alle Felder im Dokument automatisch indexiert, können solche sich bewegenden Hotspots auch im Indexbereich für ein Dokumentfeld erstellt werden, das einen sequenziell steigenden oder fallenden Wert wie einen Zeitstempel enthält.
Wenn Sie die oben beschriebenen Best Practices befolgen, kann Firestore beliebig große Arbeitslasten bewältigen, ohne dass Sie die Konfiguration anpassen müssen.
Fehlerbehebung
Firestore bietet Key Visualizer als Diagnosetool zur Analyse von Nutzungsmustern und zur Fehlerbehebung bei Hotspotting-Problemen.