L'architettura distribuita di Spanner consente di progettare lo schema per evitare hotspot , ovvero situazioni in cui vengono inviate troppe richieste allo stesso server, che satura le risorse del server e causa potenzialmente latenze elevate.
Questa pagina descrive le best practice per la progettazione degli schemi al fine di evitare la creazione di hotspot. Un modo per evitare gli hotspot è modificare la progettazione dello schema in modo da consentire a Spanner di suddividere e distribuire i dati su più server. La distribuzione dei dati tra i server aiuta il database Spanner a operare in modo efficiente, in particolare quando si eseguono inserimenti di dati in blocco.
Spanner rileva automaticamente le opportunità per applicare le best practice di progettazione dello schema. Se sono disponibili consigli per un database, puoi visualizzarli nella pagina Spanner Studio del database. Per ulteriori informazioni, consulta Visualizzare i consigli sulle best practice di progettazione dello schema.
Scegli una chiave primaria per evitare gli hotspot
Una causa comune di hotspot è l'utilizzo di una chiave che aumenta o diminuisce monotonicamente, ad esempio un timestamp. Le chiavi monotoniche fanno sì che tutte le nuove voci vengano scritte nello stesso intervallo dello spazio delle chiavi. Poiché Spanner utilizza gli intervalli di chiavi per distribuire i dati tra i server, una chiave monotona indirizza tutto il traffico di inserimento a un singolo server, creando un collo di bottiglia.
Supponiamo, ad esempio, di voler mantenere una colonna di timestamp dell'ultimo accesso nelle righe della tabella UserAccessLogs. La seguente definizione della tabella utilizza una chiave primaria basata su timestamp come prima parte della chiave. Non è consigliabile se la tabella ha una frequenza di inserimento elevata:
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) );
Il problema è che le righe vengono scritte in questa tabella in base all'ordine del timestamp dell'ultimo accesso e, poiché i timestamp dell'ultimo accesso aumentano sempre, vengono sempre scritti alla fine della tabella. L'hotspot viene creato perché un singolo server Spanner riceve tutte le scritture, sovraccaricando il server.
Il seguente diagramma illustra questo problema:
La tabella UserAccessLogs precedente include cinque righe di dati di esempio, che rappresentano cinque utenti diversi che eseguono un'azione utente a circa un millisecondo di distanza l'uno dall'altro. Il diagramma annota anche l'ordine in cui Spanner inserisce le righe (le frecce etichettate indicano l'ordine delle scritture per ogni riga). Poiché gli inserimenti sono ordinati in base al timestamp e il valore del timestamp è sempre crescente, Spanner aggiunge sempre gli inserimenti alla fine della tabella e li indirizza alla stessa suddivisione. (Come
descritto in Schema e modello di
dati, una suddivisione è un insieme di
righe di una o più tabelle correlate che Spanner
archivia in base all'ordine della chiave di riga.)
Questo è problematico perché Spanner assegna il lavoro a server diversi in unità di suddivisioni, quindi il server assegnato a questa suddivisione specifica finisce per gestire tutte le richieste di inserimento. Man mano che aumenta la frequenza degli eventi di accesso utente, aumenta anche la frequenza delle richieste di inserimento al server corrispondente. Il server diventa quindi soggetto a diventare un hotspot e appare come il bordo e lo sfondo rossi mostrati nell'immagine precedente. In questa illustrazione semplificata, ogni server gestisce al massimo una suddivisione, ma Spanner può assegnare a ogni server più di una suddivisione.
Quando Spanner aggiunge altre righe alla tabella, la suddivisione aumenta e Spanner crea nuove suddivisioni in base alle esigenze. Per scoprire di più su come vengono create le suddivisioni, consulta Suddivisione basata sul carico. Spanner aggiunge le nuove righe successive a questa nuova suddivisione e il server assegnato alla suddivisione diventa il nuovo potenziale hotspot.
Quando si verificano hotspot, potresti notare che gli inserimenti sono lenti e che anche altri lavori sullo stesso server potrebbero rallentare. La modifica dell'ordine della colonna LastAccess in ordine crescente non risolve il problema perché tutte le scritture vengono inserite nella parte superiore della tabella, il che invia comunque tutti gli inserimenti a un singolo server.
Best practice di progettazione dello schema n. 1: non scegliere una colonna il cui valore aumenta o diminuisce monotonicamente come prima parte della chiave per una tabella con frequenza di scrittura elevata.
Utilizza un identificatore univoco universale (UUID)
Puoi utilizzare un identificatore univoco universale (UUID) come definito da
RFC 9562 come chiave primaria. Ti consigliamo di utilizzare
UUID versione 4
, perché utilizza valori casuali nella sequenza di bit. Non consigliamo gli UUID versione 1 perché memorizzano il timestamp nei bit di ordine superiore. Puoi archiviare i valori UUID versione 4 in una colonna UUID su Spanner.
Prima di decidere di utilizzare gli UUID, tieni presente quanto segue:
- Funzionano indipendentemente dal contenuto del record. A differenza delle chiavi semantiche come
SingerIdeAlbumId, un UUID è strettamente un identificatore univoco non correlato ai dati stessi. - Non mantengono la località tra i record correlati, motivo per cui l'utilizzo di un UUID elimina gli hotspot.
Per una colonna UUID, puoi utilizzare la funzione GoogleSQL
NEW_UUID()
di Spanner o la funzione PostgreSQL
gen_random_uuid() per creare valori UUID.
Ad esempio, per la seguente tabella:
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);
Puoi utilizzare la funzione UUID generata per creare nuovi valori LogEntryId.
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');
Per una colonna UUID, puoi utilizzare la funzione GoogleSQL
NEW_UUID()
di Spanner o la funzione PostgreSQL
gen_random_uuid()
come valore predefinito della colonna in modo che
Spanner generi automaticamente i valori UUID.
Ad esempio, per la seguente tabella:
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);
Puoi inserire NEW_UUID() GoogleSQL o gen_random_uuid() PostgreSQL per generare i valori LogEntryId.
Queste funzioni producono un valore UUID, quindi la colonna LogEntryId deve utilizzare il tipo UUID per GoogleSQL o PostgreSQL.
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');
Puoi anche inserire valori UUID che hai generato altrove, ad esempio nella tua applicazione di backend. Questo perché gli UUID sono univoci, indipendentemente da dove vengono generati.
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');
Esegui l'inversione dei bit dei valori sequenziali
Devi verificare che le chiavi primarie numeriche (INT64 in GoogleSQL o bigint in PostgreSQL) non aumentino o diminuiscano in sequenza. Le chiavi primarie sequenziali possono causare hotspot su larga scala. Un modo per evitare questo problema è invertire i bit dei valori sequenziali, assicurandosi di distribuire uniformemente i valori chiave primaria nello spazio delle chiavi.
Spanner supporta la sequenza con bit invertiti, che genera valori interi univoci con bit invertiti. Puoi utilizzare una sequenza nel primo (o unico) componente di una chiave primaria per evitare problemi di hotspot. Per ulteriori informazioni, consulta Sequenza con bit invertiti.
Scambia l'ordine delle chiavi
Un modo per distribuire le scritture in modo più uniforme nello spazio delle chiavi è scambiare l'ordine delle chiavi in modo che la colonna che contiene il valore monotono non sia la prima parte della chiave:
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 questo schema modificato, gli inserimenti vengono ora ordinati prima in base a UserId, anziché in base al timestamp dell'ultimo accesso cronologico. Questo schema distribuisce le scritture tra suddivisioni diverse perché è improbabile che un singolo utente generi migliaia di eventi al secondo.
L'immagine seguente mostra le cinque righe della tabella UserAccessLogs che Spanner ordina con UserId anziché con il timestamp di accesso:

