Best practice di progettazione dello schema

L'architettura distribuita di Spanner ti consente di progettare lo schema per evitare Gli hotspot, le situazioni in cui vengono inviate troppe richieste allo stesso server satura le risorse del server e potrebbe causare latenze elevate.

In questa pagina vengono descritte le best practice per la progettazione degli schemi da evitare la creazione di hotspot. Un modo per evitare gli hotspot è modificare la progettazione dello schema in modo da per consentire a Spanner di suddividere e distribuire i dati tra più server. La distribuzione dei dati tra i server aiuta il tuo database Spanner a funzionare in modo efficiente, in particolare quando esegui gli inserimenti di dati collettivi.

Scegli una chiave primaria per impedire gli 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 delle cause degli hotspot è la presenza di una colonna il cui valore in modo monotonico modifiche come prima parte chiave, perché ciò determina tutti gli inserimenti che avvengono alla fine dello spazio delle chiavi. Questo pattern è indesiderato perché Spanner utilizza intervalli di chiavi per dividere i dati tra i server, il che significa che tutti gli inserimenti diretto a un singolo server che si occupa di tutto il lavoro.

Ad esempio, supponiamo che tu voglia mantenere una colonna di timestamp dell'ultimo accesso sulle righe della tabella UserAccessLog. La definizione di tabella seguente utilizza un e 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 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)
);

Il problema qui è che le righe vengono scritte in questa tabella nell'ordine delle ultime timestamp di accesso e, poiché i timestamp dell'ultimo accesso sono sempre in aumento, sono sempre scritte in fondo alla tabella. L'hotspot viene creato perché un singolo server Spanner riceve tutte le scritture, sovraccaricando un server.

Il seguente diagramma illustra questa insidia:

Tabella UserAccessLog ordinata per timestamp con l'hotspot corrispondente

