Mit der verteilten Architektur von Spanner können Sie Ihr Schema so entwerfen, dass Hotspots vermieden werden. Hotspots entstehen, wenn zu viele Anfragen an denselben Server gesendet werden, wodurch die Ressourcen des Servers überlastet werden und möglicherweise hohe Latenzen auftreten.
Auf dieser Seite werden Best Practices für das Entwerfen von Schemas beschrieben, mit denen Hotspots vermieden werden können. Eine Möglichkeit, Hotspots zu vermeiden, besteht darin, das Schemadesign so anzupassen, dass Spanner die Daten auf mehrere Server aufteilen und verteilen kann. Durch die Verteilung der Daten auf mehrere Server kann Ihre Spanner-Datenbank effizienter arbeiten, insbesondere bei Bulk-Dateneinfügungen.
Spanner erkennt automatisch Möglichkeiten, Best Practices für das Schemadesign anzuwenden. Wenn Empfehlungen für eine Datenbank verfügbar sind, können Sie sie auf der Seite Spanner Studio für diese Datenbank ansehen. Weitere Informationen finden Sie unter Empfehlungen für Best Practices für Schemadesign ansehen.
Primärschlüssel zur Vermeidung von Hotspots auswählen
Eine häufige Ursache für Hotspots ist die Verwendung eines Schlüssels, der monoton zu- oder abnimmt, z. B. ein Zeitstempel. Monotone Schlüssel führen dazu, dass alle neuen Einträge in denselben Bereich Ihres Schlüsselbereichs geschrieben werden. Da Spanner Schlüsselbereiche verwendet, um Daten auf mehrere Server zu verteilen, leitet ein monotoner Schlüssel den gesamten Einfügungstraffic an einen einzelnen Server weiter, wodurch ein Engpass entsteht.
Angenommen, Sie möchten eine Spalte mit dem Zeitstempel des letzten Zugriffs für Zeilen der Tabelle UserAccessLogs beibehalten. Bei der folgenden Tabellendefinition wird ein auf einem Zeitstempel basierender Primärschlüssel im ersten Schlüsselteil verwendet. Wir empfehlen dies nicht, wenn die Tabelle eine hohe Einfügungsrate aufweist:
GoogleSQL
CREATE TABLE UserAccessLogs ( LastAccess TIMESTAMP NOT NULL, UserId STRING(1024), ... ) PRIMARY KEY (LastAccess, UserId);
PostgreSQL
CREATE TABLE useraccesslogs ( lastaccess timestamptz NOT NULL, userid text, ... 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 Spanner-Server alle Schreibvorgänge erhält und somit überlastet wird.
In diesem Diagramm wird die Problematik dargestellt:
Die vorherige Tabelle UserAccessLogs 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 hervor, in der Spanner die Zeilen einfügt (die beschrifteten Pfeile geben die Reihenfolge der Schreibvorgänge für die Zeilen an). Da die Einfügungen nach Zeitstempel geordnet werden und der Zeitstempelwert stetig zunimmt, fügt Spanner die Einfügungen immer am Ende der Tabelle ein und weist sie demselben Split zu. Wie unter Schema und Datenmodell erläutert, besteht ein Split aus einer Reihe von Zeilen aus einer oder mehreren verbundenen Tabellen, die von Spanner in der Reihenfolge des Zeilenschlüssels gespeichert werden.
Problematisch ist dabei, dass 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 im vorherigen Bild verdeutlicht wird. In dieser vereinfachten Abbildung wird von jedem Server höchstens ein Split verarbeitet. Spanner kann jedoch jedem Server mehr als einen Split zuweisen.
Wenn Spanner weitere Zeilen an die Tabelle anhängt, wird der Split größer und Spanner erstellt nach Bedarf neue Splits. Weitere Informationen zum Erstellen von Splits finden Sie unter Lastbasierte Aufteilung. Spanner hängt nachfolgende neue Zeilen an diesen neuen Split an 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.
Universally Unique Identifier (UUID) verwenden
Sie können als
Primärschlüssel eine UUID (Universally Unique Identifier) gemäß
RFC 9562 verwenden. Wir empfehlen die Version 4 der UUID
, da bei dieser Version in der Bitsequenz zufällige Werte verwendet werden. Wir empfehlen keine UUIDs der Version 1, da bei dieser Version der Zeitstempel in den Bits höherer Ordnung gespeichert wird. Sie können UUID-Werte der Version 4 in einer UUID-Spalte in Spanner speichern.
Berücksichtigen Sie Folgendes, bevor Sie sich für die Verwendung von UUIDs entscheiden:
- Sie funktionieren unabhängig vom Inhalt des Datensatzes. Im Gegensatz zu semantischen Schlüsseln wie
SingerIdundAlbumIdist eine UUID eine eindeutige Kennung, die nicht mit den Daten selbst zusammenhängt. - Sie behalten keine Lokalität zwischen verknüpften Datensätzen bei. Daher können durch die Nutzung von UUIDs Hotspots vermieden werden.
Für eine UUID Spalte können Sie die Spanner
NEW_UUID()
GoogleSQL-Funktion oder die
gen_random_uuid() PostgreSQL-Funktion verwenden, um UUID-Werte zu erstellen.
Beispiel für die folgende Tabelle:
GoogleSQL
CREATE TABLE UserAccessLogs (
LogEntryId UUID DEFAULT (NEW_UUID()),
LastAccess TIMESTAMP NOT NULL,
UserId STRING(1024)
) PRIMARY KEY (LogEntryId);
PostgreSQL
CREATE TABLE useraccesslogs (
logentryid uuid PRIMARY KEY DEFAULT gen_random_uuid(),
lastaccess timestamptz NOT NULL,
userid text);
Sie können die generierte UUID-Funktion verwenden, um neue LogEntryId-Werte zu erstellen.
GoogleSQL
INSERT INTO UserAccessLogs (LastAccess, UserId)
VALUES ('2016-01-25 10:10:10.555555-05:00', 'TomSmith');
PostgreSQL
INSERT INTO UserAccessLogs (LastAccess, UserId)
VALUES ('2016-01-25 10:10:10.555555-05:00', 'TomSmith');
Für eine UUID Spalte können Sie die Spanner
NEW_UUID()
GoogleSQL-Funktion oder die
gen_random_uuid()
PostgreSQL-Funktion als Standardwert für die Spalte verwenden, damit
Spanner automatisch UUID-Werte generiert.
Beispiel für die folgende Tabelle:
GoogleSQL
CREATE TABLE UserAccessLogs (
LogEntryId UUID NOT NULL,
LastAccess TIMESTAMP NOT NULL,
UserId STRING(1024),
...
) PRIMARY KEY (LogEntryId);
PostgreSQL
CREATE TABLE useraccesslogs (
logentryid uuid PRIMARY KEY NOT NULL,
lastaccess timestamptz NOT NULL,
userid text);
Sie können GoogleSQL NEW_UUID() oder PostgreSQL gen_random_uuid() einfügen, um die LogEntryId-Werte zu generieren.
Diese Funktionen erzeugen einen UUID-Wert. Daher muss für die Spalte LogEntryId der Typ UUID für GoogleSQL oder PostgreSQL verwendet werden.
GoogleSQL
INSERT INTO
UserAccessLogs (LogEntryId, LastAccess, UserId)
VALUES
(NEW_UUID(), '2016-01-25 10:10:10.555555-05:00', 'TomSmith');
PostgreSQL
INSERT INTO
useraccesslogs (logentryid, lastaccess, userid)
VALUES
(gen_random_uuid(),'2016-01-25 10:10:10.555555-05:00', 'TomSmith');
Sie können auch UUID-Werte einfügen, die Sie an anderer Stelle generiert haben, z. B. in Ihrer Backend-Anwendung. UUIDs sind unabhängig davon, wo sie generiert werden, eindeutig.
GoogleSQL
INSERT INTO
UserAccessLogs (LogEntryId, LastAccess, UserId)
VALUES
('4192bff0-e1e0-43ce-a4db-912808c32493', '2016-01-25 10:10:10.555555-05:00', 'TomSmith');
PostgreSQL
INSERT INTO
useraccesslogs (logentryid, lastaccess, userid)
VALUES
('4192bff0-e1e0-43ce-a4db-912808c32493','2016-01-25 10:10:10.555555-05:00', 'TomSmith');
Bit-Umkehrungen für sequenzielle Werte
Sie sollten prüfen, ob numerische Primärschlüssel (INT64 in GoogleSQL oder bigint in PostgreSQL) nicht sequenziell zu- oder abnehmen. Sequenzielle Primärschlüssel können bei großer Skalierung zu Hotspots führen. Eine Möglichkeit, dieses Problem zu vermeiden, besteht darin, die sequenziellen Werte bitweise umzukehren und darauf zu achten, dass die Primärschlüsselwerte gleichmäßig über den Schlüsselbereich verteilt werden.
Spanner unterstützt die bitweise umgekehrte Sequenz, die eindeutige bitweise umgekehrte Ganzzahlwerte generiert. Sie können eine Sequenz in der ersten (oder einzigen) Komponente eines Primärschlüssels verwenden, um Hotspot-Probleme zu vermeiden. Weitere Informationen finden Sie unter siehe Bitweise umgekehrte Sequenz.
Schlüsselreihenfolge vertauschen
Eine Möglichkeit, Schreibvorgänge gleichmäßiger über den Schlüsselbereich zu verteilen, besteht darin, die Reihenfolge der Schlüssel so zu ändern, dass die Spalte mit dem monotonen Wert nicht der erste Schlüsselteil ist:
GoogleSQL
CREATE TABLE UserAccessLogs ( UserId INT64 NOT NULL, LastAccess TIMESTAMP NOT NULL, ... ) PRIMARY KEY (UserId, LastAccess);
PostgreSQL
CREATE TABLE useraccesslogs ( userid bigint NOT NULL, lastaccess TIMESTAMPTZ 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. 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.
Das folgende Bild zeigt die fünf Zeilen aus der Tabelle UserAccessLogs, die von Spanner nach UserId und nicht nach dem Zeitstempel des Zugriffs angeordnet werden:

Hierbei kann Spanner die UserAccessLogs-Daten in drei Splits aufteilen, wobei jeder Split etwa tausend der Reihe nach geordnete Zeilen von UserId-Werten enthält. Die Nutzerereignisse liegen zwar nur etwa eine Millisekunde auseinander, jedes Ereignis wurde jedoch von einem anderen Nutzer ausgeführt. Deshalb ist es im Vergleich zu der Anordnung nach Zeitstempel wesentlich unwahrscheinlicher, dass die Reihenfolge der Einfügungen zu einem Hotspot führt. Weitere Informationen zum Erstellen von Splits finden Sie unter
Lastbasierte Aufteilung
Weitere Informationen finden Sie in der verwandten Best Practice für 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 Datenbankserver 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 UserAccessLogs auf N logische Shards verteilen möchten, fügen Sie am Anfang der Tabelle eine Schlüsselspalte ShardId ein:
GoogleSQL
CREATE TABLE UserAccessLogs ( ShardId INT64 NOT NULL, LastAccess TIMESTAMP NOT NULL, UserId INT64 NOT NULL, ... ) PRIMARY KEY (ShardId, LastAccess, UserId);
PostgreSQL
CREATE TABLE useraccesslogs ( shardid bigint NOT NULL, lastaccess TIMESTAMPTZ NOT NULL, userid bigint NOT NULL, ... PRIMARY KEY (shardid, lastaccess, userid) );
Zum Berechnen der ShardId hashen Sie eine Kombination der Primärschlüsselspalten und berechnen dann den Modulo N des Hashs. Beispiel:
GoogleSQL
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-Werts zum Erstellen dreier logischer Fragmentierungen der Durchsatz für Schreibvorgänge gleichmäßiger auf die Server verteilt werden kann:

In diesem Fall wird die Tabelle UserAccessLogs nach ShardId sortiert, die als Hash-Funktion der Schlüsselspalten berechnet wird. Die fünf UserAccessLogs-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 einer generierten Spalte erstellen.
Verwenden Sie dazu in GoogleSQL zur Schreibzeit die FARM_FINGERPRINT Funktion, wie im folgenden Beispiel gezeigt:
GoogleSQL
CREATE TABLE UserAccessLogs (
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);
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 die folgenden Faktoren berücksichtigen:
- Vermeidung von Hotspots. 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.
Wenn Sie eine ShardId verwenden, um Hotspots zu vermeiden, beachten Sie die folgenden Richtlinien, um den Wert von N, der Anzahl der logischen Shards, auszuwählen:
N mit der Anzahl der Knoten korrelieren:Legen Sie N auf die Anzahl der Knoten fest, die Ihre Instanz voraussichtlich haben wird. Wenn Sie beispielsweise erwarten, dass Ihre Instanz auf 10 Knoten skaliert wird, ist ein Wert von N=10 ein effektiver Ausgangspunkt. So kann Spanner die Schreiblast gleichmäßig auf die Knoten verteilen.
N ist ein statischer Wert:Wenn Sie N nach der ersten Einrichtung ändern, ist eine Schemaaktualisierung und möglicherweise ein Daten-Backfill erforderlich. Daher müssen Sie einen Wert für N auswählen, der Ihren Skalierungsanforderungen entspricht.
Vermeiden Sie übermäßig große Werte für N:Es mag verlockend sein, einen sehr großen Wert für N zu wählen, um sich auf Wachstum vorzubereiten, aber das ist in der Regel nicht erforderlich. Mehr Shards als physische Server verbessern die Leistung im Vergleich zu den zusätzlichen Spanner-Kosten nicht wesentlich. N an die Anzahl der Knoten anzupassen, ist eine effektive Strategie zur Verteilung der Arbeitslast.
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 neuesten Verlauf lesen möchten, eine verschachtelte
Tabelle für den Verlauf verwenden und die übergeordnete 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
LIMITdie 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. 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:
GoogleSQL
CREATE TABLE UserAccessLogs ( UserId INT64 NOT NULL, LastAccess TIMESTAMP NOT NULL, ... ) PRIMARY KEY (UserId, LastAccess DESC);
Best Practice 2 für das Schemadesign: Die absteigende oder aufsteigende Reihenfolge hängt von den Nutzerabfragen ab, z. B. ob die neuesten oder die ältesten Einträge oben stehen sollen.
Wann ein verschachtelter Index verwendet werden sollte
Ähnlich wie beim vorherigen Beispiel für Primärschlüssel, das Sie vermeiden sollten, ist es keine gute Idee, nicht verschachtelte 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.
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, das in der vorherigen Best Practice beschrieben wurde, da Spanner Indexe als verkappte Tabellen implementiert und die resultierende Indextabelle eine Spalte verwendet, deren erster Schlüsselteil monoton zunimmt.
Es ist in Ordnung, einen verschachtelten Index zu erstellen, bei dem die Zeilen des letzten Zugriffs in den entsprechenden Nutzerzeilen verschachtelt sind. Es ist unwahrscheinlich, dass eine einzige übergeordnete Zeile Tausende von Ereignissen pro Sekunde erzeugt.
GoogleSQL
CREATE NULL_FILTERED INDEX UsersByLastAccess ON Users(UserId, LastAccess), INTERLEAVE IN Users;
PostgreSQL
CREATE INDEX usersbylastaccess ON users(userid, lastaccess) WHERE lastaccess IS NOT NULL, INTERLEAVE IN Users;
Best Practice 3 für das Schemadesign: Erstellen Sie keinen nicht verschachtelten Index für eine Spalte mit hoher Schreibrate, deren Wert monoton zu- oder abnimmt. Verwenden Sie einen verschachtelten Index oder Techniken wie beim Entwerfen des Primärschlüssels der Basistabelle, wenn Sie Indexspalten entwerfen, z. B. `shardId`.
Nächste Schritte
- Beispiele für Schemadesigns ansehen
- Informationen zum Laden von Daten im Bulk .