Strong Consistency und Eventual Consistency mit Cloud Datastore abstimmen

Eine konsistente Nutzererfahrung bieten und das Eventual-Consistency-Modell für die Skalierung auf große Datensätze nutzen

In diesem Dokument werden die Umsetzung von Strong Consistency im Sinne einer positiven Nutzererfahrung und der Einsatz des Eventual-Consistency-Modells von Cloud Datastore für die Verarbeitung großer Mengen von Daten und Nutzern erläutert.

Dieses Dokument richtet sich an Softwarearchitekten und -entwickler, die Lösungen mit Cloud Datastore erstellen möchten. Zur Unterstützung von Lesern, die eher mit relationalen Datenbanken als mit nicht relationalen Systemen wie Cloud Datastore vertraut sind, verweist dieses Dokument auf entsprechende Konzepte in relationalen Datenbanken. Im Rahmen dieses Dokuments wird vorausgesetzt, dass Sie über Grundkenntnisse mit Cloud Datastore verfügen. Der Einstieg in Cloud Datastore gelingt am leichtesten in Google App Engine, indem Sie eine der unterstützten Sprachen verwenden. Wenn Sie App Engine noch nicht verwendet haben, empfehlen wir Ihnen, zuerst den Startleitfaden und den Abschnitt Daten speichern für eine dieser Sprachen zu lesen. Obwohl Python in Beispiel-Codefragmenten genutzt wird, sind keine Python-Fachkenntnisse für das Verständnis dieses Dokuments erforderlich.

Hinweis: In den Code-Snippets in diesem Artikel wird die Python DB-Clientbibliothek für Cloud Datastore verwendet, die nicht mehr empfohlen wird. Entwicklern von neuen Anwendungen wird die NDB-Clientbibliothek dringend empfohlen. Diese bietet im Vergleich zur vorliegenden Clientbibliothek mehrere Vorteile, beispielsweise das automatische Caching von Entitäten über die Memcache API. Wenn Sie derzeit die ältere DB-Clientbibliothek verwenden, lesen Sie die Anleitung zur Migration von DB zu NDB.

Inhalt

NoSQL und Eventual Consistency
Eventual Consistency in Cloud Datastore
Ancestor-Abfrage und Entitätengruppe
Einschränkungen von Entitätengruppen und Ancestor-Abfragen
Alternativen zu Ancestor-Abfragen
Zeit bis zum Erreichen der vollständigen Konsistenz minimieren
Fazit
Zusätzliche Ressourcen

NoSQL und Eventual Consistency

In den letzten Jahren sind nicht relationale Datenbanken, die auch als NoSQL-Datenbanken bezeichnet werden, als Alternative zu relationalen Datenbanken aufgekommen. Cloud Datastore ist eine der am meisten genutzten nicht relationalen Datenbanken in der Branche. Im Jahr 2013 verarbeitete Google Cloud Datastore laut Google Cloud Platform-Blogpost 4,5 Trillionen Transaktionen pro Monat. Der Dienst stellt für Entwickler eine vereinfachte Möglichkeit dar, um Daten zu speichern und darauf zuzugreifen. Das flexible Schema ist perfekt auf objektorientierte und skriptbasierte Sprachen abgestimmt. Cloud Datastore bietet außerdem eine Reihe von Funktionsmerkmalen, die relationale Datenbanken nicht optimal bereitstellen können, insbesondere hohe Leistung bei sehr großen Datenmengen sowie hohe Zuverlässigkeit.

Für Entwickler, die relationale Datenbanken gewohnt sind, ist es möglicherweise nicht ganz einfach, ein System zu konzipieren, das nicht relationale Datenbanken nutzt, da sie mit einigen Eigenschaften und Verfahren solcher Datenbanken weniger vertraut sind. Das Cloud Datastore-Programmiermodell ist zwar unkompliziert, doch es ist wichtig, diese Eigenschaften im Blick zu behalten. Eventual Consistency ist eines dieser Eigenschaften und das Hauptthema dieses Dokuments ist die darauf ausgerichtete Programmierung.

Was ist Eventual Consistency?

Eventual Consistency ist die theoretische Garantie, dass alle Lesevorgänge der Entität schließlich den zuletzt aktualisierten Wert zurückgeben, vorausgesetzt, es werden keine neuen Aktualisierungen der Entität vorgenommen. Das Domain Name System (DNS) des Internets ist ein gut bekanntes Beispiel für ein System mit einem Eventual-Consistency-Modell. DNS-Server geben nicht unbedingt die neuesten Werte wieder, sondern die Werte werden vielmehr in den Cache aufgenommen und über das Internet in vielen Verzeichnissen repliziert. Die Replikation der geänderten Werte auf alle DNS-Clients und -Server nimmt eine gewisse Zeit in Anspruch. Dennoch ist das DNS ein sehr erfolgreiches System, das zu einem der Fundamente des Internets geworden ist. Es ist hochverfügbar und hat sich als äußerst skalierbar erwiesen, indem es das Nachschlagen von Namen für über hundert Millionen Geräte im gesamten Internet ermöglicht.

Abbildung 1 veranschaulicht das Konzept der Replikation mit Eventual Consistency. Im Diagramm ist ersichtlich, dass die Replikate zwar immer zum Lesen bereitstehen, aber einige zu einem bestimmten Zeitpunkt möglicherweise nicht mit dem letzten Schreibvorgang auf dem Ursprungsknoten übereinstimmen. Im Diagramm ist Node A (Knoten A) der Ursprungsknoten und die Knoten B und C sind die Replikate.

Abbildung 1: Konzeptionelle Darstellung einer Replikation mit Eventual Consistency

Im Gegensatz dazu basieren relationale Datenbanken auf dem Strong-Consistency-Konzept, das auch als Immediate Consistency bezeichnet wird. Es bedeutet, dass Daten, die unmittelbar nach einer Aktualisierung angezeigt werden, für alle Beobachter der Entität übereinstimmen. Viele Entwickler, die relationale Datenbanken nutzen, gehen grundsätzlich von dieser Annahme aus. Für Strong Consistency müssen die Entwickler jedoch Kompromisse bezüglich der Skalierbarkeit und Leistungsstärke ihrer Anwendungen eingehen. Vereinfacht ausgedrückt müssen die Daten während des Aktualisierungs- oder Replikationsvorgangs gesperrt werden, um sicherzugehen, dass dieselben Daten nicht durch andere Prozesse aktualisiert werden.

