Best Practices für Schemadesign

Mit der verteilten Architektur von Spanner können Sie Ihr Schema so entwerfen, dass Hotspots vermieden werden. Das sind Situationen, in denen zu viele Anfragen an denselben Server gesendet werden, was die Ressourcen des Servers überlastet und möglicherweise zu hohen Latenzen führt.

Auf dieser Seite werden Best Practices zum Entwerfen von Schemas beschrieben, die Sie vermeiden sollten. und Hotspots entstehen. Eine Möglichkeit, Hotspots zu vermeiden, besteht darin, das Schemadesign so anzupassen, dass Spanner die Daten auf mehrere Server aufteilen und verteilen kann. Die Verteilung von Daten auf mehrere Server unterstützt den Betrieb Ihrer Spanner-Datenbank besonders bei Bulk-Dateneinfügungen.

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

Wie unter Schema und Datenmodell beschrieben, sollten Sie bei der Auswahl eines Primärschlüssels im Schemadesign vorsichtig vorgehen, damit Sie nicht versehentlich Hotspots in der Datenbank erzeugen. Eine Ursache für Hotspots ist eine Spalte, deren Wert monoton ändert sich als erster Schlüsselteil, da dies dazu führt, dass alle Einfügungen das Ende des Schlüsselraums. Dieses Muster ist nicht erwünscht, da Spanner verwendet Schlüsselbereiche, um die Daten auf die Server aufzuteilen. Das bedeutet, dass alle Einfügungen die an einen Server geleitet werden, der die gesamte Arbeit übernimmt.

Angenommen, Sie möchten eine Spalte mit dem Zeitstempel des letzten Zugriffs für Zeilen der Tabelle UserAccessLog beibehalten. In der folgenden Tabellendefinition wird ein zeitstempelbasierten Primärschlüssel als ersten Schlüsselteil. Wir empfehlen dies nicht, wenn in die Tabelle häufig Daten eingefügt werden:

GoogleSQL

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

PostgreSQL

CREATE TABLE UserAccessLog (
LastAccess TIMESTAMPTZ NOT NULL,
UserId bigint NOT NULL,
...
PRIMARY KEY (LastAccess, UserId)
);

Das Problem ist, dass Zeilen in der Reihenfolge der letzten und weil sich die Zeitstempel des letzten Zugriffs ständig erhöhen, werden sie immer an das Ende der Tabelle gesetzt. Der Hotspot entsteht dadurch, dass ein einzelner Spanner-Server alle Schreibvorgänge erhält und somit überlastet wird.

Das folgende Diagramm veranschaulicht dieses Problem:

Nach Zeitstempel sortierte UserAccessLog-Tabelle mit entsprechendem Hotspot

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 Spanner die Zeilen einfügt, 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 in den Schema und Datenmodell, Split ist ein Satz von Zeilen aus einer oder mehreren verbundenen Tabellen, in der Reihenfolge des Zeilenschlüssels gespeichert.)

Dies ist problematisch, da Spanner verschiedenen Servern in der Split-Einheiten, sodass der Server, der diesem Split zugewiesen ist, die alle Einfügeanfragen verarbeitet. Je häufiger Nutzerzugriffe stattfinden, desto häufiger erhält der entsprechende Server Einfügungsanfragen. Der Server wird dann anfällig für Hotspots und sieht die rote Rahmenlinie und den Hintergrund. In dieser vereinfachten Illustration Jeder Server verarbeitet höchstens einen Split. Tatsächlich kann Spanner jedoch jedem Server mehrere Splits zuweisen.

Wenn Spanner weitere Zeilen an die Tabelle anhängt, wird der Split größer. ca. 8 GB erreicht hat, erstellt Spanner eine weitere wie unter Lastbasierte Aufteilung beschrieben. Spanner hängt nachfolgende neue Zeilen an diesen neuen Split an und der Server die der Aufteilung zugewiesen ist, wird zum neuen potenziellen Hotspot.

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.

Universally Unique Identifier verwenden

