Best Practices für Schemadesign

Auf dieser Seite werden Best Practices für das Entwerfen von Cloud Spanner-Schemas beschrieben, die Sie dabei unterstützen, Hotspots zu vermeiden und Daten in Cloud Spanner zu laden.

Primärschlüssel zur Vermeidung von Hotspots auswählen

Wie unter Schema und Datenmodell beschrieben, sollten Sie bei der Auswahl eines Primärschlüssels vorsichtig vorgehen, damit Sie nicht versehentlich Hotspots in der Datenbank erzeugen. Hotspots können entstehen, wenn Sie eine Spalte auswählen, in der der Wert des ersten Schlüsselteils monoton zunimmt. Dies führt dazu, dass alle Einfügungen am Ende des Schlüsselbereichs vorgenommen werden. Dieses Muster ist nicht wünschenswert, weil die Daten von Cloud Spanner abhängig von den Schlüsselbereichen auf die Server verteilt werden. Somit werden alle Einfügungen zu einem einzigen Server geleitet, der die gesamte Arbeitslast bewältigen muss.

Angenommen, Sie möchten eine Zeitstempelspalte für den letzten Zugriff in den Zeilen der Tabelle UserAccessLog beibehalten. Die folgende Tabellendefinition, die einen zeitstempelbasierten Primärschlüssel als ersten Schlüsselteil verwendet, ist ein Anti-Muster, wenn die Tabelle eine hohe Einfügungsrate aufweist:

-- ANTI-PATTERN: USING A COLUMN WHOSE VALUE MONOTONICALLY INCREASES OR
-- DECREASES AS THE FIRST KEY PART OF A HIGH WRITE RATE TABLE

CREATE TABLE UserAccessLog (
  LastAccess TIMESTAMP NOT NULL,
  UserId     INT64 NOT NULL,
  ...
) PRIMARY KEY (LastAccess, UserId);

Das Problem liegt hier darin, dass die Zeilen in der Reihenfolge des Zeitstempels des letzten Zugriffs in die Tabelle geschrieben werden. Da die Zeitstempel des letzten Zugriffs sich stetig erhöhen, werden sie immer an das Ende der Tabelle geschrieben. Der Hotspot entsteht dadurch, dass ein einzelner Cloud Spanner-Server alle Schreibvorgänge erhält und somit überlastet wird.

In diesem Diagramm wird die Problematik dargestellt:

UserAccessLog-Tabelle nach Zeitstempel mit dem entsprechenden Hotspot sortiert

Die oben dargestellte Tabelle UserAccessLog enthält fünf Beispieldatenzeilen, die fünf verschiedene Nutzer darstellen, wobei alle fünf eine Nutzeraktion im Abstand von etwa einer Millisekunde voneinander ausführen. Aus dem Diagramm geht auch die Reihenfolge, in der die Zeilen eingefügt werden, hervor (die beschrifteten Pfeile geben die Reihenfolge der Schreibvorgänge für die Zeilen an). Da die Einfügungen nach Zeitstempel sortiert werden und der Zeitstempelwert stetig zunimmt, werden die Einfügungen immer am Ende der Tabelle vorgenommen und demselben Split zugewiesen. Wie unter Schema und Datenmodell erläutert, besteht ein Split aus einer Reihe von Zeilen aus einer oder mehreren verbundenen Tabellen, die in der Reihenfolge des Zeilenschlüssels gespeichert werden.

Problematisch ist dabei, dass Cloud Spanner verschiedenen Servern Arbeit in Split-Einheiten zuweist, sodass der diesem Split zugewiesene Server alle Einfügungsanfragen alleine verarbeitet. Je häufiger Nutzerzugriffe stattfinden, desto häufiger erhält der entsprechende Server Einfügungsanfragen. Der Server läuft dann Gefahr, zu einem Hotspot zu werden, was durch den roten Rahmen und Hintergrund oben verdeutlicht wird. Beachten Sie, dass in dieser vereinfachten Abbildung von jedem Server höchstens ein Split verarbeitet wird. Tatsächlich kann jedoch jedem Cloud Spanner-Server mehr als ein Split zugewiesen werden.

Je mehr Zeilen an die Tabelle angehängt werden, desto größer wird der Split. Wenn die maximale Größe (etwa 4 GB) erreicht ist, wird von Cloud Spanner ein neuer Split erstellt, wie unter Lastbasierte Aufteilung beschrieben. Nachfolgende neue Zeilen werden an diesen neuen Split angehängt und der Server, der diesem Split zugewiesen wird, stellt damit den neuen potenziellen Hotspot dar.

