Best Practices

Sie können die hier aufgeführten Best Practices als Kurzreferenz dafür verwenden, was beim Erstellen einer Anwendung, die Firestore im Datastore-Modus verwendet, zu beachten ist. Wenn Sie den Cloud Datastore-Modus das erste Mal verwenden, ist diese Seite möglicherweise nicht der richtige Ausgangspunkt, weil sie nicht die Grundlagen der Arbeit mit dem Datastore-Modus vermittelt. Wenn Sie ein neuer Nutzer sind, empfehlen wir Ihnen, mit Einstieg in Firestore im Datastore-Modus zu beginnen.

Allgemein

  • Verwenden Sie immer UTF-8-Zeichen für Namespace-Namen, Artnamen, Attributnamen und benutzerdefinierte Schlüsselnamen. Nicht-UTF-8-Zeichen in diesen Namen können die Funktionalität des Datastore-Modus beeinträchtigen. Beispiel: Ein Nicht-UTF-8-Zeichen in einem Attributnamen kann das Erstellen eines Index verhindern, der das Attribut verwendet.
  • Verwenden Sie keinen Schrägstrich (/) in Artnamen oder benutzerdefinierten Schlüsselnamen. Schrägstriche in diesen Namen könnten die zukünftige Funktionalität beeinträchtigen.
  • Speichern Sie keine vertraulichen Informationen in einer Cloud-Projekt-ID. Eine Cloud-Projekt-ID kann über die Lebensdauer Ihres Projekts hinaus beibehalten werden.

API-Aufrufe

  • Verwenden Sie anstelle von einzelnen Vorgängen Batch-Vorgänge für Lese-, Schreib- und Löschvorgänge. Batch-Vorgänge sind effizienter, weil mit ihnen der Overhead für mehrere Vorgänge derselbe ist wie für einen einzelnen Vorgang.
  • Wenn eine Transaktion fehlschlägt, müssen Sie ein Rollback der Transaktion durchführen. Das Rollback minimiert die Wiederholungslatenz für eine andere Anfrage, die dieselben Ressourcen in einer Transaktion verwendet. Beachten Sie, dass das Rollback selbst fehlschlagen kann und daher nur ein Best-Effort-Versuch sein sollte.
  • Verwenden Sie sofern verfügbar asynchrone Aufrufe anstelle von synchronen Aufrufen. Asynchrone Aufrufe minimieren die Auswirkung auf die Latenz. Beispiel: Sie haben eine Anwendung, die für ihre Antwort das Ergebnis eines synchronen lookup() und die Ergebnisse einer Abfrage benötigt. Wenn lookup() und die Abfrage keine Datenabhängigkeit haben, muss nicht synchron auf den Abschluss von lookup() gewartet werden, bevor die Abfrage initiiert wird.

Entitäten

  • Schließen Sie dieselbe Entität (nach Schlüssel) nicht mehrmals in denselben Commit-Vorgang ein. Dies könnte sich auf die Latenz auswirken.

  • Weitere Informationen finden Sie im Abschnitt zu Aktualisierungen einer Entität.