La tabella UserAccessLog riportata sopra 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 dei per ogni riga). Poiché gli inserimenti sono ordinati per timestamp e il valore del timestamp è sempre in aumento, Spanner aggiunge sempre gli inserimenti alla fine della tabella e le indirizza alla stessa suddivisione. (Come discusso in Schema e modello dei dati, un split è 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 server diversi in di suddivisioni, quindi il server assegnato a questa particolare suddivisione 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 in alto. Tieni presente che in questa illustrazione ogni server gestisce al massimo una suddivisione, ma in realtà Spanner può assegnare a ogni server più di una suddivisione.

Quando Spanner aggiunge altre righe alla tabella, la suddivisione aumenta e quando raggiunge circa 8 GB, Spanner crea un'altra come descritto in Suddivisione basata sul carico. Spanner aggiunge nuove righe successive a questa nuova suddivisione e il server assegnato al segmento diventa il nuovo potenziale hotspot.

Quando si verificano hotspot, potresti notare che gli inserti sono lenti e che sullo stesso server potrebbe subire un rallentamento. 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 RFC 4122 come chiave primaria. Consigliamo di utilizzare l'UUID versione 4, perché utilizza valori casuali nella sequenza di bit. Sconsigliamo di usare gli UUID della versione 1 perché memorizzano il timestamp nei bit di ordine più alto.

Esistono diversi modi per archiviare l'UUID come chiave primaria:

  • In una colonna STRING(36).
  • In una coppia di INT64 colonne.
  • In una colonna BYTES(16).

Per una colonna STRING(36), puoi utilizzare lo Spanner GENERATE_UUID() (GoogleSQL o PostgreSQL) come valore predefinito della colonna da utilizzare consente a Spanner di generare automaticamente valori UUID.

L'utilizzo di un UUID presenta alcuni svantaggi:

  • Sono leggermente grandi e utilizzano almeno 16 byte. Altre opzioni per il principale non utilizzano così tanto spazio di archiviazione.
  • Non contengono informazioni sul record. Ad esempio, una chiave primaria SingerId e AlbumId hanno 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 diminuiscono in sequenza. Primaria sequenziale possono causare hotspot su larga scala. Un modo per evitare questo problema consiste nell'invertire a bit i valori sequenziali, assicurandosi di distribuire le coppie chiave-valore primarie in modo uniforme nello spazio delle chiavi.

Spanner supporta la sequenza invertita di bit, che genera valori univoci valori interi invertiti a bit. Puoi utilizzare una sequenza nella prima (o solo) in una chiave primaria per evitare problemi di hotspot. Per ulteriori informazioni, consulta la sezione Sequenza invertita in bit.

Inverti 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 (
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 questo schema modificato, gli inserimenti ora vengono ordinati per UserId, anziché in base al timestamp dell'ultimo accesso. Questo schema distribuisce le scritture tra suddivisioni diverse perché è improbabile che un singolo utente produca migliaia di eventi al secondo.

Di seguito sono indicate le cinque righe della tabella UserAccessLog Spanner ordina con UserId invece del timestamp di accesso:

Tabella UserAccessLog ordinata per UserId con velocità effettiva di scrittura bilanciata

In questo caso Spanner suddivide i dati di UserAccessLog in tre suddivisioni, ciascuna suddivisione contenente circa un migliaio di righe di valori UserId ordinati. Si tratta di una stima ragionevole di come i dati utente potrebbero essere suddivisi, supponendo che ogni riga contiene circa 1 MB di dati utente e viene assegnata una dimensione di suddivisione massima pari a di circa 8 GB. Anche se gli eventi utente si sono verificati circa di millisecondi, ogni evento è stato generato da un utente diverso, quindi l'ordine degli inserti ha molto meno probabilità di creare un hotspot rispetto all'utilizzo timestamp per l'ordine.

Consulta anche la best practice correlata per l'ordinazione in base al timestamp chiave.

Esegui l'hashing della chiave univoca e distribuisci le scritture tra shard logici

Un'altra tecnica comune per distribuire il carico su più server è la crea una colonna che contiene l'hash della chiave univoca effettiva, utilizza colonna hash (o la colonna hash insieme alle colonne delle chiavi univoche) come e la 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 shard logici, o partizioni, nella tua per configurare un database. In un database con sharding fisico, le righe sono distribuite su più server di database. In un database con sharding logico, i dati della tabella definiscono la o 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 il valore ShardId, esegui l'hashing di una combinazione delle colonne di chiave primaria e poi calcolare il modulo N dell'hash. Ad esempio:

ShardId = hash(LastAccess and UserId) % N

L'hash che hai scelto la funzione e la combinazione di colonne determina la distribuzione delle righe spazio dei tasti. Spanner creerà quindi suddivisioni tra le righe per ottimizzare le prestazioni.

Il diagramma seguente illustra come utilizzare un hash per creare tre shard logici può distribuire la velocità effettiva di scrittura in modo più uniforme tra i server:

Tabella UserAccessLog ordinata per ShardId con velocità effettiva di scrittura bilanciata

In questo caso la tabella UserAccessLog è ordinata in base al valore ShardId, 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 consente anche di creare una funzione hash in una colonna generata.

Per farlo in Google SQL, utilizza FARM_FINGERPRINT durante la scrittura, come illustrato 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 hai bisogno di un hash di crittografia, anche se un hash di crittografia potrebbe essere una buona scelta. Quando scegli una funzione hash, 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 sono presenti meno valori hash da scansionare.
  • Conteggio dei nodi.

Utilizza l'ordine decrescente per le chiavi basate su timestamp

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 nei seguenti casi: applica:

  • Per leggere la cronologia più recente, è in uso un con interleaving per la cronologia, per leggere la riga padre. In questo caso, con un timestamp DESC le ultime voci della cronologia sono memorizzate accanto alla riga principale. In caso contrario, per leggere la riga principale e la sua cronologia recente sarà necessaria 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, potresti 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 queste casi, vuoi iniziare con le voci più recenti e leggere in sequenza le voci meno recenti fino a quando la condizione non è stata soddisfatta, cosa che viene fatto da Spanner in modo più efficiente per le chiavi timestamp che Spanner archivia 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.

Utilizzare un indice con interleaving 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 a chiave primaria.

Ad esempio, supponiamo che tu definisca la seguente tabella, in cui LastAccess è un colonna non a 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 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;

Tuttavia, ciò comporta la stessa insidia descritta nella precedente definizione 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 gli indici con interleaving, utilizza tecniche come quelle che useresti per la progettazione della chiave primaria della tabella di base durante la progettazione delle colonne di indice, esempio, aggiungi "shardId".

Passaggi successivi