In Abbildung 2 ist die Bereitstellungstopologie und der Replikationsprozess mit Strong Consistency dargestellt. Sie können in diesem Diagramm sehen, dass Replikate immer mit dem Ursprungsknoten übereinstimmende Werte haben, aber ein Zugriff darauf erst nach Abschluss des Aktualisierungsvorgangs möglich ist.

Abbildung 2: Konzeptionelle Darstellung einer Replikation mit Strong Consistency

Ausgleich zwischen Strong Consistency und Eventual Consistency herstellen

Nicht relationale Datenbanken sind in jüngster Zeit populär geworden, insbesondere bei Webanwendungen, die hohe Skalierbarkeit und hochverfügbare Leistungsfähigkeit erfordern. Mit nicht relationalen Datenbanken können die Entwickler für jede Anwendung einen optimalen Ausgleich zwischen Strong Consistency und Eventual Consistency erzielen. Auf diese Weise lassen sich die Vorteile beider Modelle kombinieren. Anwendungsfälle, bei denen Strong Consistency nicht benötigt wird, sind zum Beispiel: "Wer von meinen Freunden ist zu einer bestimmten Zeit online?" oder "Wie viele Nutzer haben meinem Post +1 gegeben?". Über die Nutzung von Eventual Consistency können Sie solche Anwendungsfälle jedoch mit Skalierbarkeit und Leistungsstärke versehen. Anwendungsfälle, die Strong Consistency erfordern, sind zum Beispiel: "Hat ein Nutzer den Abrechnungsprozess durchlaufen oder nicht?" oder "Wie viele Punkte hat ein Spieler in seiner Spielsitzung erzielt?".

Grundsätzliche Schlussfolgerung aus den obigen Beispielen: Für Anwendungsfälle mit einer sehr großen Anzahl von Entitäten empfiehlt sich häufig Eventual Consistency als bestes Modell. Wenn sehr viele Ergebnisse in einer Abfrage vorliegen, wird die Nutzererfahrung durch die Aufnahme oder den Ausschluss bestimmter Entitäten wahrscheinlich nicht beeinträchtigt. Andererseits ist davon auszugehen, dass bei Anwendungsfällen mit einer kleinen Zahl von Entitäten und einem eingeschränkten Kontext Strong Consistency erforderlich ist. Die Nutzererfahrung wird beeinträchtigt, weil dem Nutzer durch den Kontext klar wird, welche Entitäten ein- oder auszuschließen sind.

Aus den oben genannten Gründen ist es wichtig, dass Entwickler die nicht relationalen Eigenschaften von Cloud Datastore kennen. In den folgenden Abschnitten wird erläutert, wie Eventual-Consistency- und Strong-Consistency-Modelle miteinander kombiniert werden können, um eine skalierbare, hochverfügbare und leistungsstarke Anwendung zu erstellen. Dabei werden die Konsistenzanforderungen im Sinne einer positiven Nutzererfahrung erfüllt.

Eventual Consistency in Cloud Datastore

Wenn eine stark konsistente Ansicht der Daten benötigt wird, muss die richtige API ausgewählt werden. Die unterschiedlichen Versionen von Cloud Datastore-Abfrage-APIs und die entsprechenden Konsistenzmodelle sind in Tabelle 1 dargestellt.

Cloud Datastore API

Entitätswert lesen

Index lesen

Globale Abfrage

Eventual Consistency

Eventual Consistency

Globale Abfrage (nur Schlüssel)

Nicht zutreffend

Eventual Consistency

Ancestor-Abfrage

Strong Consistency

Strong Consistency

Suche nach Schlüssel (get())

Strong Consistency

Nicht zutreffend

Tabelle 1: Cloud Datastore-Abfragen/-get-Aufrufe und mögliches Konsistenzverhalten

Cloud Datastore-Abfragen ohne Ancestor werden als globale Abfragen bezeichnet und sind für die Einbindung in ein Eventual-Consistency-Modell vorgesehen. Dies garantiert keine Strong Consistency. Eine ausschließlich schlüsselbasierte globale Abfrage ist eine globale Abfrage, die nur die Schlüssel der Entitäten zurückgibt, die der Abfrage entsprechen, nicht die Attributwerte der Entitäten. Eine Ancestor-Abfrage wird auf Basis einer Ancestor-Entität durchgeführt. In den folgenden Abschnitten wird das unterschiedliche Konsistenzverhalten näher beleuchtet.

Eventual Consistency beim Lesen von Entitätswerten

Mit der Ausnahme von Ancestor-Abfragen ist ein aktualisierter Entitätswert bei der Ausführung einer Abfrage möglicherweise nicht sofort sichtbar. Gehen Sie zum Verständnis der Auswirkung von Eventual Consistency beim Lesen von Entitätswerten von einem Szenario aus, bei dem eine Entität, "Spieler", eine Eigenschaft, "Spielstand", besitzt. Angenommen, der Spielstand hat anfangs einen Wert von 100. Nach einiger Zeit wird der Wert auf 200 aktualisiert. Wenn eine globale Abfrage ausgeführt wird und dieselbe Entität "Spieler" in das Ergebnis einbezieht, ist es möglich, dass der Wert der Eigenschaft "Spielstand" der zurückgegebenen Entität unverändert angezeigt wird.