Sie können als Primärschlüssel eine UUID (Universally Unique Identifier) gemäß RFC 4122 verwenden. Wir empfehlen die Verwendung der UUID Version 4, da werden zufällige Werte in der Bitsequenz verwendet. UUIDs der Version 1 werden nicht empfohlen weil sie den Zeitstempel in den Bits höherer Ordnung speichern.

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.

Für eine STRING(36)-Spalte können Sie den Spanner-GENERATE_UUID() verwenden (GoogleSQL oder PostgreSQL) als Spaltenstandardwert für Spanner automatisch UUID-Werte generieren kann.

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. Ein Primärschlüssel von SingerId und AlbumId haben eine inhärente Bedeutung, eine UUID jedoch nicht.
  • Da die Lokalität zwischen verwandten Einträgen verloren geht, ist die Verwendung einer UUID beseitigt Hotspots.

Bit-Umkehrungen für sequenzielle Werte

Sie sollten darauf achten, dass numerisch (INT64 in GoogleSQL oder bigint in PostgreSQL) Primärschlüssel ändern sich nicht sequentiell. Sequenzielle primäre Schlüssel können in großem Maßstab zu Heißlaufen führen. Eine Möglichkeit, besteht das Problem darin, die sequentiellen Werte in Bit-Umkehrung zu kehren, Primärschlüsselwerte gleichmäßig über den Schlüsselbereich verteilt sind.

Spanner unterstützt eine bitumkehrte Sequenz, die eindeutige Bitumgekehrte Ganzzahlwerte. Sie können eine Sequenz in der ersten (oder nur) Komponente in einem Primärschlüssel, um Heißlaufensprobleme zu vermeiden. Weitere Informationen finden Sie unter Bit-reversed sequence.

Schlüsselreihenfolge vertauschen

Eine Möglichkeit, Schreibvorgänge gleichmäßiger über den Schlüsselbereich zu verteilen, besteht darin, die Reihenfolge sodass die Spalte mit dem monotonen Wert nicht der erster wichtiger Teil:

GoogleSQL

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

PostgreSQL

CREATE TABLE UserAccessLog (
LastAccess TIMESTAMPTZ NOT NULL,
UserId bigint NOT NULL,
...
PRIMARY KEY (UserId, LastAccess)
);

In diesem geänderten Schema werden die Einfügungen nun nach UserId und nicht in chronologischer Reihenfolge nach dem Zeitstempel des letzten Zugriffs angeordnet. Dieses Schema verteilt Schreibvorgänge auf da es unwahrscheinlich ist, dass ein einzelner Nutzer Tausende von Ereignissen pro Sekunde.

Im Folgenden sehen Sie die fünf Zeilen aus der Tabelle UserAccessLog. die Spanner mit UserId statt mit dem Zugriffszeitstempel anordnet:

Nach UserId sortierte UserAccessLog-Tabelle mit ausgeglichenem Schreibdurchsatz

Hier teilt Spanner die UserAccessLog-Daten in drei Splits auf, wobei jeder Split etwa tausend Zeilen mit geordneten UserId-Werten enthält. Dies ist eine vernünftige Schätzung, wie die Nutzerdaten aufgeteilt werden könnten, vorausgesetzt, Zeile enthält etwa 1 MB Nutzerdaten mit einer maximalen Aufteilungsgröße von etwa 8 GB. Auch wenn die Nutzerereignisse wird jedes Ereignis von einem anderen Nutzer ausgelöst. bei Einfügungen ist es viel geringer, dass ein Hotspot entsteht als Zeitstempel für die Sortierung.

Siehe auch die verwandte Best Practice zur Reihenfolge des Zeitstempels Schlüssel.

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 Datenbankservern. In einer logisch fragmentierten Datenbank definieren die Daten in der Tabelle die Shards. 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:

GoogleSQL

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

PostgreSQL

CREATE TABLE UserAccessLog (
ShardId bigint NOT NULL,
LastAccess TIMESTAMPTZ NOT NULL,
UserId bigint NOT NULL,
...
PRIMARY KEY (ShardId, LastAccess, UserId)
);

