Strikte Konsistenz und Eventual Consistency mit Datastore abstimmen

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

In diesem Dokument werden die Umsetzung von strikter Konsistenz im Sinne der Nutzerfreundlichkeit und der Einsatz des Eventual-Consistency-Modells von 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 Datastore erstellen möchten. Zur Unterstützung von Lesern, die eher mit relationalen Datenbanken als mit nicht relationalen Systemen wie Datastore vertraut sind, verweist dieses Dokument auf entsprechende Konzepte in relationalen Datenbanken. In diesem Dokument wird davon ausgegangen, dass Sie mit Datastore vertraut sind. Der Einstieg in Datastore gelingt am leichtesten in Google App Engine, wenn 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 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 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 haben sich nicht relationale Datenbanken, die auch als NoSQL-Datenbanken bezeichnet werden, als Alternative zu relationalen Datenbanken entwickelt. Datastore ist eine der am häufigsten verwendeten nicht relationalen Datenbanken in der Branche. Im Jahr 2013 verarbeitete Datastore 4,5 Billionen Transaktionen pro Monat (Google Cloud Platform-Blogpost). 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. Datastore bietet außerdem eine Reihe von Features, 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 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 beruhen relationale Datenbanken auf dem Konzept strikter Konsistenz, 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 strikte Konsistenz 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 sind die Bereitstellungstopologie und der Replikationsprozess mit strikter Konsistenz dargestellt. Sie können in diesem Diagramm sehen, dass Replikate immer mit dem Ursprungsknoten übereinstimmende Werte haben, aber ein Zugriff darauf erst nach Abschluss der Aktualisierung möglich ist.

Abbildung 2: Konzeptionelle Darstellung einer Replikation mit strikter Konsistenz

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, Leistungsfähigkeit und Hochverfügbarkeit erfordern. Mit nicht relationalen Datenbanken können die Entwickler für jede Anwendung einen optimalen Ausgleich zwischen strikter Konsistenz und Eventual Consistency erzielen. Auf diese Weise lassen sich die Vorteile beider Modelle kombinieren. Anwendungsfälle, bei denen strikte Konsistenz 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 strikte Konsistenz 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 strikte Konsistenz 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 Datastore kennen. In den folgenden Abschnitten wird erläutert, wie Modelle mit Eventual Consistency und Modelle mit strikter Konsistenz 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 Datastore

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

Datastore API

Entitätswert lesen

Index lesen

Globale Abfrage

Eventual Consistency

Eventual Consistency

Globale Abfrage (nur Schlüssel)

Eventual Consistency

Ancestor-Abfrage

Strikte Konsistenz

Strikte Konsistenz

Suche nach Schlüssel (get())

Strikte Konsistenz

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

Datastore-Abfragen ohne Ancestor werden als globale Abfragen bezeichnet und sind für die Einbindung in ein Eventual-Consistency-Modell vorgesehen. Dies garantiert keine strikte Konsistenz. 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", ein Attribut, "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 des Attributs "Spielstand" der zurückgegebenen Entität unverändert angezeigt wird.

Dieses Verhalten beruht auf der Replikation zwischen Datastore-Servern. Die Replikation wird von Cloud Bigtable und Megastore verwaltet, den zugrunde liegenden Technologien für 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 bestätigt 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 Abschluss 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 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 Attributwerte 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 Datastore eingefügt wird. Die Entität hat ein Attribut, "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 das Attribut "Spielstand" zum Zeitpunkt der Ausführung der Abfrage nicht mit dem neu eingefügten Wert aktualisiert wurde.

Beachten Sie, dass zwar alle Abfragen in Datastore auf Indextabellen angewendet werden, die Aktualisierungen der Indextabellen jedoch asynchron sind. 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 Attributwert von 0 für "Spielstand" 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.

Strikte Konsistenz beim Lesen von Entitätswerten und Indexen

In Datastore gibt es nur zwei APIs, die eine strikt 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 strikte Konsistenz erfordert, sollte der Entwickler eine dieser Methoden zum Lesen von Entitäten aus Datastore nutzen.

Datastore ist speziell auf die Bereitstellung strikter Konsistenz mithilfe dieser APIs ausgelegt. Wenn Sie eine dieser APIs aufrufen, überträgt 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, in Datastore ist eine Ancestor-Abfrage die einzige Möglichkeit, einer Anforderung für strikte Konsistenz 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 Datastore darin, dass Entwickler einen optimalen Ausgleich zwischen strikter Konsistenz und Eventual Consistency erreichen können. In Datastore ist eine Entitätengruppe eine Einheit mit strikter Konsistenz, Transaktionalität und einem Ort. Mithilfe von Entitätengruppen können Entwickler den Bereich der strikten Konsistenz 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 Konzepts der Entitätengruppe