Dieses Verhalten beruht auf der Replikation zwischen Cloud Datastore-Servern. Die Replikation wird von Cloud Bigtable und Megastore verwaltet, den zugrunde liegenden Technologien für Cloud Datastore. Nähere Informationen zu Bigtable und Megastore finden Sie unter Zusätzliche Ressourcen. Die Replikation wird mit dem Paxos-Algorithmus durchgeführt, der gleichzeitig wartet, bis eine Mehrzahl der Replikate die Aktualisierungsanfrage quittiert hat. Die einzelnen Replikate werden nach einer gewissen Zeit mit den Daten aus der Anfrage aktualisiert. Dieser Zeitraum ist in der Regel kurz, seine tatsächliche Länge wird aber nicht garantiert. Es kann vorkommen, dass eine Abfrage die veralteten Daten liest, wenn sie vor Fertigstellung der Aktualisierung durchgeführt wird.

In vielen Fällen erreicht die Aktualisierung sämtliche Replikate sehr schnell. Treffen allerdings mehrere Faktoren zusammen, kann die Dauer bis zum Erreichen der Konsistenz länger werden. Zu diesen Faktoren gehören das ganze Rechenzentrum betreffende Vorkommnisse, bei denen eine große Anzahl von Servern zwischen Rechenzentren umgestellt werden. Angesichts der Unterschiedlichkeit dieser Faktoren ist es unmöglich, feste Zeitvorgaben für die Herstellung vollständiger Konsistenz festzulegen.

Die Zeitspanne, bis eine Abfrage den neuesten Wert zurückgibt, ist gewöhnlich sehr kurz. Wenn die Replikationslatenz in seltenen Fällen jedoch zunimmt, kann es wesentlich länger dauern. Anwendungen, die globale Cloud Datastore-Abfragen nutzen, sollten sorgfältig so konzipiert werden, dass sie solche Situationen gut bewältigen können.

Die Eventual Consistency beim Lesen von Entitätswerten kann mithilfe einer ausschließlich schlüsselbasierten Abfrage, einer Ancestor-Abfrage oder einer schlüsselbasierten Suche mit der Methode "get()" vermieden werden. Diese verschiedenen Abfragetypen werden weiter unten erläutert.

Eventual Consistency beim Lesen eines Index

Ein Index ist bei Ausführung einer globalen Abfrage möglicherweise noch nicht aktualisiert. Das heißt, auch wenn Sie die neuesten Eigenschaftswerte der Entitäten lesen können, wird die "Liste der Entitäten" im Abfrageergebnis möglicherweise auf der Grundlage alter Indexwerte gefiltert.

Gehen Sie zum Verständnis von Eventual Consistency beim Lesen eines Index von einem Szenario aus, bei dem eine neue Entität, "Spieler", in Cloud Datastore eingefügt wird. Die Entität hat eine Eigenschaft, "Spielstand", mit einem anfänglichen Wert von 300. Unmittelbar nach dem Einfügen führen Sie eine ausschließlich schlüsselbasierte Abfrage durch, um alle Entitäten mit einem Spielstand über 0 abzurufen. Sie würden dann erwarten, dass die Entität "Spieler", die soeben eingefügt wurde, in den Abfrageergebnissen erscheint. Stattdessen kann es sein, dass die Entität "Spieler" nicht in den Ergebnissen aufgeführt wird. Diese Situation kann eintreten, wenn die Indextabelle für die Eigenschaft "Spielstand" zum Zeitpunkt der Ausführung der Abfrage nicht mit dem neu eingefügten Wert aktualisiert ist.

Beachten Sie, dass zwar alle Abfragen in Cloud Datastore auf Indextabellen angewendet werden, die Aktualisierungen der Indextabellen jedoch asynchron sind (nur auf Englisch verfügbar). Jede Entitätsaktualisierung besteht im Wesentlichen aus zwei Phasen. In der ersten Phase, der Übernahmephase, wird ein Schreibvorgang in das Transaktionsprotokoll durchgeführt. In der zweiten Phase werden Daten geschrieben und Indexe aktualisiert. Wenn die Übernahmephase erfolgreich verläuft, wird die Schreibphase mit Sicherheit gelingen, auch wenn diese eventuell nicht sofort erfolgt. Wenn Sie eine Entität abfragen, bevor die Indexe aktualisiert sind, sehen Sie eventuell Daten, die noch nicht konsistent sind.

Infolge dieses Zwei-Phasen-Prozesses besteht eine Zeitverzögerung, bevor die neuesten Aktualisierungen von Entitäten bei globalen Abfragen sichtbar sind. Wie bei der Eventual Consistency von Entitätswerten ist die Zeitverzögerung typischerweise gering. Sie kann aber auch länger dauern und unter außergewöhnlichen Umständen sogar mehrere Minuten betragen.

Die gleiche Situation kann auch nach Aktualisierungen eintreten. Gehen wir zum Beispiel davon aus, dass Sie eine vorhandene Entität "Spieler" mit einem Eigenschaftswert für "Spielstand" von 0 aktualisieren und sofort anschließend dieselbe Abfrage ausführen. Sie würden erwarten, dass die Entität nicht in den Abfrageergebnissen erscheint, weil der neue "Spielstand" von 0 dies ausschließt. Aufgrund desselben asynchronen Verhaltens bei der Indexaktualisierung ist es dennoch möglich, dass die Entität im Ergebnis enthalten ist.

Die Eventual Consistency beim Lesen eines Indexwerts lässt sich nur mithilfe einer Ancestor-Abfrage oder einer schlüsselbasierten Suche vermeiden. Mit einer ausschließlich schlüsselbasierten Abfrage kann dieses Verhalten nicht vermieden werden.

Strong Consistency beim Lesen von Entitätswerten und Indexen

In Cloud Datastore gibt es nur zwei APIs, die eine stark konsistente Ansicht für das Lesen von Entitätswerten und Indexen liefern: (1) die schlüsselbasierte Suche und (2) die Ancestor-Abfrage. Wenn die Anwendungslogik Strong Consistency erfordert, dann sollte der Entwickler eine dieser Methoden zum Lesen von Entitäten aus Cloud Datastore nutzen.

Cloud Datastore ist speziell für die Bereitstellung von Strong Consistency mithilfe dieser APIs ausgelegt. Wenn Sie eine davon aufrufen, überträgt Cloud Datastore alle ausstehenden Aktualisierungen auf eines der Replikate und eine der Indextabellen und führt dann die Suche oder Ancestor-Abfrage durch. Auf diese Weise wird der neueste Entitätswert auf Basis der aktualisierten Indextabelle immer mit Werten auf der Grundlage der neuesten Aktualisierungen zurückgegeben.

