Best Practices für Cloud Datastore

Sie können die hier aufgeführten Best Practices als schnelle Referenz dafür verwenden, was beim Erstellen von Anwendungen, die Datastore verwenden, zu beachten ist. Wenn Sie gerade erst mit Datastore beginnen, ist diese Seite möglicherweise nicht der beste Ausgangspunkt, da sie nicht die Grundlagen zur Verwendung von Datastore vermittelt. Wenn Sie Datastore noch nicht gut kennen, sollten Sie mit dem Einstieg in Datastore 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 Funktion von Datastore 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.
  • Als Best Practice für die Datencompliance empfehlen wir, keine vertraulichen Informationen in Datastore-Entitätsnamen oder Entitätsattributnamen zu speichern.

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.

Entities

  • Gruppieren Sie eng zusammenhängende Daten in Entitätengruppen. Entitätengruppen ermöglichen Ancestor-Abfragen, die strikt konsistente Ergebnisse zurückgeben. Ancestor-Abfragen scannen eine Entitätengruppe außerdem schnell mit minimaler E/A, weil die Entitäten in einer Entitätengruppe eng beieinander auf Datastore-Servern gespeichert sind.
  • Vermeiden Sie es, mehr als einmal pro Sekunde in eine Entitätengruppe zu schreiben. Wenn mit einer kontinuierlichen Rate über diesem Grenzwert geschrieben wird, dauert es bei Lesevorgängen mit Eventual Consistency länger, bis letztendlich Konsistenz erreicht wird. Bei strikt konsistenten Lesevorgängen kann es zu Zeitüberschreitungen kommen. Ihre Anwendung reagiert insgesamt langsamer. Für diesen Grenzwert zählt ein Batch- oder transaktionaler Schreibvorgang in eine Entitätengruppe nur als einzelner Schreibvorgang.
  • Schließen Sie dieselbe Entität (nach Schlüssel) nicht mehrmals in denselben Commit-Vorgang ein. Wenn Sie dieselbe Entität mehrmals in dasselbe Commit einfügen, kann sich dies auf die Datastore-Latenz auswirken.

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, der einen benutzerdefinierten Namen verwendet, immer UTF-8-Zeichen mit Ausnahme des Schrägstrichs (/). Nicht-UTF-8-Zeichen beeinträchtigen verschiedene Prozesse, z. B. den Import einer Datastore-Sicherung in Google 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 Datastore 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 Datastore-Latenz erhöhen. 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 konsistenten 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.
  • Wenn Sie strikte Konsistenz für Ihre Abfragen benötigen, verwenden Sie eine Ancestor-Abfrage. (Zur Verwendung von Ancestor-Abfragen müssen Sie zuerst Ihre Daten für strikte Konsistenz strukturieren.) Eine Ancestor-Abfrage gibt strikt konsistente Ergebnisse zurück. Beachten Sie, dass eine keys-only Nicht-Ancestor-Abfrage gefolgt von einem lookup() keine strikt konsistenten Ergebnisse liefert. Eine solche Abfrage könnte Ergebnisse aus einem Index abrufen, der zum Zeitpunkt der Abfrage nicht konsistent ist.

Skalierbares Programmdesign

Updates für eine einzelne Entitätengruppe

Eine einzelne Entitätengruppe in Datastore sollte nicht zu schnell aktualisiert werden.

Wenn Sie Datastore verwenden, empfiehlt Google, die Anwendung so zu entwerfen, dass eine Entitätengruppe nicht mehr als einmal pro Sekunde aktualisiert werden muss. Denken Sie daran, dass eine Entität ohne übergeordnete oder untergeordnete Entitäten selbst eine Entitätengruppe ist. Wenn Sie eine Entitätengruppe zu schnell aktualisieren, kommt es bei Datastore-Schreibvorgängen zu einer höheren Latenz, zu Zeitüberschreitungen und zu anderen Arten von Fehlern. Dies wird als Konflikt bezeichnet.

Datastore-Schreibraten in eine einzelne Entitätengruppe können gelegentlich den Grenzwert von einem Vorgang pro Sekunde überschreiten, sodass Lasttests dieses Problem möglicherweise nicht aufzeigen.

Hohe Lese-/Schreibraten bei einem schmalen Schlüsselbereich

Vermeiden Sie hohe Lese- oder Schreibraten bei Datastore-Schlüsseln, die lexikografisch eng beieinanderliegen.

Datastore basiert auf der NoSQL-Datenbank Bigtable von Google und unterliegt den Leistungsdaten von Bigtable. Bigtable skaliert, indem Zeilen in separate Tabellenreihen fragmentiert werden. Diese Zeilen werden lexikografisch nach Schlüssel angeordnet.