Beim Auftreten von Hotspots können Sie beobachten, dass Einfügungen langsam verarbeitet werden und auch andere Arbeiten auf demselben Server langsamer vorangehen. Die Änderung der Reihenfolge der Spalte LastAccess in aufsteigender Reihenfolge löst dieses Problem nicht, da dann alle Schreibvorgänge stattdessen am Anfang der Tabelle eingefügt werden. Auch in diesem Fall würden alle Einfügungen an einen einzigen Server gesendet.

Best Practice 1 für das Schemadesign: Wählen Sie keine Spalte aus, deren Wert als erster Schlüssel für eine Tabelle mit hoher Schreibrate monoton zu- oder abnimmt.

Schlüsselreihenfolge vertauschen

Eine Methode zum Verteilen der Schreibvorgänge über den Schlüsselbereich besteht darin, die Reihenfolge der Schlüssel so zu ändern, dass die Spalte mit dem monoton zu- oder abnehmenden Wert nicht der erste Schlüsselteil ist:

CREATE TABLE UserAccessLog (
  UserId     INT64 NOT NULL,
  LastAccess TIMESTAMP NOT NULL,
  ...
) PRIMARY KEY (UserId, LastAccess);

In diesem geänderten Schema werden Einfügungen jetzt zuerst nach UserId und nicht nach dem chronologischen Zeitstempel des letzten Zugriffs sortiert. Mit diesem Schema werden die Schreibvorgänge auf unterschiedliche Splits verteilt, da es unwahrscheinlich ist, dass ein einzelner Nutzer Tausende von Ereignissen pro Sekunde erzeugt.

Im folgenden Diagramm werden die fünf Zeilen aus der Tabelle UserAccessLog nach UserId und nicht nach dem Zeitstempel des Zugriffs angeordnet:

Nach UserId sortierte UserAccessLog-Tabelle mit ausgeglichenem Schreibdurchsatz

Hierbei werden die UserAccessLog-Daten in drei Splits aufgeteilt, wobei jeder Split tausend der Reihe nach geordnete Zeilen von UserId-Werten enthält. Dies ist eine realistische Schätzung, wie Nutzerdaten aufgeteilt werden können. Jede Zeile enthält etwa 1 MB Nutzerdaten und die maximale Aufteilungsgröße beträgt ca. 4 GB. Obwohl die Nutzerereignisse im Abstand von etwa einer Millisekunde aufgetreten sind, wurde jedes Ereignis von einem anderen Nutzer ausgelöst, sodass die Reihenfolge der Einfügungen im Vergleich zur Sortierung nach Zeitstempel viel weniger wahrscheinlich ist.

Weitere Informationen finden Sie in der verwandten Best Practice Auf dem Zeitstempel basierende Schlüssel anordnen.

Eindeutigen Schlüssel hashen und Schreibvorgänge auf logische Shards aufteilen

Die Last kann auch auf mehrere Server verteilt werden. Erstellen Sie zu diesem Zweck eine Spalte, die den Hash des eindeutigen Schlüssels enthält, und nutzen Sie diese Hash-Spalte (oder die Hash-Spalte und die Spalten mit dem eindeutigen Schlüssel) als Primärschlüssel. Mit diesem Muster können Hotspots vermieden werden, da neue Zeilen gleichmäßiger über den Schlüsselbereich verteilt werden.

Sie können den Hash-Wert verwenden, um logische Shards oder Partitionen in einer Datenbank zu erstellen. In einer physisch fragmentierten Datenbank sind die Zeilen auf mehrere Datenbanken verteilt. In einer logisch fragmentierten Datenbank werden die Shards durch die Daten in der Tabelle definiert. Wenn Sie zum Beispiel Schreibvorgänge in die Tabelle UserAccessLog auf N logische Shards verteilen möchten, fügen Sie am Anfang der Tabelle eine Schlüsselspalte ShardId ein:

CREATE TABLE UserAccessLog (
  ShardId     INT64 NOT NULL,
  LastAccess  TIMESTAMP NOT NULL,
  UserId      INT64 NOT NULL,
  ...
) PRIMARY KEY (ShardId, LastAccess, UserId);

Zum Berechnen der ShardId hashen Sie eine Kombination der Primärschlüsselspalten und berechnen den Modulo N des Hashs: ShardId = hash(LastAccess and UserId) % N. Durch die Auswahl der Hash-Funktion und die Kombination der Spalten bestimmen Sie, wie die Zeilen über den Schlüsselbereich verteilt werden. Cloud Spanner erstellt dann Splits für die Zeilen, um die Leistung zu optimieren. Beachten Sie, dass die Splits eventuell nicht den logischen Shards entsprechen.