Beim Aufruf der schlüsselbasierten Suche wird im Gegensatz zu Abfragen nur eine Entität oder eine Gruppe von Entitäten zurückgegeben, die über einen Schlüssel oder eine Gruppe von Schlüsseln definiert ist. Das heißt, eine Ancestor-Abfrage stellt die einzige Möglichkeit in Cloud Datastore dar, um einer Strong-Consistency-Anforderung zusammen mit einer Filteranforderung zu genügen. Allerdings funktionieren Ancestor-Abfragen nicht ohne die Angabe einer Entitätengruppe.

Ancestor-Abfrage und Entitätengruppe

Wie am Anfang dieses Dokuments erläutert, besteht einer der Vorteile von Cloud Datastore darin, dass die Entwickler einen optimalen Ausgleich zwischen Strong Consistency und Eventual Consistency erreichen können. In Cloud Datastore ist eine Entitätengruppe eine Einheit mit Strong Consistency, Transaktionalität und Lokalität. Mithilfe von Entitätengruppen können Entwickler den Grad der Strong Consistency unter den Entitäten in einer Anwendung bestimmen. Auf diese Weise kann die Anwendung die Konsistenz innerhalb der Entitätengruppe aufrechterhalten und gleichzeitig als ein komplettes System Skalierbarkeit, Verfügbarkeit und Leistungsfähigkeit erzielen.

Eine Entitätengruppe ist eine hierarchische Struktur aus einer Stammentität sowie deren untergeordneten und nachfolgenden Elementen.[1] Zur Erstellung einer Entitätengruppe gibt der Entwickler einen Ancestor-Pfad vor, bei dem es sich im Wesentlichen um eine Reihe von übergeordneten Schlüsseln handelt, die dem untergeordneten Schlüssel vorangestellt sind. Das Konzept von Entitätengruppen ist in Abbildung 3 dargestellt. In diesem Fall hat die Root-Entität mit dem Schlüssel "ateam" zwei untergeordnete Elemente mit den Schlüsseln "ateam/098745" und "ateam/098746".

Abbildung 3: Schematische Darstellung des Entitätengruppenkonzepts

Innerhalb der Entitätengruppe sind die folgenden Eigenschaften garantiert:

  • Strong Consistency
    • Eine Ancestor-Abfrage auf der Entitätengruppe gibt ein stark konsistentes Ergebnis zurück. Auf diese Weise werden die neuesten Entitätswerte wiedergegeben, die nach dem letzten Indexstatus gefiltert wurden.
  • Transaktionalität
    • Wenn Sie eine Transaktion programmatisch abgrenzen, sieht die Entitätengruppe ACID-Eigenschaften (Atomarität, Konsistenz, Isolation und Langlebigkeit) in der Transaktion vor.
  • Lokalität
    • Entitäten in einer Entitätengruppe werden an physisch nahe gelegenen Orten auf Cloud Datastore-Servern gespeichert, weil alle Entitäten nach der lexikografischen Reihenfolge der Schlüssel sortiert und abgelegt werden. Eine Ancestor-Abfrage kann somit die Entitätengruppe mit minimalem E/A-Einsatz rasch scannen.

Eine Ancestor-Abfrage ist die Sonderform einer Abfrage, die in Bezug auf eine vorgegebene Entitätengruppe durchgeführt wird. Sie wird mit Strong Consistency ausgeführt. Im Hintergrund stellt Cloud Datastore sicher, dass alle anhängigen Replikationen und Indexaktualisierungen vor der Durchführung der Abfrage angewendet werden.

Beispiel für eine Ancestor-Abfrage

In diesem Abschnitt wird beschrieben, wie Entitätengruppen und Ancestor-Abfragen in der Praxis einzusetzen sind. Im folgenden Beispiel betrachten wir das Problem der Verwaltung von Datensätzen für Personen. Dabei gehen wir von einem Code aus, der eine Entität eines bestimmten Typs hinzufügt, auf die unmittelbar eine Abfrage dieses Typs folgt. Dieses Konzept wird anhand des folgenden Python-Beispielcodes demonstriert.

# Define the Person entity
class Person(db.Model):
    given_name = db.StringProperty()
    surname = db.StringProperty()
    organization = db.StringProperty()
# Add a person and retrieve the list of all people
class MainPage(webapp2.RequestHandler):
    def post(self):
        person = Person(given_name='GI', surname='Joe', organization='ATeam')
        person.put()
        q = db.GqlQuery("SELECT * FROM Person")
        people = []
        for p in q.run():
            people.append({'given_name': p.given_name,
                        'surname': p.surname,
                        'organization': p.organization})

Das Problem bei diesem Code besteht darin, dass die Abfrage in den meisten Fällen nicht die Entität zurückgibt, die in der Anweisung davor hinzugefügt wurde. Da die Abfrage unmittelbar in der Zeile nach der Einfügung folgt, wird der Index nicht aktualisiert, wenn die Abfrage ausgeführt wird. Außerdem liegt ein Problem mit der Validität dieses Anwendungsfalls vor: Ist es wirklich notwendig, eine Liste aller Personen auf einer Seite ohne Kontext zurückzugeben? Was wäre, wenn es eine Millionen Personen sind? Es würde zu lange dauern, bis die Seite zurückgegeben wird.

Die Art des Anwendungsfalls legt nahe, dass wir zur Eingrenzung der Abfrage einigen Kontext angeben sollten. In diesem Beispiel werden wir als Kontext die Organisation verwenden. Wir können dann die Organisation als Entitätengruppe nutzen und eine Ancestor-Abfrage ausführen, die unser Konsistenzproblem löst. Der folgende Python-Code veranschaulicht dieses Vorgehen.

class Organization(db.Model):
    name = db.StringProperty()
class Person(db.Model):
    given_name = db.StringProperty()
    surname = db.StringProperty()