Schlüssel

  • Schlüsselnamen werden automatisch generiert, wenn sie nicht bei der Entitätserstellung bereitgestellt wurden. Sie werden so zugewiesen, dass sie im Schlüsselraum gleichmäßig verteilt sind.
  • Verwenden Sie für einen Schlüssel mit einem benutzerdefinierten Namen immer UTF-8-Zeichen, jedoch nicht den Schrägstrich (/). Nicht-UTF-8-Zeichen beeinträchtigen verschiedene Prozesse, wie das Importieren einer Cloud Datastore-Modus-Exportdatei in BigQuery. Ein Schrägstrich könnte die zukünftige Funktionalität beeinträchtigen.
  • Bei einem Schlüssel, der eine numerische ID verwendet:
    • Verwenden Sie keine negative Zahl für die ID. Eine negative ID könnte die Sortierung beeinträchtigen.
    • Verwenden Sie nicht den Wert 0(null) als ID. In diesem Fall wird automatisch eine ID zugewiesen.
    • Wenn Sie den erstellten Entitäten manuell eigene numerische IDs zuweisen möchten, fordern Sie über Ihre Anwendung mit der Methode allocateIds() einen Block von IDs an. Dadurch wird verhindert, dass der Datastore-Modus eine Ihrer manuellen numerischen IDs einer anderen Entität zuweist.
  • Wenn Sie den Entitäten, die Sie erstellen, Ihre eigene manuelle numerische ID oder einen eigenen benutzerdefinierten Namen zuweisen, verwenden Sie keine kontinuierlich ansteigenden Werte wie:

    1, 2, 3, …,
    "Customer1", "Customer2", "Customer3", ….
    "Product 1", "Product 2", "Product 3", ….
    

    Wenn eine Anwendung sehr viel Traffic erzeugt, könnte eine derartige sequenzielle Nummerierung zu Engpässen führen, die die Latenz des Datastore-Modus beeinträchtigen. Um das Problem sequenzieller numerischer IDs zu vermeiden, fordern Sie die numerischen IDs mit der Methode allocateIds() an. Die Methode allocateIds() generiert gut verteilte Sequenzen von numerischen IDs.

  • Wenn Sie einen Schlüssel angeben oder den generierten Namen speichern, können Sie später einen lookup()-Vorgang für diese Entität ausführen, ohne dass eine Abfrage zum Auffinden der Entität erforderlich ist.

Indexe

Attribute

  • Verwenden Sie immer UTF-8-Zeichen für Attribute vom Typ String. Ein Nicht-UTF-8-Zeichen in einem Attribut vom Typ String könnte Abfragen beeinträchtigen. Wenn Sie Daten mit Nicht-UTF-8-Zeichen speichern müssen, verwenden Sie einen Bytestring.
  • Verwenden Sie in Attributnamen keine Punkte. Punkte in Attributnamen beeinträchtigen die Indexierung von Attributen eingebetteter Entitäten.

Abfragen

  • Wenn Sie nur auf den Schlüssel von Abfrageergebnissen zugreifen müssen, verwenden Sie eine ausschließlich schlüsselbasierte Abfrage. Eine ausschließlich schlüsselbasierte Abfrage gibt Ergebnisse mit einer niedrigeren Latenz und niedrigeren Kosten zurück als der Abruf ganzer Entitäten.
  • Wenn Sie nur auf bestimmte Attribute aus einer Entität zugreifen müssen, verwenden Sie eine Projektionsabfrage. Eine Projektionsabfrage gibt Ergebnisse mit einer niedrigeren Latenz und niedrigeren Kosten zurück als der Abruf ganzer Entitäten.
  • Verwenden Sie ebenfalls eine Projektionsabfrage, wenn Sie nur die Attribute im Abfragefilter benötigen, beispielsweise in einer Klausel des Typs order by.
  • Verwenden Sie keine Offsets. Verwenden Sie stattdessen cursors. Die Verwendung eines Offsets vermeidet nur die Rückgabe der übersprungenen Entitäten an Ihre Anwendung, diese Entitäten werden jedoch weiterhin intern abgerufen. Die übersprungenen Entitäten wirken sich auf die Latenz der Abfrage aus. Außerdem werden Ihrer Anwendung die Lesevorgänge in Rechnung gestellt, die für deren Abruf erforderlich sind.

Skalierbares Programmdesign

In den folgenden Best Practices wird beschrieben, wie Sie Situationen vermeiden, die zu Konflikten führen können.

Aktualisierungen einer Entität

Überlegen Sie beim Entwerfen Ihrer App, wie schnell einzelne Entitäten aktualisiert werden. Die beste Möglichkeit, die Leistung Ihrer Arbeitslast zu charakterisieren, sind Lasttests. Die genaue maximale Rate, mit der eine Anwendung eine einzelne Entität aktualisieren kann, hängt stark von der Arbeitslast ab. Zu den Faktoren gehören die Schreibrate, Konflikte zwischen Anfragen und die Anzahl der betroffenen Indexe.

Ein Schreibvorgang für Entitäten aktualisiert die Entität und alle zugehörigen Indexe und Firestore im Datastore-Modus wendet den Schreibvorgang synchron auf ein Quorum von Replikaten an. Wenn die Schreibraten hoch genug sind, treten in der Datenbank Konflikte, eine höhere Latenz oder andere Fehler auf.

