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 Firestore zum ersten Mal verwenden, lesen Sie die Kurzanleitung.
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.
SDKs, Clientbibliotheken und Treiber
Firestore unterstützt SDKs, Clientbibliotheken und Treiber für verschiedene Plattformen.
Google Front End (GFE)
Dies ist ein Infrastrukturdienst, der für alle Google Cloud -Dienste gilt. Das GFE akzeptiert eingehende Anfragen und leitet sie an den entsprechenden Google-Dienst weiter (in diesem Fall den Firestore-Dienst).
Firestore-Dienst
Der Firestore-Dienst führt Prüfungen der API-Anfrage durch, einschließlich Authentifizierung, Autorisierung und Kontingentprüfungen, und verwaltet auch Transaktionen. Dieser Firestore-Dienst umfasst 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 werden, 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 Sammlungen organisiert sind. Der Sammlungsname und die Dokument-ID bilden einen eindeutigen Schlüssel für ein Dokument. Dokumente in derselben Sammlung werden zusammen im Keyspace gespeichert. Innerhalb dieses Keyspace wird die Dokument-ID gehasht. Der Begriff Schlüsselbereich bezieht sich auf einen zusammenhängenden Bereich von Schlüsseln im Speicher.
Firestore partitioniert Daten in Sammlungen automatisch auf mehrere Speicherserver. Diese Partitionen werden als Splits bezeichnet.
Aus Dokumenten können Indexeinträge generiert werden, die lexikografisch sortiert sind und auf dieselbe Weise wie die Dokumentdaten aufgeteilt und platziert werden.
Synchrone Replikation
Jeder Schreibvorgang wird synchron mit Paxos auf eine Mehrheit der Replikate repliziert. Ein Replikat pro Split gilt als Leader und koordiniert den Replikationsprozess. Im Falle eines Leader-Fehlers wird ein neuer Leader ausgewählt. Replikate befinden sich in verschiedenen Zonen, um gegen potenzielle Zonenausfälle resistent zu sein. Das Ergebnis ist ein skalierbares und hochverfügbares System, das unabhängig von starken Arbeitslasten und in großem Umfang niedrige Latenzen sowohl für Lese- als auch für Schreibvorgänge bietet.
Eine Region im Vergleich zu mehreren Regionen
Wenn Sie eine Datenbank erstellen, müssen Sie einen einzelnen regionalen Standort oder einen multiregionalen Standort 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 Standort mit mehreren Regionen besteht aus einer definierten Gruppe von Regionen, in denen Firestore Replikate der Datenbank speichert. Bei einer multiregionalen Bereitstellung von Firestore gibt es in zwei 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. Daten können auch bei Verlust einer ganzen Region geschrieben und gelesen werden, da Firestore die Daten zwischen mehreren Regionen repliziert.
Weitere Informationen zu den Standorten einer Region finden Sie unter Firestore-Standorte.
Lebenszyklus eines Schreibvorgangs
Ein Treiber 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 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 Treiber einen Schreibvorgang ausgibt oder einen Commit für eine Transaktion mit einer der oben genannten Methoden ausführt, wird dies intern als Lese-/Schreibtransaktion für die Datenbank in der Speicherebene ausgeführt. Die Transaktion ermöglicht es Firestore, die oben genannten ACID-Attribute bereitzustellen.
Im ersten Schritt einer Transaktion liest Firestore das vorhandene Dokument und ermittelt die Mutationen, die an den Daten im Dokument vorgenommen werden sollen.
Dazu gehört auch das Aktualisieren aller relevanten Indexe:
- Für indexierte Felder, die den Dokumenten hinzugefügt werden, sind entsprechende Einfügungen in die Indexe erforderlich.
- Für indexierte Felder, die aus den Dokumenten entfernt werden, müssen entsprechende Löschvorgänge in den Indexen erfolgen.
- Für indexierte Felder, die in den Dokumenten geändert werden, sind sowohl Löschvorgänge (für alte Werte) als auch Einfügungen (für neue Werte) in den Indexen 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.
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, ist für einen Schreibvorgang in Firestore eine Lese-/Schreibtransaktion in der Speicherebene erforderlich. Je nach Datenlayout kann ein Schreibvorgang einen oder mehrere Splits umfassen.
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 Treiber 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
restaurant1
-Dokument in derRestaurants
-Sammlung. - Lesen Sie die Indexe für das Dokument.
- 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
, um die Änderung des Werts des FeldspriceCategory
zu berücksichtigen. - M2 und M3: Löschen Sie die alten Indexeinträge für
priceCategory
. - M4 und M5: Fügen Sie neue Indexeinträge für
priceCategory
hinzu.
- 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 Koordinator 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.
Lebenszyklus eines Lesevorgangs
In diesem Abschnitt geht es um Lesevorgänge in Firestore. Abfragen bestehen insbesondere aus einer Mischung aus Dokument- und Indexeintraglesevorgängen.
Die Datenlesevorgänge aus der Speicherebene werden intern mithilfe 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 abrufen, blockieren sie keine gleichzeitigen Lese-Schreib-Transaktionen. Um diese Transaktion auszuführen, gibt der Speicherclient in Firestore eine Zeitstempelgrenze an, die der Speicherebene mitteilt, wie ein Lesezeitstempel ausgewählt werden soll. Der Typ der Zeitstempelgrenze, die vom Speicherclient in Firestore ausgewählt wird, hängt von den Leseoptionen für die Leseanfrage ab.
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 aus Split 3 aus dem vorherigen Abschnitt gelesen werden. Der Client sendet die Leseanfrage an das nächstgelegene Replikat, um die Roundtrip-Latenz zu verringern.
An dieser Stelle 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.
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 bei Bedarf werden 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 Sie beachten sollten.
Das Aufteilen von Speicher und Last dauert einige Zeit. 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. Es empfiehlt sich, Vorgänge über den Schlüsselbereich zu verteilen und den Traffic für eine Sammlung in einer Datenbank schrittweise zu erhöhen.
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 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 zu schreiben.
Wenn Sie die auf dieser Seite beschriebenen Best Practices befolgen, kann Firestore beliebig große Arbeitslasten bewältigen, ohne dass Sie die Konfiguration anpassen müssen.