class MainPage(webapp2.RequestHandler):
    def post(self):
        org = Organization.get_or_insert('ateam', name='ATeam')
        person = Person(parent=org)
        person.given_name='GI'
        person.surname='Joe'
        person.put()
        q = db.GqlQuery("SELECT * FROM Person WHERE ANCESTOR IS :1 ", org)
        people = []
        for p in q.run():
            people.append({'given_name': p.given_name,
                        'surname': p.surname})

Da dieses Mal das Ancestor-ORG-Objekt in der GqlQuery angegeben ist, gibt die Abfrage die Entität zurück, die soeben eingefügt wurde. Das Beispiel ließe sich auf eine Einzelperson ausdehnen, indem zusammen mit dem Namen der Person das Ancestor-Objekt abgefragt wird. Alternativ hätte man dazu auch den Entitätsschlüssel speichern und anschließend zur Eingrenzung anhand einer schlüsselbasierten Suche verwenden können.

Konsistenz zwischen Memcache und Cloud Datastore aufrechterhalten

Entitätengruppen können für die Aufrechterhaltung der Konsistenz zwischen Memcache-Einträgen und Cloud Datastore-Entitäten verwendet werden. Nehmen wir beispielsweise ein Szenario an, bei dem Sie die Anzahl der Personen in jedem Team zählen und diese in Memcache speichern. Zur Sicherstellung, dass die Daten im Cache mit den neuesten Werten im Cloud Datastore übereinstimmen, können Sie Metadaten der Entitätengruppen verwenden. Die Metadaten geben die neueste Versionsnummer der angegebenen Entitätengruppe zurück. Sie können die Versionsnummer mit der in Memcache gespeicherten Nummer vergleichen. Mithilfe dieser Methode können Sie eine Änderung in einer der Entitäten in der gesamten Entitätengruppe erkennen, indem Sie eine Gruppe von Metadaten auslesen, anstatt alle einzelnen Entitäten in der Gruppe zu scannen.

Einschränkungen von Entitätengruppen und Ancestor-Abfragen

Die Verwendung von Entitätengruppen und Ancestor-Abfragen ist keine Patentlösung. In der Praxis erschweren die folgenden zwei Aspekte die allgemeine Anwendung der Methode.

  1. Der Schreibvorgang für jede Entitätengruppe ist auf eine Aktualisierung pro Sekunde beschränkt.
  2. Die Beziehung der Entitätengruppe kann nach der Erstellung der Entitäten nicht geändert werden.

Schreibgrenze

Eine entscheidende Problemstellung besteht darin, dass das System auf die Aufnahme der Anzahl von Aktualisierungen oder Transaktionen in jeder Entitätengruppe ausgelegt werden muss. Der unterstützte Grenzwert ist eine Aktualisierung pro Sekunde und Entitätengruppe.[2] Wenn die Zahl der Aktualisierungen diesen Grenzwert überschreitet, kann die Entitätengruppe einen Leistungsengpass darstellen.

Im obigen Beispiel muss jede Organisation potenziell den Datensatz einer jeden Person in der Organisation aktualisieren. Stellen Sie sich ein Szenario vor, in dem sich 1.000 Personen im "ateam" befinden und für jede Person die Aktualisierung einer Eigenschaft pro Sekunde anfällt. Dadurch könnten bis zu 1.000 Aktualisierungen pro Sekunde in der Entitätengruppe stattfinden; das wäre eine Anforderung, die aufgrund der Aktualisierungsbeschränkung nicht erfüllt werden könnte. Dies macht deutlich, dass es wichtig ist, ein geeignetes Entitätengruppendesign zu wählen, das die Leistungsanforderungen berücksichtigt. Dies ist eine der Herausforderungen beim Herstellen des optimalen Ausgleichs zwischen Eventual Consistency und Strong Consistency.

Unveränderlichkeit von Entitätengruppenbeziehungen

Eine zweite Schwierigkeit besteht in der Unveränderlichkeit der Entitätengruppenbeziehungen. Die Entitätengruppenbeziehung wird statistisch auf Basis der Schlüsselbenennung gebildet. Nach der Erstellung der Entität kann sie nicht geändert werden. Die einzige verfügbare Option zur Änderung der Beziehung besteht darin, die Entitäten in einer Entitätengruppe zu löschen und neu zu erstellen. Diese Schwierigkeit hindert uns daran, mithilfe von Entitätengruppen dynamisch Ad-hoc-Anwendungsbereiche für Konsistenz und Transaktionalität festzulegen. Die Anwendungsbereiche für Konsistenz und Transaktionalität sind stattdessen eng mit der statischen Entitätengruppe verbunden, die bei der Konzeption definiert wird.

Stellen Sie sich beispielsweise ein Szenario vor, bei dem Sie eine Überweisung zwischen zwei Bankkonten vornehmen möchten. Ein solches Geschäftsszenario erfordert Strong Consistency und Transaktionalität. Die beiden Konten können jedoch nicht in letzter Minute in einer Entitätengruppe zusammengefasst oder mit einem globalen übergeordneten Element verknüpft werden. Diese Entitätengruppe würde einen Engpass für das gesamte System darstellen, der die Ausführung von anderen Überweisungsanfragen blockieren würde. Entitätengruppen können demzufolge nicht auf diese Weise verwendet werden.

Es gibt eine alternative Methode zum Implementieren einer elektronischen Überweisung auf äußerst skalierbare und hochverfügbare Weise. Statt alle Konten in einer einzelnen Entitätengruppe zu platzieren, können Sie für jedes Konto eine Entitätengruppe erstellen. Auf diese Weise können Sie Transaktionen verwenden, damit ACID-Aktualisierungen für beide Bankkonten durchgeführt werden. Transaktionen sind eine Cloud Datastore-Funktion, mit der Sie Gruppen von Vorgängen mit ACID-Eigenschaften für bis zu 25 Entitätengruppen erstellen können. Beachten Sie, dass Sie innerhalb einer Transaktion stark konsistente Abfragen wie Suchvorgänge nach Schlüsseln und Ancestor-Abfragen verwenden müssen. Weitere Informationen zu den Einschränkungen von Transaktionen finden Sie unter Transaktionen und Entitätengruppen.