Innerhalb der Entitätengruppe sind die folgenden Eigenschaften garantiert:

  • Strikte Konsistenz
    • Eine Ancestor-Abfrage nach 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.
  • Ort
    • Entitäten in einer Entitätengruppe werden an nahe gelegenen Orten auf 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 strikter Konsistenz ausgeführt. Im Hintergrund sorgt Datastore dafür, dass alle ausstehenden Replikationen und Indexaktualisierungen angewendet werden, bevor die Abfrage ausgeführt wird.

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 von einem bestimmten Typ 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, was 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 die Ancestor-Organisation 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 das Ancestor-Objekt in der Anfrage nach dem Namen der Person angegeben 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 Datastore aufrechterhalten

Entitätengruppen können für die Aufrechterhaltung der Konsistenz zwischen Memcache-Einträgen und 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. Damit die Daten im Cache mit den neuesten Werten in 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 für 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 möglicherweise 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 eines Attributs 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 strikter Konsistenz.

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 strikte Konsistenz 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 ein Datastore-Feature, mit dem 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 strikt konsistente Abfragen verwenden müssen, z. B. Suchvorgänge nach Schlüsseln und Ancestor-Abfragen. 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 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 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 Attributwerte 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 eine Anwendungsanforderung es zulässt, dass der Indexwert zum Zeitpunkt der Abfrage noch nicht konsistent ist.

Memcache verwenden

Der Memcache-Dienst ist zwar flüchtig, aber strikt konsistent. Durch die Kombination von Memcache-Suchvorgängen und Datastore-Abfragen ist es also möglich, ein System zu erstellen, das die Konsistenzprobleme in den meisten Fällen 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 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 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 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 Datastore haben sich einige Verfahren bewährt:

  • Fangen Sie Memcache-Ausnahmen und -Fehler ab, um die Konsistenz zwischen dem Memcache-Wert und dem 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 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 beim Aktualisieren der Einträge das Feature 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 strikte Konsistenz benötigt wird, ist es am besten, die Anwendung beruhend 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, die strikte Konsistenz erfordern, deaktiviert sind. 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 von Nutzern oder Terabyte an Datastore-Entitäten ist es möglich, dass eine unzweckmäßige Nutzung von 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 Datastore als automatisch generierten Standardschlüssel eine Abfolge kleiner Ganzzahl-IDs mit allgemein 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 erläutert, speichert Datastore Entitäten jedoch in lexikografischer Reihenfolge der Schlüsselnamen. Diese Entitäten werden also sehr wahrscheinlich auf denselben Datastore-Servern abgelegt. Wenn eine Anwendung sehr starken Traffic generiert, könnte diese fortlaufende Nummerierung eine Konzentration der Vorgänge auf einem bestimmten Server bewirken, was bezüglich der Konsistenz zu längeren Latenzzeiten führen kann.

Im App Engine SDK 1.8.1 wurde für 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 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 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 Datastore führt die Aktualisierung einer Entität zu einer Aktualisierung sämtlicher Indexe, die für diesen Entitätstyp definiert sind. Nähere Informationen finden Sie unter Lebensdauer eines Datastore-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 Datastore.

Eine Vorgehensweise besteht darin, Datastore und BigQuery für die Erfüllung unterschiedlicher Geschäftsanforderungen zu kombinieren. Nutzen Sie 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 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 Attribute explizit vorzugeben (siehe Attribute und Werttypen). Standardmäßig erstellt Datastore eine unterschiedliche Indextabelle für jedes indexierbare Attribut eines Entitätstyps. Wenn also 100 Attribute 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 Best Practice wäre dann, Attribute 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 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 Consistency und strikter Konsistenz zu beherrschen, um ein optimales Datenmodell für eine Anwendung zu konzipieren. In Datastore besteht die optimale Vorgehensweise zur Gewährleistung von strikter Konsistenz in einem Entitätenbereich darin, Entitätengruppen und Ancestor-Abfragen zu verwenden. 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, Datastore mit BigQuery zu kombinieren, um die geschäftlichen Anforderungen für komplexe Abfragen zu erfüllen und die Nutzung von Datastore-Indexen 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.