Hashen Sie zum Berechnen des ShardId eine Kombination der Primärschlüsselspalten und dann Modulo N des Hashwerts berechnen. Beispiel:

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. Spanner erstellt dann Splits für die Zeilen, um die Leistung zu optimieren.

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.

Mit Spanner können Sie auch eine Hash-Funktion in einem generierte Spalte.

Verwenden Sie dazu in Google SQL die Methode FARM_FINGERPRINT während der Schreibzeit, wie im folgenden Beispiel gezeigt:

GoogleSQL

CREATE TABLE UserAccessLog (
ShardId INT64 NOT NULL
AS (MOD(FARM_FINGERPRINT(CAST(LastAccess AS STRING)), 2048)) STORED,
LastAccess TIMESTAMP NOT NULL,
UserId    INT64 NOT NULL,
) PRIMARY KEY (ShardId, LastAccess, UserId);

Die ausgewählte Hash-Funktion bestimmt, wie gut die Einfügungen gestreut werden im gesamten Schlüsselbereich. Sie benötigen keinen kryptografischen Hash, obwohl ein könnte ein kryptografischer Hash eine gute Wahl sein. Wenn Sie eine Hash-Funktion auswählen, sind folgende Faktoren zu berücksichtigen:

  • Hotspot-Vermeidung. 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.

Bei zeitstempelbasierten Schlüsseln absteigende Reihenfolge verwenden

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

  • Wenn Sie den aktuellen Verlauf lesen möchten, verwenden Sie ein für den Verlauf verschränkt. in der übergeordneten Zeile lesen. 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 sollten Sie mit den neuesten Einträgen beginnen und die Einträge der Reihe nach lesen. ältere Einträge, bis die Bedingung erfüllt ist. Das funktioniert bei Spanner. effizienter für Zeitstempelschlüssel, die Spanner in absteigend sortiert.

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

GoogleSQL

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

Best Practice 2 für das Schemadesign: Absteigende oder aufsteigende Reihenfolge hängt von den Nutzeranfragen ab, z. B. ist „Top“ die neueste oder „Top“ die älteste.

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

Ähnlich wie beim vorherigen Primärschlüsselbeispiel, das Sie vermeiden sollten, ist es auch ein schlechte Idee, nicht verschränkte Indexe für Spalten zu erstellen, deren Werte monoton zu- oder abnehmen, auch wenn es sich nicht um Primärschlüsselspalten handelt.

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

GoogleSQL

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

PostgreSQL

CREATE TABLE Users (
UserId     bigint NOT NULL,
LastAccess TIMESTAMPTZ,
...
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:

GoogleSQL

CREATE NULL_FILTERED INDEX UsersByLastAccess ON Users(LastAccess);

PostgreSQL

CREATE INDEX UsersByLastAccess ON Users(LastAccess)
WHERE LastAccess IS NOT NULL;

Dies führt jedoch zum selben Problem wie in den vorherigen Da Spanner Indexe im Hintergrund als Tabellen implementiert, und die resultierende Indextabelle eine Spalte verwendet, deren Wert monoton zunimmt, als ersten wichtigen Teil an.

Es ist jedoch in Ordnung, einen verschachtelten Index wie diesen zu erstellen, verschränkte Indexe in entsprechenden übergeordneten Zeilen verschränkt. Es ist unwahrscheinlich, dass eine einzelne übergeordnete Zeile Tausende von Ereignissen pro Sekunde erzeugt.

Best Practice Nr. 3 für das Schemadesign: Kein nicht verschränkter Index auf einer Spalte mit hoher Schreibrate, deren Wert monoton steigt oder sinkt. Anstelle von verschränkten Indexen sollten Sie Techniken wie die verwenden, die Sie verwenden würden. für das Primärschlüsseldesign der Basistabelle, wenn Sie Indexspalten entwerfen – für Fügen Sie beispielsweise „shardId“ hinzu.

Nächste Schritte