Alternativen zu Ancestor-Abfragen

Wenn Sie bereits eine Anwendung haben, bei der eine große Anzahl von Entitäten in Cloud Datastore gespeichert ist, kann es sich als schwierig erweisen, Entitätengruppen nachträglich im Rahmen einer Refaktorierung einzubinden. Dazu müssten alle Entitäten gelöscht und innerhalb einer Entitätengruppenbeziehung hinzugefügt werden. Bei der Datenmodellierung in Cloud Datastore ist es deshalb wichtig, in der Frühphase des Anwendungsdesigns eine Entscheidung zur Konzeption der Entitätengruppen zu treffen. Andernfalls sind Sie dahingehend eingeschränkt, dass Sie auf andere Alternativen refaktorieren müssen, um ein bestimmtes Maß an Konsistenz zu erreichen, zum Beispiel eine ausschließlich schlüsselbasierte Abfrage gefolgt von einer schlüsselbasierten Suche oder die Nutzung von Memcache.

Ausschließlich schlüsselbasierte, globale Abfrage gefolgt von einer schlüsselbasierten Suche

Eine ausschließlich schlüsselbasierte, globale Abfrage ist eine Sonderform der globalen Abfrage, die ausschließlich Schlüssel ohne die Eigenschaftswerte der Entitäten zurückgibt. Da es sich bei den zurückgegebenen Werten ausschließlich um Schlüssel handelt, umfasst die Abfrage keinen Entitätswert mit einem potenziellen Konsistenzproblem. Über eine Kombination der ausschließlich schlüsselbasierten, globalen Abfrage mit einer Suchmethode werden die neuesten Entitätswerte gelesen. Es ist allerdings zu beachten, dass mit einer ausschließlich schlüsselbasierten, globalen Abfrage nicht die Möglichkeit eines Index ausgeschlossen werden kann, der zum Zeitpunkt der Abfrage noch nicht konsistent war, was dazu führen kann, dass eine Entität überhaupt nicht abgerufen wird. Das Ergebnis der Abfrage könnte potenziell über das Ausfiltern alter Indexwerte generiert werden. Ein Entwickler kann also eine ausschließlich schlüsselbasierte, globale Abfrage gefolgt von einer schlüsselbasierten Suche nur verwenden, wenn es eine Anwendungsanforderung zulässt, dass der Indexwert zum Zeitpunkt der Abfrage noch nicht konsistent ist.

Memcache verwenden

Der Memcache-Dienst ist zwar volatil, aber stark konsistent. Über die Kombination von Memcache-Suchvorgängen und Cloud Datastore-Abfragen ist es möglich, ein System aufzubauen, das Konsistenzprobleme die meiste Zeit über minimiert.

Nehmen wir zum Beispiel das Szenario einer Spieleanwendung an, die eine Liste der Spielerentitäten pflegt, die jeweils einen Spielstand größer null haben.

  • Wenden Sie Anfragen zum Einfügen oder Aktualisieren auf die Liste der Spielerentitäten in Memcache und in Cloud Datastore an.
  • Lesen Sie für Abfrageanfragen die Liste der Spielerentitäten aus Memcache aus. Führen Sie eine ausschließlich schlüsselbasierte Abfrage in Cloud Datastore durch, wenn die Liste in Memcache nicht vorliegt.

Die zurückgegebene Liste ist immer dann konsistent, wenn die zwischengespeicherte Liste in Memcache vorliegt. Wenn der Eintrag entfernt wurde oder der Memcache-Dienst vorübergehend nicht verfügbar ist, muss das System den Wert möglicherweise aus einer Cloud Datastore-Abfrage lesen, die eventuell ein inkonsistentes Ergebnis liefert. Dieses Verfahren kann bei jeder Anwendung eingesetzt werden, die einen geringen Grad an Inkonsistenz toleriert.

Bei der Nutzung von Memcache als Caching-Ebene für Cloud Datastore haben sich einige Verfahren bewährt:

  • Fangen Sie Memcache-Ausnahmen und -Fehler ab, um die Konsistenz zwischen dem Memcache-Wert und dem Cloud Datastore-Wert aufrechtzuerhalten. Wenn Sie bei der Aktualisierung des Eintrags in Memcache eine Ausnahme empfangen, machen Sie den alten Eintrag in Memcache unbedingt ungültig. Andernfalls kann es zu unterschiedlichen Werten für eine Entität kommen (ein alter Wert in Memcache und ein neuer Wert in Cloud Datastore).
  • Legen Sie einen Ablaufzeitraum für die Memcache-Einträge fest. Es wird empfohlen, kurze Zeiträume für den Ablauf der einzelnen Einträge einzustellen, um die Möglichkeit der Inkonsistenz bei Memcache-Ausnahmen zu minimieren.
  • Verwenden Sie zur Gleichzeitigkeitserkennung bei der Aktualisierung der Einträge die Funktion compare-and-set. Damit wird sichergestellt, dass sich simultane Aktualisierungen desselben Eintrags nicht gegenseitig beeinträchtigen.

Graduelle Umstellung auf Entitätengruppen

Die Empfehlungen im vorherigen Abschnitt verringern lediglich die Möglichkeit inkonsistenten Verhaltens. Wenn Strong Consistency benötigt wird, ist es am besten, die Anwendung basierend auf Entitätengruppen und Ancestor-Abfragen zu konzipieren. Allerdings ist es möglicherweise nicht durchführbar, eine vorhandene Anwendung zu migrieren, was die Umstellung eines bestehenden Datenmodells und der Anwendungslogik von globalen Abfragen auf Ancestor-Abfragen bedeuten kann. Die Umstellung kann jedoch mit einem graduellen Prozess erfolgen, zum Beispiel:

  1. Bestimmen und priorisieren Sie die Funktionen in der Anwendung, die Strong Consistency erfordern.
  2. Schreiben Sie zusätzlich zur vorhandenen Logik (besser als Ersetzen) unter Verwendung von Entitätengruppen neue Programmlogik für insert()- oder update()-Funktionen. Auf diese Weise können neue Einfügungen oder Aktualisierungen sowohl für neue Entitätengruppen als auch alte Entitäten über eine geeignete Funktion abgewickelt werden.
  3. Modifizieren Sie die vorhandene Logik für Lese- und Abfragefunktionen. Ancestor-Abfragen werden erst durchgeführt, wenn eine neue Entitätengruppe für die Anfrage vorliegt. Führen Sie die alte globale Abfrage als Fallback-Logik aus, wenn die Entitätengruppe nicht existiert.