Hohe Lese-/Schreibraten bei einem schmalen Schlüsselbereich

Vermeiden Sie hohe Lese- oder Schreibraten bei Dokumenten, die lexikografisch eng beieinanderliegen. Andernfalls treten bei Ihrer Anwendung Konfliktfehler auf. Dieses Problem wird als Heißlaufen bezeichnet. Dazu kann es bei Ihrer Anwendung kommen, wenn sie eine der folgenden Aktionen ausführt:

  • Sie erstellt neue Entitäten mit einer sehr hohen Rate und ordnet ihre eigenen monoton ansteigenden IDs zu.

    Der Datastore-Modus ordnet Schlüssel mithilfe eines verteilten Algorithmus zu. Es sollte bei Schreibvorgängen nicht zu einem Heißlaufen kommen, wenn Sie neue Entitäten mit automatischer Entitäts-ID-Zuordnung erstellen.

  • Sie erstellt mit einer sehr hohen Rate neue Entitäten mithilfe der alten sequenziellen ID-Zuordnungsrichtlinie.

  • Sie erstellt neue Entitäten mit hoher Rate für eine Art mit wenigen Entitäten.

  • Sie erstellt neue Entitäten mit einem indexierten und monoton ansteigenden Attributwert, z. B. einem Zeitstempel, mit einer sehr hohen Rate.

  • Sie löscht Entitäten aus einer Art mit hoher Rate.

  • Sie schreibt mit einer sehr hohen Rate in die Datenbank, ohne den Traffic nach und nach zu erhöhen.

Wenn die Schreibrate auf einen kleinen Schlüsselbereich plötzlich ansteigt, kann es aufgrund eines Hotspots zu langsamen Schreibvorgängen kommen. Im Datastore-Modus wird der Schlüsselbereich letztendlich aufgeteilt, um die hohe Last zu bewältigen.

Der Grenzwert für Lesevorgänge ist im Allgemeinen wesentlich höher als der für Schreibvorgänge, es sei denn, Sie lesen mit einer hohen Rate aus einem einzelnen Schlüssel.

Hotspots können bei Schlüsselbereichen auftreten, die sowohl von Entitätsschlüsseln als auch von Indexen verwendet werden.

In einigen Fällen kann ein Hotspot eine Anwendung auf Arten beeinträchtigen, die über das Verhindern von Lese- oder Schreibvorgängen in einem kleinen Schlüsselbereich hinausgehen. Beispiel: Die Hot-Schlüssel könnten beim Hochfahren der Instanz gelesen oder geschrieben werden, wodurch das Laden von Anfragen fehlschlägt.

Bei einem Schlüssel oder einem indexierten, kontinuierlich erhöhten Attribut können Sie einen zufälligen Hash-Wert voranstellen, damit die Schlüssel auch tatsächlich in mehrere Tabellenreihen fragmentiert werden.

Wenn eine Abfrage für ein monoton ansteigendes oder abnehmendes Attribut mit Sortierung oder Filter erforderlich ist, könnten Sie stattdessen für ein neues Attribut indexieren. Versehen Sie dafür den monotonen Wert mit einem Wert als Präfix, der über das Dataset hinweg eine hohe Kardinalität aufweist, den aber alle von Ihrer geplanten Abfrage betroffenen Entitäten gemeinsam haben. Wenn Sie beispielsweise in Ihrer Abfrage Einträge anhand ihres Zeitstempels suchen möchten, aber jeweils nur die Ergebnisse für einen einzelnen Nutzer zurückgegeben werden sollen, können Sie dem Zeitstempel die Nutzer-ID voranstellen und dieses neue Attribut indexieren. Dies würde Abfragen und sortierte Ergebnisse für diesen Nutzer weiterhin zulassen, aber durch die Nutzer-ID wäre sichergestellt, dass der Index selbst gut fragmentiert ist.

Traffic erhöhen

Erhöhen Sie schrittweise den Traffic an neue Typen oder Teile des Schlüsselraums.