Im folgenden Diagramm wird dargestellt, wie durch die Verwendung eines Hash zum Erstellen dreier logischer Shards der Durchsatz für Schreibvorgänge gleichmäßiger auf die Server verteilt werden kann:

Nach ShardID sortierte UserAccessLog-Tabelle mit ausgeglichenem Schreibdurchsatz

In diesem Fall wird die Tabelle UserAccessLog nach ShardId sortiert, die als Hash-Funktion der Schlüsselspalten berechnet wird. Die fünf UserAccessLog-Zeilen werden in drei logische Shards aufgeteilt, die sich zufälligerweise jeweils in einem anderen Split befinden. Die Einfügungen werden gleichmäßig auf die Splits aufgeteilt. So wird auch der Durchsatz für Schreibvorgänge gleichmäßig auf die drei Server verteilt, die die Splits verarbeiten.

Durch Auswählen der Hash-Funktion bestimmen Sie, wie gut die Einfügungen über den Schlüsselbereich verteilt werden. Ein kryptografischer Hash ist hier nicht zielführend, kann in einem anderen Fall aber durchaus geeignet sein. Beim Auswählen einer Hash-Funktion müssen Sie mehrere Faktoren berücksichtigen:

  • Hotspots vermeiden. Eine Funktion, die zu mehr Hashwerten führt, reduziert in der Regel Hotspots.
  • Leseeffizienz. Lesevorgänge in allen Hashwerten sind schneller, wenn weniger Hashwerte zu scannen sind.
  • Knotenanzahl.

Universally Unique Identifier verwenden

Sie können als Primärschlüssel eine UUID (Universally Unique Identifier) gemäß RFC 4122 verwenden. Wir empfehlen die Version 4 der UUID, da bei dieser Version in der Bitsequenz zufällige Werte verwendet werden. Bei der Version 1 der UUID wird der Zeitstempel in den Bits höherer Ordnung gespeichert. Daher empfehlen wir diese Version nicht.

Die UUID kann auf verschiedene Arten als Primärschlüssel gespeichert werden:

  • In einer STRING(36)-Spalte.
  • In einem INT64-Spaltenpaar
  • In einer BYTES(16)-Spalte.

Die Verwendung einer UUID bringt einige Nachteile mit sich:

  • Ihre beträchtliche Größe belegt mindestens 16 Byte. Andere mögliche Primärschlüssel erfordern nicht so viel Speicherplatz.
  • Sie enthalten keine Informationen zum Datensatz. Zum Beispiel hat ein Primärschlüssel aus "SingerId" und "AlbumId" im Gegensatz zu einer UUID eine inhärente Bedeutung.
  • Die Lokalität zwischen Datensätzen, die sich aufeinander beziehen, geht verloren. Daher können durch die Nutzung von UUIDs Hotspots vermieden werden.

Bit-Umkehrungen für sequenzielle Werte

Wenn Sie numerische eindeutige Primärschlüssel generieren, sollten die Bits höherer Ordnung nachfolgender Zahlen ungefähr gleichmäßig über den gesamten Zahlenbereich verteilt werden. Eine Methode besteht darin, sequenzielle Zahlen auf konventionelle Weise zu generieren, um dann durch Bit-Umkehrung die endgültigen Werte zu erhalten.

Durch das Umkehren der Bits werden für die Primärschlüssel eindeutige Werte beibehalten. Nur das Speichern des umgekehrten Werts ist erforderlich, da Sie den ursprünglichen Wert im Anwendungscode neu berechnen können.

Zeilengröße begrenzen

Für eine optimale Leistung sollte die Größe einer Zeile weniger als 4 GB betragen. Die Zeilengröße bezieht sich auf die oberste Zeilenebene sowie die verschränkten untergeordneten Zeilen und Indexzeilen. Normalerweise erstellt Cloud Spanner einen neuen Split, wenn ein vorhandener Split eine Größe von 4 GB erreicht hat. Das Aufteilen ist in Cloud Spanner nur auf Basis der obersten Zeilenebene möglich. Wenn eine Zeile größer als 4 GB ist, wird der Durchsatz für Schreibvorgänge eventuell beeinträchtigt.

Hotspots durch verschränkte Tabellen verhindern

Cloud Spanner kann Splits nur auf Basis der obersten Zeilenebene erstellen. Hier ein Beispiel mit drei verschränkten Tabellen:

-- Schema hierarchy:
-- + Singers
--   + Albums (interleaved table, child table of Singers)
--     + Songs (interleaved table, child table of Albums)