Diese Strategie ermöglicht auf der Basis von Entitätengruppen eine graduelle Migration von einem vorhandenen Datenmodell zu einem neuen Datenmodell. Damit werden die Risiken aufgrund von Eventual Consistency minimiert. In der Praxis hängt dieser Ansatz von den spezifischen Anwendungsfällen und Anforderungen für die Anwendung auf ein tatsächliches System ab.

Fallback auf eingeschränkten Modus

Derzeit ist es schwierig, eine Situation programmatisch zu erkennen, in der eine Anwendung Konsistenz eingebüßt hat. Wenn Sie jedoch auf anderem Weg feststellen, dass eine Anwendung an Konsistenz eingebüßt hat, ist es potenziell möglich, einen eingeschränkten Modus zu implementieren, in dem einige Bereiche der Anwendungslogik deaktiviert sind, die starke Konsistenz benötigen. Anstelle der Anzeige eines inkonsistenten Abfrageergebnisses in einem Abrechnungsbericht könnte zum Beispiel eine Wartungsmeldung für diesen Bildschirm dargestellt werden. Auf diese Weise können die anderen Dienste in der Anwendung weiter ausgeführt werden und reduzieren so die Auswirkung auf die Nutzererfahrung.

Zeit bis zum Erreichen der vollständigen Konsistenz minimieren

In einer großen Anwendung mit Millionen Nutzern oder Terabyte an Cloud Datastore-Entitäten ist es möglich, dass eine unzweckmäßige Nutzung von Cloud Datastore zu verschlechterter Konsistenz führt. Beispiele für eine solche Nutzung:

  • Fortlaufende Nummerierung in Entitätsschlüsseln
  • Zu viele Indexe

Bei kleinen Anwendungen wirken sich diese Vorgehensweisen nicht aus. Wenn die Anwendung jedoch sehr umfangreich wird, nimmt die Wahrscheinlichkeit zu, dass die Konsistenz mehr Zeit in Anspruch nimmt. Es ist deshalb am besten, diese Vorgehensweisen in den frühen Phasen des Anwendungsdesigns zu vermeiden.

Gegenmaßnahme 1: Fortlaufende Nummerierung von Entitätsschlüsseln

Vor der Herausgabe des App Engine SDK 1.8.1 nutzte Cloud Datastore als automatisch generierten Standardschlüssel eine Abfolge kleiner Ganzzahl-IDs mit üblicherweise fortlaufenden Schemas. In manchen Dokumenten wird dies als eine Legacy-Strategie für die Erstellung von Entitäten bezeichnet, die keinen über die Anwendung vorgegebenen Schlüsselnamen haben. Aufgrund dieser Legacy-Strategie wurden Schlüsselnamen mit fortlaufender Nummerierung generiert, zum Beispiel 1000, 1001, 1002. Wie bereits weiter oben ausgeführt, speichert Cloud Datastore Entitäten jedoch in lexikografischer Reihenfolge der Schlüsselnamen. Diese Entitäten werden also sehr wahrscheinlich auf denselben Cloud Datastore-Servern abgelegt. Wenn eine Anwendung sehr starken Traffic generiert, könnte diese fortlaufende Nummerierung eine Konzentration der Operationen auf einem bestimmten Server bewirken, die bezüglich der Konsistenz zu längeren Latenzzeiten führen kann.

In App Engine SDK 1.8.1 wurde für Cloud Datastore ein neues Verfahren zur ID-Nummerierung mit einer Standardstrategie eingeführt, die verteilte IDs nutzt (siehe Referenzdokumentation). Bei diesem Verfahren wird eine zufällige Abfolge von IDs mit einer Länge von bis zu 16 Ziffern erzeugt, die nahezu gleichmäßig verteilt werden. Damit ist es wahrscheinlich, dass der Traffic der großen Anwendung besser auf eine Gruppe von Cloud Datastore-Servern verteilt wird und die Konsistenz weniger Zeit in Anspruch nimmt. Diese Standardstrategie wird empfohlen, es sei denn, Ihre Anwendung muss mit der Legacy-Strategie kompatibel sein.

Wenn Sie explizit Schlüsselnamen für Entitäten vergeben, sollte das Benennungsschema so konzipiert werden, dass der Zugriff auf die Entitäten gleichmäßig über den gesamten Namensraum der Schlüssel verteilt ist. In anderen Worten: Konzentrieren Sie den Zugriff nicht auf einen bestimmten Bereich, weil die Entitäten nach der lexikografischen Reihenfolge der Schlüsselnamen angeordnet sind. Andernfalls kann das gleiche Problem wie bei der fortlaufenden Nummerierung auftreten.

Betrachten Sie zur Veranschaulichung einer ungleichmäßigen Zugriffsverteilung über den Schlüsselraum ein Beispiel, bei dem die Entitäten mit den sequenziellen Schlüsselnamen erstellt werden, so wie das im folgenden Code gezeigt wird:

p1 = Person(key_name='0001')
p2 = Person(key_name='0002')
p3 = Person(key_name='0003')
...

Das Zugriffsschema der Anwendung kann zu einer Konzentration in einem bestimmten Bereich der Schlüsselnamen führen, zum Beispiel zu einem konzentrierten Zugriff auf kürzlich erstellte Personenentitäten. In diesem Fall hätten die Schlüssel mit häufigem Zugriff höhere IDs. Die Last ist dann möglicherweise auf einem bestimmten Cloud Datastore-Server konzentriert.

