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 potenzialmente causa elevate latente.
Questa pagina descrive le best practice per progettare gli schemi in modo da evitare di creare hot spot. 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 su più server consente al database Spanner di funzionare in modo efficiente, in particolare quando vengono eseguite inserzioni collettive di dati.
Scegli una chiave primaria per evitare hotspot
Come menzionato in Schema e modello dei dati, devi fare attenzione quando scegli una chiave primaria nella progettazione dello schema per non creare accidentalmente hotspot nel tuo per configurare un database. Una causa degli hotspot è la presenza di una colonna il cui valore cambia monotonicamente come prima parte della chiave, perché tutti gli inserimenti avvengono alla fine dello spazio della chiave. Questo modello non è auspicabile perché Spanner utilizza intervalli di chiavi per suddividere i dati tra i server, il che significa che tutti gli inserimenti sono indirizzati a un singolo server che finisce per svolgere tutto il lavoro.
Ad esempio, supponiamo che tu voglia mantenere una colonna di timestamp dell'ultimo accesso sulle righe
della tabella UserAccessLog
. La seguente definizione di tabella utilizza una chiave primaria basata su timestamp come prima parte della chiave. Lo sconsigliamo se
la tabella registra un tasso di inserimento elevato:
GoogleSQL
CREATE TABLE UserAccessLogs ( LastAccess TIMESTAMP NOT NULL, UserId STRING(1024), ... ) PRIMARY KEY (LastAccess, UserId);
PostgreSQL
CREATE TABLE useraccesslog ( lastaccess timestamptz NOT NULL, userid text, ... PRIMARY KEY (lastaccess, userid) );
Il problema è che le righe vengono scritte in questa tabella in ordine di timestamp dell'ultimo accesso e, poiché i timestamp dell'ultimo accesso sono sempre in aumento, vengono sempre scritti alla fine della tabella. L'hotspot viene creato perché Il server Spanner riceve tutte le scritture, sovraccaricandole server web.
Il seguente diagramma illustra questo problema:
La tabella UserAccessLog
precedente include cinque righe di dati di esempio, che
rappresentare cinque utenti diversi che eseguono un'azione utente su un
millisecondi l'uno dall'altro. Il diagramma annota anche l'ordine in cui
Spanner inserisce le righe (le frecce etichettate indicano l'ordine
di scritture per ogni riga). Poiché gli inserimenti sono ordinati per timestamp e la
il valore del timestamp è sempre in aumento, Spanner aggiunge sempre
vengono inseriti alla fine della tabella e li indirizza alla stessa suddivisione. (come
in Schema e dati.
modello, una suddivisione è un insieme
di righe di una o più tabelle correlate che Spanner
negozi in ordine di chiave di riga).
Questo è un problema perché Spanner assegna il lavoro a diversi server in unità di suddivisioni, quindi il server assegnato a questa particolare suddivisione finisce per gestire tutte le richieste di inserimento. Come frequenza degli eventi di accesso dell'utente aumenta, anche la frequenza delle richieste di inserimento al server corrispondente aumenta. Il server diventa quindi incline a diventare un hotspot e ha l'aspetto di 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ù suddivisioni.
Quando Spanner aggiunge altre righe alla tabella, la suddivisione aumenta e quando raggiunge circa 8 GB, Spanner crea un'altra suddivisione, come descritto in Basate sul carico dei dati. Spanner appende 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 altre attività sul medesimo server potrebbero rallentare. Modifica dell'ordine di LastAccess
colonna in ordine crescente non risolve il problema perché tutte le scritture
vengono invece inserite nella parte superiore della tabella, che invia comunque tutti gli inserimenti
a un singolo server.
Best practice n. 1 per la progettazione dello schema: non scegliere una colonna la cui valore aumenta o diminuisce monotonicamente come prima parte di chiave per un e scrivere la tabella delle tariffe.
Utilizza un UUID (Universally Unique Identifier)
Puoi utilizzare un UUID (Universally Unique Identifier) come definito dal documento RFC 4122 come chiave primaria. Ti consigliamo di utilizzare la versione 4 dell'UUID, perché utilizza valori casuali nella sequenza di bit. Sconsigliamo gli UUID versione 1 perché memorizzano il timestamp nei bit di ordine superiore.
Esistono diversi modi per memorizzare l'UUID come chiave principale:
- In una colonna
STRING(36)
. - In una coppia di
INT64
colonne. - In una colonna
BYTES(16)
.
Per una colonna STRING(36)
, puoi utilizzare la funzione GENERATE_UUID()
di Spanner (GoogleSQL o
PostgreSQL) come valore predefinito della colonna per fare in modo che
Spanner generi automaticamente i valori UUID.
Ad esempio, per la seguente tabella:
GoogleSQL
CREATE TABLE UserAccessLogs (
LogEntryId STRING(36) NOT NULL,
LastAccess TIMESTAMP NOT NULL,
UserId STRING(1024),
...
) PRIMARY KEY (LogEntryId, LastAccess, UserId);
PostgreSQL
CREATE TABLE useraccesslog (
logentryid VARCHAR(36) NOT NULL,
lastaccess timestamptz NOT NULL,
userid text,
...
PRIMARY KEY (lastaccess, userid)
);
Puoi inserire GENERATE_UUID()
per generare i valori LogEntryId
.
GENERATE_UUID()
produce un valore STRING
, pertanto la colonna LogEntryId
deve utilizzare il tipo STRING
per GoogleSQL o il tipo text
per PostgreSQL.
GoogleSQL
INSERT INTO
UserAccessLog (LogEntryId, LastAccess, UserId)
VALUES
(GENERATE_UUID(), '2016-01-25 10:10:10.555555-05:00', 'TomSmith');
PostgreSQL
INSERT INTO
useraccesslog (logentryid, lastaccess, userid)
VALUES
(spanner.generate_uuid(),'2016-01-25 10:10:10.555555-05:00', 'TomSmith');
L'utilizzo di un UUID presenta alcuni svantaggi:
- Sono leggermente grandi e utilizzano almeno 16 byte. Altre opzioni per le chiavi principali non utilizzano tanto spazio di archiviazione.
- Non contengono informazioni sul record. Ad esempio, una chiave primaria di
SingerId
eAlbumId
ha un significato intrinseco, mentre un UUID no. - Perdi la località tra i record correlati: ecco perché usare un UUID elimina gli hotspot.
Valori sequenziali inversi in bit
Devi assicurarti che i valori numerici (INT64
in GoogleSQL o
bigint
in PostgreSQL) le chiavi primarie non aumentano o aumentano in modo sequenziale
in diminuzione. 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 i valori della chiave primaria in modo uniforme nello spazio delle chiavi.
Spanner supporta la sequenza con inversione dei bit, che genera valori interi con inversione dei bit univoci. Puoi utilizzare una sequenza nella prima (o solo) in una chiave primaria per evitare problemi relativi agli hotspot. Per ulteriori informazioni, consulta la sezione Sequenza con inversione di bit.
Scambia l'ordine delle chiavi
Un modo per distribuire le scritture sullo spazio della chiave in modo più uniforme è scambiare l'ordine delle chiavi in modo che la colonna che contiene il valore monotonico non sia prima parte fondamentale:
GoogleSQL
CREATE TABLE UserAccessLog ( UserId INT64 NOT NULL, LastAccess TIMESTAMP NOT NULL, ... ) PRIMARY KEY (UserId, LastAccess);
PostgreSQL
CREATE TABLE useraccesslog ( userid bigint NOT NULL, lastaccess TIMESTAMPTZ NOT NULL, ... PRIMARY KEY (UserId, LastAccess) );
In questo schema modificato, gli inserimenti ora vengono ordinati per UserId
, anziché
in base al timestamp dell'ultimo accesso. Questo schema suddivide le scritture tra diverse suddivisioni perché è improbabile che un singolo utente produca migliaia di eventi al secondo.
L'immagine seguente mostra le cinque righe della tabella UserAccessLog
che
Spanner ordina con UserId
anziché con il timestamp di accesso:
In questo caso, Spanner suddivide i dati UserAccessLog
in tre suddivisioni, ciascuna contenente circa mille righe di valori UserId
ordinati. Si tratta di una stima ragionevole di come potrebbero essere suddivisi i dati utente, assumendo che ogni riga contenga circa 1 MB di dati utente e data una dimensione massima della suddivisione di circa 8 GB. Anche se gli eventi utente si sono verificati
a circa un millisecondo l'uno dall'altro, ogni evento è stato generato da un utente diverso, quindi
è molto meno probabile che crei un hotspot rispetto all'uso
timestamp per l'ordine.
Consulta anche la best practice correlata per ordinare le chiavi basate su timestamp.
Esegui l'hashing della chiave univoca e distribuisci le scritture tra shard logici
Un'altra tecnica comune per distribuire il carico su più server è creare una colonna contenente 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 consente di evitare gli hotspot, perché le nuove righe sono più distribuite in modo uniforme sullo spazio dei tasti.
Puoi utilizzare il valore hash per creare partizioni o shard logici nel database. In un database con partizioni fisiche, le righe sono distribuite su più
server di database. In un database con sharding logico, i dati della tabella definiscono
gli shard. Ad esempio, per distribuire le scritture nella tabella UserAccessLog
tra N
shard logici, puoi anteporre alla tabella una colonna chiave ShardId
:
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) );
Per calcolare ShardId
, esegui l'hash di una combinazione di colonne della 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 frammenti logici può distribuire in modo più uniforme la produttività in scrittura tra i server:
Qui la tabella UserAccessLog
è ordinata in base a ShardId
, che viene calcolato come funzione hash delle colonne chiave. Le cinque righe UserAccessLog
sono suddivise in
tre shard logici, ognuno dei quali è coincidente con una suddivisione diversa. La
gli inserimenti vengono distribuiti uniformemente tra le suddivisioni, il che bilancia la velocità effettiva di scrittura
dai tre server che gestiscono le suddivisioni.
Spanner ti consente anche di creare una funzione di hashing in una colonna generata.
Per farlo in GoogleSQL, utilizza la funzione FARM_FINGERPRINT durante la scrittura, come mostrato nell'esempio seguente:
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);
La scelta della funzione hash determina l'efficacia 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, considerare i seguenti fattori:
- Evitamento di 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 sono presenti meno valori hash da eseguire la scansione.
- Conteggio nodi.
Utilizza l'ordine decrescente per le chiavi basate su timestamp
Se hai una tabella per la cronologia che utilizza il timestamp come chiave, considera utilizzando l'ordine decrescente per la colonna chiave se si applica una delle seguenti condizioni:
- Per leggere la cronologia più recente, è in uso un modello
per la cronologia e stai leggendo la riga padre. In questo caso, con una colonna di timestamp
DESC
, le voci della cronologia più recenti vengono memorizzate adiacenti alla riga principale. Altrimenti, se leggi la riga padre e la relativa riga cronologia richiederà una ricerca al centro per saltare la cronologia precedente. - Se leggi voci sequenziali in ordine cronologico inverso e
non sai esattamente quanto indietro stai andando a ritroso. Ad esempio,
usa una query SQL con
LIMIT
per ottenere i N eventi più recenti oppure annullare la lettura dopo aver letto un certo numero di righe. In questi casi, è consigliabile iniziare con le voci più recenti e leggere in sequenza le voci precedenti finché la condizione non viene soddisfatta, il che avviene in modo più efficiente per le chiavi timestamp che Spanner memorizza in ordine decrescente.
Aggiungi la parola chiave DESC
per impostare la chiave timestamp in ordine decrescente. Ad esempio:
GoogleSQL
CREATE TABLE UserAccessLog ( UserId INT64 NOT NULL, LastAccess TIMESTAMP NOT NULL, ... ) PRIMARY KEY (UserId, LastAccess DESC);
Best practice n. 2 per la progettazione dello schema: ordine decrescente o crescente dipende dalle query degli utenti, ad esempio "superiore" è il più recente o "superiore" è il meno recente.
Utilizza un indice interlacciato su una colonna il cui valore aumenta o diminuisce monotonicamente
Analogamente al precedente esempio di chiave primaria che dovresti evitare, è presente anche una cattiva idea di creare indici senza interleaving nelle colonne i cui valori sono aumentando o diminuendo in modo monotonico, anche se non sono colonne di chiave primaria.
Ad esempio, supponiamo di definire la seguente tabella, in cui LastAccess
è una colonna di chiave non principale:
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 nella colonna LastAccess
per
interrogando rapidamente il database per individuare gli accessi dell'utente "dal momento della X", in questo modo:
GoogleSQL
CREATE NULL_FILTERED INDEX UsersByLastAccess ON Users(LastAccess);
PostgreSQL
CREATE INDEX usersbylastaccess ON users(lastaccess) WHERE lastaccess IS NOT NULL;
Questo, però, comporta la stessa insidia descritta nella sezione perché Spanner implementa gli indici come tabelle in background, e la tabella di indice risultante utilizza una colonna il cui valore aumenta monotonicamente come prima parte fondamentale.
Va bene però creare un indice con interleaving come questo, perché le righe di gli indici con interleaving sono interleali nelle righe padre corrispondenti è improbabile che una singola riga padre produca migliaia di eventi al secondo.
Best practice n. 3 per la progettazione dello schema: non creare un indice senza interleaving su una colonna con frequenza di scrittura elevata il cui valore in modo monotonico aumenta o diminuisce. Anziché utilizzare indici interlacciati, utilizza tecniche come quelle che useresti per la progettazione della chiave primaria della tabella di base quando progetti le colonne dell'indice, ad esempio aggiungi "shardId".
Passaggi successivi
- Consulta gli esempi di design dello schema.
- Scopri di più sul caricamento collettivo dei dati.