Cloud Spanner erstellt die Splits so, dass sich alle Alben und Songs für jeden Sänger im selben Split befinden. Falls die Songs eines Sängers zu einem Hotspot für Lese- oder Schreibvorgänge werden, kann Cloud Spanner die Tabelle Songs nicht auf mehrere Server verteilen. Wenn Songs hingegen eine Tabelle der obersten Ebene ist, kann Cloud Spanner songbasierte Splits erstellen:

-- Schema hierarchy:
-- + Singers (top-level table)
--   + Albums (interleaved table, child table of Singers)
-- + Songs (top-level table)

Bei zeitstempelbasierten Schlüsseln absteigende Reihenfolge verwenden

Wenn Sie für Ihren Verlauf eine Tabelle mit Zeitstempel haben, sollten Sie eine absteigende Reihenfolge für die Schlüsselspalte(n) in Betracht ziehen, wenn eine der folgenden Bedingungen zutrifft:

  • Wenn Sie für den Verlauf eine verschachtelte Tabelle verwenden, lesen Sie auch die übergeordnete Zeile. In diesem Fall werden bei einer DESC-Zeitstempelspalte die neuesten Verlaufseinträge neben der übergeordneten Zeile gespeichert. Andernfalls erfordert das Lesen der übergeordneten Zeile und des neusten Verlaufs einen Suchvorgang in der Mitte, um den älteren Verlauf zu überspringen.
  • Wenn Sie sequenzielle Einträge in umgekehrter chronologischer Reihenfolge lesen und nicht genau wissen, wie weit Sie zurückgehen. Sie können beispielsweise mit einer SQL-Abfrage mit einem LIMIT die neuesten N-Ereignisse abrufen oder Sie brechen möglicherweise den Lesevorgang ab, nachdem Sie eine bestimmte Anzahl von Zeilen gelesen haben. In diesen Fällen möchten Sie mit den neuesten Einträgen beginnen und sequenziell ältere Einträge lesen, bis die Bedingung erfüllt ist. Cloud Spanner ist bei Zeitstempelschlüsseln in absteigender Reihenfolge effizienter.

Fügen Sie das Schlüsselwort DESC hinzu, damit Sie den Zeitstempelschlüssel in absteigender Reihenfolge festlegen. Beispiel:

CREATE TABLE UserAccessLog (
  UserId     INT64 NOT NULL,
  LastAccess TIMESTAMP NOT NULL,
  ...
) PRIMARY KEY (UserId, LastAccess DESC);

Best Practice 2 für das Schemadesign: Verwenden Sie bei zeitstempelbasierten Schlüsseln absteigende Reihenfolge.

Verschränkte Indexe für eine Spalte, deren Wert monoton zu- oder abnimmt, verwenden

Ähnlich wie beim vorherigen Anti-Pattern für Primärschlüssel ist es keine gute Idee, nicht verschränkte Indexe für Spalten mit monoton zu- oder abnehmenden Werten zu erstellen, selbst wenn sie keine Primärschlüsselspalten sind.

Beispiel: Angenommen, Sie definieren die folgende Tabelle, in der LastAccess keine Primärschlüsselspalte ist.

CREATE TABLE Users (
  UserId     INT64 NOT NULL,
  LastAccess TIMESTAMP,
  ...
) PRIMARY KEY (UserId);

Es mag auf den ersten Blick praktisch erscheinen, einen Index für die Spalte LastAccess zu definieren, um die Nutzerzugriffe "seit dem Zeitpunkt X" schnell aus der Datenbank abrufen zu können:

-- ANTI-PATTERN: CREATING A NON-INTERLEAVED INDEX ON A COLUMN WHOSE VALUE
-- MONOTONICALLY INCREASES OR DECREASES ON A HIGH WRITE RATE COLUMN

CREATE NULL_FILTERED INDEX UsersByLastAccess ON Users(LastAccess)

Dies führt jedoch zum selben Problem, das in der vorherigen Best Practice beschrieben wurde, da Indexe als verkappte Tabellen implementiert werden und die resultierende Indextabelle eine Spalte verwenden würde, deren erster Schlüsselteil monoton zunimmt.

Es ist dagegen in Ordnung, einen verschachtelten Index nach diesem Muster zu erstellen, da die Zeilen von verschachtelten Indexen in den entsprechenden übergeordneten Zeilen verschachtelt werden. Es ist unwahrscheinlich, dass eine einzige übergeordnete Zeile Tausende von Ereignissen pro Sekunde erzeugt.

Best Practice 3 für das Schemadesign: Erstellen Sie keinen nicht verschränkten Index für eine Spalte mit hoher Schreibrate, deren Wert monoton zu- oder abnimmt.

Nächste Schritte