Betrachten Sie alternativ zur Veranschaulichung einer gleichmäßigen Verteilung über den Schlüsselraum die Verwendung langer, zufälliger Strings für Schlüsselnamen. Dies ist im folgenden Beispiel dargestellt:

p1 = Person(key_name='t9P776g5kAecChuKW4JKCnh44uRvBDhU')
p2 = Person(key_name='hCdVjL2jCzLqRnPdNNcPCAN8Rinug9kq')
p3 = Person(key_name='PaV9fsXCdra7zCMkt7UX3THvFmu6xsUd')
...

Die kürzlich erstellten Personenentitäten werden jetzt über den Schlüsselraum und auf mehrere Server verteilt. Dies setzt voraus, dass eine ausreichend große Zahl an Personenentitäten vorhanden ist.

Gegenmaßnahme 2: Zu viele Indexe

In Cloud Datastore führt eine Aktualisierung in einer Entität zur Aktualisierung sämtlicher Indexe, die für diesen Entitätstyp definiert sind. Nähere Informationen finden Sie unter Leben eines Datenspeicher-Schreibvorgangs. Wenn eine Anwendung viele benutzerdefinierte Indexe verwendet, könnte eine Aktualisierung Dutzende, Hunderte oder gar Tausende von Aktualisierungen in Indextabellen umfassen. In einer großen Anwendung kann eine übermäßige Verwendung benutzerdefinierter Indexe zu einer erhöhten Beanspruchung des Servers führen und die Latenzzeit bis zum Erreichen der Konsistenz verlängern.

In den meisten Fällen werden benutzerdefinierte Indexe für die Unterstützung von Aufgaben des Kundensupports, der Fehlerbehebung oder der Datenanalyse hinzugefügt. BigQuery ist eine stark skalierbare Abfrage-Engine, die in der Lage ist, Ad-hoc-Abfragen in großen Datensätzen ohne zuvor angelegte Indexe durchzuführen. Der Dienst eignet sich besser für Anwendungsfälle wie Kundensupport, Fehlerbehebung oder Datenanalyse, die komplexere Abfragen benötigen als Cloud Datastore.

Eine Vorgehensweise besteht darin, Cloud Datastore und BigQuery für die Erfüllung unterschiedlicher Geschäftsanforderungen zu kombinieren. Nutzen Sie Cloud Datastore für das Online Transaction Processing (OLTP) der Kernanwendungslogik und BigQuery für das Online Analytical Processing (OLAP) der Back-End-Prozesse. Es kann notwendig sein, einen kontinuierlichen Datenexportstrom aus Cloud Datastore nach BigQuery zu implementieren, um die benötigten Daten für diese Abfragen zu verschieben.

Neben einer alternativen Implementierung für benutzerdefinierte Indexe wäre eine andere Empfehlung, nicht indexierte Eigenschaften explizit vorzugeben (siehe Eigenschaften und Werttypen). In der Standardeinstellung erstellt Cloud Datastore eine andere Indextabelle für jede indexierbare Eigenschaft eines Entitätstyps. Wenn also 100 Eigenschaften für einen Typ vorliegen, sind 100 Indextabellen für diesen Typ vorhanden und zusätzlich 100 Aktualisierungen für jede Aktualisierung einer Entität. Eine bewährte Vorgehensweise wäre es dann, Eigenschaften soweit möglich nicht indexiert vorzugeben, wenn diese nicht für eine Abfragebedingung benötigt werden.

Diese Indexoptimierungen können neben der geringeren Wahrscheinlichkeit für längere Konsistenzzeiten zu einer erheblichen Reduzierung der Cloud Datastore-Speicherkosten bei großen Anwendungen mit erheblicher Indexnutzung führen.

Fazit

Eventual Consistency ist ein wesentliches Element nicht relationaler Datenbanken, das es Entwicklern ermöglicht, einen optimalen Ausgleich zwischen Skalierbarkeit, Leistungsfähigkeit und Konsistenz zu finden. Es ist wichtig, das Ausbalancieren zwischen Eventual und Strong Consistency zu beherrschen, um ein optimales Datenmodell für eine Anwendung zu konzipieren. In Cloud Datastore besteht die optimale Vorgehensweise zur Gewährleistung von Strong Consistency in einem Entitätsbereich in der Nutzung von Entitätengruppen und Ancestor-Abfragen. Wenn Ihre Anwendung aufgrund der oben beschriebenen Einschränkungen keine Entitätengruppen einbinden kann, können Sie andere Optionen wie die Verwendung von ausschließlich schlüsselbasierten Abfragen oder Memcache in Betracht ziehen. Wenden Sie bei großen Anwendungen Best Practices wie verteilte IDs und reduzierte Indexierung an, um die für die Konsistenz benötigte Zeit zu verringern. Eventuell ist es auch erforderlich, Cloud Datastore mit BigQuery zu kombinieren, um die geschäftlichen Anforderungen für komplexe Abfragen zu erfüllen und die Nutzung von Cloud Datastore-Indexe so weit wie möglich zu reduzieren.

Zusätzliche Ressourcen

In den folgenden Quellen finden Sie weitere Informationen über die in diesem Dokument behandelten Themen:




[1] Eine Entitätengruppe kann selbst über die Angabe von nur einem Schlüssel der Root- oder übergeordneten Entität gebildet werden, ohne die tatsächlichen Entitäten des Root- oder übergeordneten Elements zu speichern, weil sämtliche Funktionen der Entitätengruppe auf der Grundlage von Beziehungen zwischen den Schlüsseln implementiert werden.

[2] Der unterstützte Grenzwert ist eine Aktualisierung pro Sekunde und Entitätengruppe außerhalb von Transaktionen oder eine Transaktion pro Sekunde und Entitätengruppe. Wenn Sie mehrere Aktualisierungen in einer Transaktion zusammenfassen, gilt für Sie das Limit einer maximalen Transaktionsgröße von 10 MB und der maximalen Schreibrate des Datastore-Servers.

Hat Ihnen diese Seite weitergeholfen? Teilen Sie uns Ihr Feedback mit:

Feedback geben zu...

Cloud Datastore-Dokumentation