Sie sollten den Traffic an neue Typen schrittweise erhöhen, damit Firestore im Datastore-Modus ausreichend Zeit hat, sich auf den erhöhten Traffic vorzubereiten. Wir empfehlen einen Höchstwert von 500 Vorgängen pro Sekunde bei einer neuen Art. Danach kann der Traffic alle 5 Minuten um 50 % erhöht werden. Theoretisch können Sie den Traffic mit diesem Erhöhungsplan in 90 Minuten auf 740.000 Vorgänge ausbauen. Stellen Sie sicher, dass Schreibvorgänge in dem gesamten Schlüsselbereich relativ gleichmäßig verteilt sind. Unsere SREs (Site Reliability Engineers) nennen dies die "500/50/5"-Regel.

Dieses Muster des schrittweisen Anstiegs ist besonders wichtig, wenn Sie Ihren Code von der Verwendung von Art A auf die Verwendung von Art B umstellen. Eine einfache Möglichkeit für eine solche Migration besteht darin, den Code so zu ändern, dass er Art B und, wenn diese nicht vorhanden ist, Art A liest. Dies könnte jedoch zu einem plötzlichen Anstieg der Zugriffe auf eine neue Art mit einem sehr kleinen Teil des Schlüsselraums führen.

Dasselbe Problem kann auch auftreten, wenn Sie die Entitäten so migrieren, dass ein anderer Schlüsselbereich innerhalb derselben Art verwendet wird.

Die Strategie, die Sie zur Migration der Entitäten zu einer neuen Art oder einem neuen Schlüssel verwenden, hängt von Ihrem Datenmodell ab. Unten sehen Sie eine Beispielstrategie, die als "parallele Lesevorgänge" bezeichnet wird. Sie müssen feststellen, ob diese Strategie für Ihre Daten passend ist. Eine wichtige Überlegung ist die Auswirkung von Parallelvorgängen während der Migration auf die Kosten.

Lesen Sie zuerst aus der alten Entität oder dem alten Schlüssel. Wenn diese nicht vorhanden sind, könnten Sie aus der neuen Entität oder dem neuen Schlüssel lesen. Eine hohe Rate von Lesevorgängen für nicht vorhandene Entitäten kann zu Engpässen führen, sodass Sie die Belastung in jedem Fall nach und nach erhöhen müssen. Es ist besser, die alte Entität in die neue Entität zu kopieren und dann die alte Entität zu löschen. Erhöhen Sie parallele Lesevorgänge nach und nach, um sicherzustellen, dass der neue Schlüsselraum ordnungsgemäß aufgeteilt wird.

Eine Möglichkeit zur schrittweisen Erhöhung von Lese- oder Schreibvorgängen für eine neue Art ist die Verwendung eines deterministischen Hash-Werts der Nutzer-ID, um einen zufälligen Prozentsatz aller Nutzer zu erhalten, die neue Entitäten schreiben. Sie müssen sicherstellen, dass das Ergebnis des Nutzer-ID-Hashwerts nicht durch die Zufallsfunktion oder durch Nutzerverhalten verzerrt wird.

Führen Sie in der Zwischenzeit einen Dataflow-Job aus, um alle Daten aus den alten Entitäten oder Schlüsseln in die neuen zu kopieren. Der Batch-Job sollte Schreibvorgänge in sequenzielle Schlüssel vermeiden, um Hotspots zu verhindern. Wenn der Batch-Job abgeschlossen ist, können Sie nur aus dem neuen Speicherort lesen.

Diese Strategie kann noch verfeinert werden, indem kleine Batches von Nutzern gleichzeitig migriert werden. Fügen Sie der Nutzerentität ein Feld hinzu, mit dem der Migrationsstatus dieses Nutzers verfolgt wird. Wählen Sie einen Batch von zu migrierenden Nutzern basierend auf einem Hash der Nutzer-ID aus. Ein MapReduce- oder Dataflow-Job migriert die Schlüssel für diesen Batch von Nutzern. Die Nutzer, für die eine Migration läuft, verwenden parallele Lesevorgänge.

Beachten Sie, dass ein Rollback nur problemlos durchgeführt werden kann, wenn Sie während der Migrationsphase doppelte Schreibvorgänge für die alten und neuen Entitäten vornehmen. Dies würde die anfallenden Kosten für den Datastore-Modus erhöhen.

Löschvorgänge

Vermeiden Sie das Löschen einer großen Anzahl von Entitäten in einem kleinen Schlüsselbereich.