Wenn Sie Datastore verwenden, können bei einem plötzlichen Anstieg der Schreibrate in einen kleinen Bereich von Schlüsseln, der die Kapazität eines einzelnen Tabellenreihenservers übersteigt, langsame Schreibvorgänge aufgrund von übermäßig genutzten Tabellenreihen ("heißen" Tabellenreihen) resultieren. Bigtable teilt den Schlüsselbereich letztendlich auf, um die hohe Belastung abzufangen.

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. Bigtable kann einen einzelnen Schlüssel nicht in mehr als eine Tabellenreihe aufteilen.

Hot-Tabellenreihen können auf Schlüsselbereiche angewendet werden, die sowohl von Entitätsschlüsseln als auch von Indexen verwendet werden.

In einigen Fällen kann ein Datastore-Engpass eine Anwendung noch weitergehend beeinträchtigen, als dass er nur Lese- oder Schreibvorgänge in einem kleinen Schlüsselbereich verhindert. Beispiel: Die Hot-Schlüssel könnten beim Hochfahren der Instanz gelesen oder geschrieben werden, wodurch das Laden von Anfragen fehlschlägt.

Standardmäßig weist Datastore Schlüssel mithilfe eines Verteilungsalgorithmus zu. Daher treten in der Regel bei Datenspeicher-Schreibvorgängen keine heißen Phasen auf, wenn Sie neue Entitäten mit einer hohen Schreibrate mithilfe der standardmäßigen ID-Zuordnungsrichtlinie erstellen. Es gibt einige seltene Fälle, bei denen dieses Problem auftreten kann:

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

  • Wenn Sie neue Entitäten mit einer sehr hohen Rate erstellen und Ihre eigenen IDs zuordnen, die kontinuierlich erhöht werden.

  • Wenn Sie mit einer sehr hohen Rate neue Entitäten für eine Art erstellen, für die vorher sehr wenige Entitäten vorhanden waren. Bigtable beginnt mit allen Entitäten auf demselben Tabellenreihenserver und es dauert eine gewisse Zeit, bis der Schlüsselbereich in separate Tabellenreihenserver aufgeteilt wird.

  • Dieses Problem tritt auch auf, wenn Sie mit einer hohen Rate neue Entitäten mit einem kontinuierlich erhöhten indexierten Attribut wie einem Zeitstempel erstellen, weil diese Attribute die Schlüssel für Zeilen in den Indextabellen in Bigtable darstellen.

  • Datastore stellt dem Bigtable-Zeilenschlüssel den Namespace und die Art der Stammentitätengruppe voran. Es kann zu einem Engpass kommen, wenn Sie mit dem Schreiben in einen neuen Namespace oder eine neue Art beginnen, ohne den Traffic nach und nach zu erhöhen.

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 für ein monoton ansteigendes oder abnehmendes Attribut eine Abfrage mit Sortierung oder Filter erforderlich ist, könnten Sie stattdessen ein neues Attribut indexieren. Versehen Sie für dieses den monotonen Wert mit einem Präfixwert, 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ückgeben wollen, können Sie dem Zeitstempel die Nutzer-ID voranstellen und dieses neue Attribut indexieren. Dies würde weiterhin Abfragen und sortierte Ergebnisse für diesen Nutzer zulassen, aber durch die Nutzer-ID wäre sichergestellt, dass der Index selbst gut fragmentiert ist.

Eine detaillierte Erläuterung dieses Problems finden Sie im Blog von Ikai Lan zum Speichern kontinuierlich ansteigender Werte in Datastore.

Traffic erhöhen

Erhöhen Sie den Traffic für neue Datastore-Arten oder Teile des Schlüsselraums schrittweise.

Sie sollten den Traffic für neue Datastore-Arten nach und nach erhöhen, um Bigtable ausreichend Zeit zur Aufteilung der Tabellenreihen bei wachsendem Traffic zu geben. Wir empfehlen einen Höchstwert von 500 Vorgängen pro Sekunde für eine neue Datastore-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. Bigtable kann Tabellen möglicherweise nicht effizient teilen, wenn der Schlüsselraum knapp ist.

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 Batchjob muss Schreibvorgänge in sequenzielle Schlüssel vermeiden, um Bigtable-Engpässe zu verhindern. Wenn der Batchjob 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. Dadurch steigen die damit verbundenen Datastore-Kosten.

Löschvorgänge

Vermeiden Sie das Löschen einer großen Anzahl von Datastore-Entitäten über einen kleinen Schlüsselbereich hinweg.

Bigtable 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 Datastore-Entitäten 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 erreichen, 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 Fragmentierung oder Replikation für häufig genutzte Datastore-Schlüssel.

Sie können die Replikation verwenden, wenn Sie einen Teil des Schlüsselbereichs mit einer höheren Rate lesen müssen, als Bigtable 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.

Wenn Sie in einen Teil des Schlüsselbereichs mit einer höheren Geschwindigkeit schreiben müssen, als Bigtable zulässt, können Sie die Fragmentierung nutzen. 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.

Nächste Schritte