In questo caso, Spanner potrebbe dividere i dati UserAccessLogs in tre suddivisioni, ognuna contenente circa mille righe di valori UserId ordinati. Anche se gli eventi utente si sono verificati a circa un millisecondo di distanza, ogni evento è stato generato da un utente diverso, quindi è molto meno probabile che l'ordine degli inserimenti crei un hotspot rispetto all'utilizzo del timestamp per l'ordinamento. Per
scoprire di più su come vengono create le suddivisioni, consulta
Suddivisione basata sul carico
Consulta anche la best practice correlata per l'ordinamento delle chiavi basate su timestamp.
Esegui l'hashing della chiave univoca e distribuisci le scritture tra gli shard logici
Un'altra tecnica comune per distribuire il carico su più server è creare una colonna che contenga l'hash della chiave univoca effettiva, quindi utilizzare la colonna hash (o la colonna hash insieme alle colonne delle chiavi univoche) come chiave primaria. Questo pattern aiuta a evitare gli hotspot, perché le nuove righe vengono distribuite in modo più uniforme nello spazio delle chiavi.
Puoi utilizzare il valore hash per creare shard logici o partizioni nel database. In un database con sharding fisico, le righe vengono distribuite su più server di database. In un database con sharding logico, i dati nella tabella definiscono gli shard. Ad esempio, per distribuire le scritture nella tabella UserAccessLogs su N shard logici, puoi anteporre una colonna chiave ShardId alla tabella:
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) );
Per calcolare ShardId, esegui l'hashing di una combinazione delle colonne di chiave primaria e poi calcola il modulo N dell'hash. Ad esempio:
GoogleSQL
ShardId = hash(LastAccess and UserId) % N
La scelta della funzione hash e della combinazione di colonne determina la modalità di distribuzione delle righe nello spazio delle chiavi. Spanner creerà quindi suddivisioni tra le righe per ottimizzare le prestazioni.
Il seguente diagramma illustra come l'utilizzo di un hash per creare tre shard logici può distribuire la velocità effettiva di scrittura in modo più uniforme tra i server:

Qui la tabella UserAccessLogs è ordinata in base a ShardId, che viene calcolato come funzione hash delle colonne chiave. Le cinque righe UserAccessLogs sono suddivise in tre shard logici, ognuno dei quali si trova in una suddivisione diversa. Gli inserimenti vengono distribuiti uniformemente tra le suddivisioni, il che bilancia la velocità effettiva di scrittura sui tre server che gestiscono le suddivisioni.
Spanner ti consente anche di creare una funzione hash in una colonna generata.
Per farlo in GoogleSQL, utilizza la FARM_FINGERPRINT funzione durante il tempo di scrittura, come mostrato nell'esempio seguente:
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);
La scelta della funzione hash determina la qualità della distribuzione degli inserimenti nell'intervallo di chiavi. Non è necessario un hash crittografico, anche se potrebbe essere una buona scelta. Quando scegli una funzione hash, devi considerare i seguenti fattori:
- Evitare gli hotspot. Una funzione che genera più valori hash tende a ridurre gli hotspot.
- Efficienza di lettura. Le letture di tutti i valori hash sono più veloci se ci sono meno valori hash da analizzare.
- Conteggio nodi.
Quando utilizzi un ShardId per evitare gli hotspot, utilizza le seguenti linee guida per scegliere il valore di N, il numero di shard logici:
Correlare N con il numero di nodi: imposta N in modo che sia uguale al numero di nodi che prevedi che la tua istanza abbia. Ad esempio, se prevedi che la tua istanza possa fare lo scale up fino a 10 nodi, un valore di N=10 è un punto di partenza efficace. In questo modo, Spanner distribuisce uniformemente il carico di scrittura tra i nodi.
N è un valore statico: la modifica di N dopo la configurazione iniziale richiede un aggiornamento dello schema e potenzialmente un backfill dati. Pertanto, devi scegliere un valore per N che possa soddisfare le tue esigenze di scalabilità.
Evita valori eccessivamente grandi per N: anche se potrebbe essere allettante scegliere un valore molto grande per N per prepararsi alla crescita, in genere non è necessario. Un numero di shard superiore a quello dei server fisici non migliorerà in modo significativo le prestazioni rispetto al costo aggiuntivo di Spanner. L'allineamento di N con il numero di nodi è una strategia efficace per distribuire il workload.
Disponi le chiavi basate su timestamp in ordine decrescente
Se hai una tabella per la cronologia che utilizza il timestamp come chiave, valuta la possibilità di utilizzare l'ordine decrescente per la colonna chiave se si verifica una delle seguenti condizioni:
- Se vuoi leggere la cronologia più recente, stai utilizzando una tabella con interleaving
per la cronologia e stai leggendo la riga principale. In questo caso, con una colonna timestamp
DESC, le voci della cronologia più recenti vengono archiviate accanto alla riga principale. In caso contrario, la lettura della riga principale e della cronologia recente richiederà una ricerca nel mezzo per saltare la cronologia precedente. - Se stai leggendo le voci sequenziali in ordine cronologico inverso e
non sai esattamente quanto indietro stai andando. Ad esempio, potresti utilizzare una query SQL con un
LIMITper ottenere gli N eventi più recenti oppure potresti pianificare di annullare la lettura dopo aver letto un certo numero di righe. In questi casi, devi iniziare con le voci più recenti e leggere in sequenza le voci precedenti finché non viene soddisfatta la condizione, cosa che Spanner fa in modo più efficiente per le chiavi timestamp che Spanner archivia in ordine decrescente.
Aggiungi la parola chiave DESC per rendere decrescente la chiave timestamp. Ad esempio:
GoogleSQL
CREATE TABLE UserAccessLogs ( UserId INT64 NOT NULL, LastAccess TIMESTAMP NOT NULL, ... ) PRIMARY KEY (UserId, LastAccess DESC);
Best practice di progettazione dello schema n. 2: l'ordine decrescente o crescente dipende dalle query utente, ad esempio la parte superiore è la più recente o la parte superiore è la più vecchia.
Quando utilizzare un indice con interleaving
Per indicazioni sulla scelta tra indici con interleaving e indici globali, consulta Scegliere tra indici con interleaving e indici globali.
Analogamente all'esempio di chiave primaria precedente che dovresti evitare, è anche una cattiva idea creare indici senza interleaving su colonne i cui valori aumentano o diminuiscono monotonicamente, anche se non sono colonne di chiave primaria.
Supponiamo, ad esempio, di definire la seguente tabella, in cui LastAccess è una colonna non di chiave primaria:
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) );
Potrebbe sembrare conveniente definire un indice sulla colonna LastAccess per eseguire rapidamente query sul database per gli accessi utente "dal momento X", come questa:
GoogleSQL
CREATE NULL_FILTERED INDEX UsersByLastAccess ON Users(LastAccess);
PostgreSQL
CREATE INDEX usersbylastaccess ON users(lastaccess) WHERE lastaccess IS NOT NULL;
Tuttavia, questo comporta lo stesso problema descritto nella best practice precedente, perché Spanner implementa gli indici come tabelle sottostanti e la tabella dell'indice risultante utilizza una colonna il cui valore aumenta monotonicamente come prima parte della chiave.
È possibile creare un indice con interleaving in cui le righe dell'ultimo accesso sono intercalate sotto la riga utente corrispondente. Questo perché è improbabile che una singola riga principale generi migliaia di eventi al secondo.
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 di progettazione dello schema n. 3: non creare un indice senza interleaving su una colonna con frequenza di scrittura elevata il cui valore aumenta o diminuisce monotonicamente. Utilizza un indice con interleaving o tecniche come quelle che utilizzeresti per la progettazione della chiave primaria della tabella di base quando progetti le colonne dell'indice, ad esempio aggiungi `shardId`.
Passaggi successivi
- Esamina gli esempi di progettazione dello schema.
- Scopri di più sul caricamento in blocco dei dati.