Firestore im Datastore-Modus schreibt seine Tabellen in regelmäßigen Abständen neu, um gelöschte Einträge zu entfernen und die Daten neu anzuordnen, damit die Lese- und Schreibvorgänge effizienter werden. Dieser Prozess wird als Verdichtung bezeichnet.

Wenn Sie eine große Anzahl von Entitäten des Datastore-Modus in einem kleinen Schlüsselbereich löschen, sind Abfragen innerhalb dieses Teils des Index bis zum Abschluss der Verdichtung langsamer. In extremen Fällen können die Abfragen das Zeitlimit überschreiten, bevor Ergebnisse zurückgegeben werden.

Die Verwendung eines Zeitstempelwerts für ein indexiertes Feld zur Darstellung der Ablaufzeit der Entität ist eine ungünstige Strategie, die Sie vermeiden sollten. Um abgelaufene Entitäten abzurufen, müssen Sie eine Abfrage für dieses indexierte Feld durchführen, das wahrscheinlich in einem überlappenden Teil des Schlüsselraums mit Indexeinträgen für die zuletzt gelöschten Entitäten liegt.

Sie können die Leistung mit "fragmentierten Abfragen" verbessern, die dem Ablaufzeitstempel einen String mit fester Länge voranstellen. Der Index wird nach dem vollständigen String sortiert, sodass Entitäten mit demselben Zeitstempel im gesamten Schlüsselbereich des Index gefunden werden. Sie führen mehrere Abfragen parallel durch, um Ergebnisse aus jedem Shard abzurufen.

Eine umfassendere Lösung für das Problem des Ablaufzeitstempels besteht in der Verwendung einer "Generierungsnummer", bei der es sich um einen globalen Zähler handelt, der regelmäßig aktualisiert wird. Die Generierungsnummer wird dem Ablaufzeitstempel vorangestellt, sodass Abfragen nach Generierungsnummer, dann nach Shard und schließlich nach Zeitstempel sortiert werden. Das Löschen von alten Entitäten erfolgt in einer früheren Generierung. Für jede nicht gelöschte Entität muss die Generierungsnummer erhöht werden. Nachdem der Löschvorgang abgeschlossen ist, gehen Sie weiter zur nächsten Generierung. Die Leistung von Abfragen für eine ältere Generierung ist bis zum Abschluss der Verdichtung schwach. Sie müssen möglicherweise auf den Abschluss einiger Generierungen warten, bevor Sie den Index abfragen können, um eine Liste der zu löschenden Entitäten zu erhalten. Dies ist notwendig, um das Risiko fehlender Ergebnisse wegen Eventual Consistency zu vermeiden.

Fragmentierung und Replikation

Verwenden Sie für den Umgang mit Hotspots Fragmentierung oder Replikation.

Sie können Replikation verwenden, wenn Sie in einen Teil des Schlüsselbereichs mit einer höheren Rate schreiben müssen, als Firestore im Datastore-Modus zulässt. Mit dieser Strategie würden Sie n Kopien derselben Entität speichern, was eine n-mal höhere Rate von Lesevorgängen erlaubt, als von einer einzelnen Entität unterstützt wird.

Sie können die Fragmentierung verwenden, wenn Sie in einen Teil des Schlüsselbereichs mit einer höheren Rate schreiben müssen, als Firestore im Datastore-Modus zulässt. Bei der Fragmentierung wird eine Entität in kleinere Teile aufgeteilt.

Einige häufige Fehler bei der Fragmentierung sind etwa:

  • Fragmentierung mit einem Zeitpräfix: Wenn die Zeit zum nächstem Präfix verschoben wird, wird der neue nicht aufgeteilte Teil zu einem Engpass. Stattdessen sollten Sie einen Teil der Schreibvorgänge schrittweise zu dem neuen Präfix verschieben.

  • Nur die wichtigsten Entitäten werden fragmentiert: Wenn Sie einen kleinen Teil der Gesamtanzahl von Entitäten fragmentieren, sind möglicherweise nicht genügend Zeilen zwischen den Hot-Entitäten vorhanden, um sicherzustellen, dass sie in unterschiedlichen Aufteilungen bleiben.

Weiteres Vorgehen