Puoi utilizzare le best practice elencate qui come riferimento rapido per ciò che devi tenere presente quando crei un'applicazione che utilizza Datastore. Se hai appena iniziato a utilizzare Datastore, questa pagina potrebbe non essere il punto di partenza migliore, perché non spiega le nozioni di base su come utilizzare Datastore. Se sei un nuovo utente, ti consigliamo di iniziare con la sezione Introduzione a Datastore.
Generale
- Utilizza sempre caratteri UTF-8 per i nomi di spazi dei nomi, tipi, proprietà e chiavi personalizzate. I caratteri non UTF-8 utilizzati in questi nomi possono interferire con la funzionalità di Datastore. Ad esempio, un carattere non UTF-8 in un nome di proprietà può impedire la creazione di un indice che utilizza la proprietà.
- Non utilizzare una barra (
/
) nei nomi di tipo o nei nomi delle chiavi personalizzate. I trattini diagonali in questi nomi potrebbero interferire con la funzionalità futura. - Evita di archiviare informazioni sensibili in un ID progetto Cloud. Un ID progetto Cloud potrebbe essere conservato oltre la durata del progetto.
- Come best practice per la conformità dei dati, ti consigliamo di non archiviare informazioni sensibili nei nomi delle entità o delle proprietà dell'entità di Datastore.
Chiamate API
- Utilizza le operazioni collettive per le letture, le scritture ed eliminazioni anziché le singole operazioni. Le operazioni collettive sono più efficienti perché eseguono più operazioni con lo stesso overhead di una singola operazione.
- Se una transazione non va a buon fine, assicurati di provare a eseguire il rollback della transazione. Il rollback riduce al minimo la latenza del nuovo tentativo per una richiesta diversa in competizione per le stesse risorse in una transazione. Tieni presente che un rollback potrebbe non riuscire, pertanto il rollback deve essere solo un tentativo secondo il criterio del massimo impegno.
- Utilizza chiamate asincrone, se disponibili, anziché chiamate sincrone.
Le chiamate asincrone riducono al minimo l'impatto della latenza. Ad esempio, prendiamo in considerazione un'applicazione che ha bisogno del risultato di un
lookup()
sincrono e dei risultati di una query prima di poter restituire una risposta. Selookup()
e la query non hanno una dipendenza di dati, non è necessario attendere in modo sincrono il completamento dilookup()
prima di avviare la query.
Entità
- Raggruppa i dati altamente correlati in gruppi di entità. I gruppi di entità consentono di eseguire query sugli antenati, che restituiscono risultati fortemente coerenti. Le query ancestor esaminano rapidamente anche un gruppo di entità con I/O minime perché le entità in un gruppo di entità sono archiviate in luoghi fisicamente vicini sui server Datastore.
- Evita di scrivere in un gruppo di entità più di una volta al secondo. La scrittura a una frequenza costante superiore a questo limite rende le letture eventualmente coerenti più eventuali, porta a timeout per le letture fortemente coerenti e comporta un rallentamento complessivo delle prestazioni dell'applicazione. Una scrittura batch o transazionale in un gruppo di entità viene conteggiata come una sola scrittura ai fini di questo limite.
- Non includere la stessa entità (per chiave) più volte nello stesso commit. L'inclusione della stessa entità più volte nello stesso commit potrebbe influire sulla latenza di Datastore.
Chiavi
- I nomi delle chiavi vengono generati automaticamente se non vengono forniti al momento della creazione dell'entità. Vengono allocate in modo da essere distribuite uniformemente nello spazio chiavi.
- Per una chiave che utilizza un nome personalizzato, utilizza sempre caratteri UTF-8, ad eccezione della barra (
/
). I caratteri non UTF-8 interferiscono con vari processi, come l'importazione di un backup di Datastore in Google BigQuery. Una barra verticale potrebbe interferire con le funzionalità future. - Per una chiave che utilizza un ID numerico:
- Non utilizzare un numero negativo per l'ID. Un ID negativo potrebbe interferire con l'ordinamento.
- Non utilizzare il valore
0
(zero) per l'ID. In questo caso, riceverai un ID assegnato automaticamente. - Se vuoi assegnare manualmente i tuoi ID numerici alle entità che crei, fai in modo che la tua applicazione ottenga un blocco di ID con il metodo
allocateIds()
. In questo modo, Datastore non assegnerà uno dei tuoi ID numerici manuali a un'altra entità.
Se assegni un ID numerico manuale o un nome personalizzato alle entità che crei, non utilizzare valori in aumento monotono come:
1, 2, 3, …, "Customer1", "Customer2", "Customer3", …. "Product 1", "Product 2", "Product 3", ….
Se un'applicazione genera un traffico elevato, questa numerazione sequenziale potrebbe portare a hotspot che influiscono sulla latenza di Datastore. Per evitare il problema degli ID numerici sequenziali, ottieni gli ID numerici dal metodo
allocateIds()
. Il metodoallocateIds()
genera sequenze ben distribuite di ID numerici.Se specifichi una chiave o memorizzi il nome generato, in un secondo momento puoi eseguire un
lookup()
coerente su quell'entità senza dover eseguire una query per trovarla.
Indici
- Se una proprietà non sarà mai necessaria per una query, escludi la proprietà dagli indici. L'indicizzazione non necessaria di una proprietà potrebbe comportare un aumento della latenza per ottenere la coerenza e un aumento dei costi di archiviazione delle voci dell'indice.
- Evita di avere troppi indici composti. L'uso eccessivo di indici compositi potrebbe comportare un aumento della latenza per ottenere la coerenza e un aumento dei costi di archiviazione delle voci dell'indice. Se devi eseguire query ad hoc su grandi set di dati senza indici definiti in precedenza, utilizza Google BigQuery.
- Non indicizzare le proprietà con valori in aumento monotonico (ad esempio un timestamp
NOW()
). La gestione di un indice di questo tipo potrebbe portare a hotspot che influiscono sulla latenza di Datastore per le applicazioni con alte velocità di lettura e scrittura. Per ulteriori indicazioni su come gestire le proprietà monotone, consulta Tariffe di lettura/scrittura elevate per un intervallo di chiavi ristretto di seguito.
Proprietà
- Utilizza sempre caratteri UTF-8 per le proprietà di tipo stringa. Un carattere non UTF-8 in una proprietà di tipo stringa potrebbe interferire con le query. Se devi salvare i dati con caratteri non UTF-8, utilizza una stringa di byte.
- Non utilizzare punti nei nomi delle proprietà. I punti nei nomi delle proprietà interferiscono con l'indicizzazione delle proprietà delle entità incorporate.
Query
- Se devi accedere solo alla chiave dai risultati della query, utilizza una query solo per chiavi. Una query basata solo su chiavi restituisce risultati con latenza e costi inferiori rispetto al recupero di intere entità.
- Se devi accedere solo a proprietà specifiche di un'entità, utilizza una query di proiezione. Una query di proiezione restituisce risultati con latenza e costi inferiori rispetto al recupero di intere entità.
- Analogamente, se devi accedere solo alle proprietà incluse nel
filtro della query (ad esempio quelle elencate in una clausola
order by
), utilizza una query di proiezione. - Non utilizzare gli offset. Utilizza invece i cursor. L'utilizzo di un offset evita solo di restituire le entità saltate all'applicazione, ma queste entità vengono comunque recuperate internamente. Le entità ignorate influiscono sulla latenza della query e alla tua applicazione vengono addebitate le operazioni di lettura necessarie per recuperarle.
- Se hai bisogno di elevata coerenza per le tue query, utilizza una query sull'antenato.
Per utilizzare le query sugli antenati, devi prima strutturare i dati per una coerenza elevata. Una query da predecessore restituisce risultati fortemente coerenti. Tieni presente che una query solo chiavi non principale followed by a
lookup()
non restituisce risultati affidabili, perché la query solo chiavi non principale potrebbe ottenere risultati da un indice non coerente al momento della query.
Progettazione per la scalabilità
Aggiornamenti a un singolo gruppo di entità
Un singolo gruppo di entità in Datastore non deve essere aggiornato troppo rapidamente.
Se utilizzi Datastore, Google consiglia di progettare l'applicazione in modo che non debba aggiornare un gruppo di entità più di una volta al secondo. Ricorda che un'entità senza elementi principali e secondari è un gruppo di entità a sé stante. Se aggiorni un gruppo di entità troppo rapidamente, le scritture in Datastore avranno una latenza più elevata, timeout e altri tipi di errori. Questo fenomeno è noto come competizione.
A volte le frequenze di scrittura del datastore in un singolo gruppo di entità possono superare il limite di una al secondo, pertanto i test di carico potrebbero non mostrare questo problema.
Frequenze di lettura/scrittura elevate per un intervallo di chiavi ristretto
Evita frequenze di lettura o scrittura elevate per le chiavi di Datastore che sono alfabeticamente vicine.
Datastore è basato sul database NoSQL di Google, Bigtable, ed è soggetto alle caratteristiche di prestazioni di Bigtable. Bigtable esegue la scalabilità suddividendo le righe in tablet distinti e queste righe sono ordinate lessicograficamente in base alla chiave.
Se utilizzi Datastore, puoi riscontrare scritture lente a causa di un tablet caldo se si verifica un aumento improvviso della frequenza di scrittura per un piccolo intervallo di chiavi che supera la capacità di un singolo server tablet. Alla fine, Bigtable suddividerà lo spazio delle chiavi per supportare un carico elevato.
Il limite per le letture è in genere molto più elevato rispetto a quello per le scritture, a meno che tu non stia leggendo da una singola chiave a una frequenza elevata. Bigtable non può suddividere una singola chiave in più di un tablet.
Le tabelle calde possono essere applicate agli intervalli di chiavi utilizzati sia dalle chiavi delle entità sia dagli indici.
In alcuni casi, un hotspot di Datastore può avere un impatto più ampio su un'applicazione rispetto all'impedire letture o scritture in un piccolo intervallo di chiavi. Ad esempio, i tasti di scelta rapida potrebbero essere letti o scritti durante l'avvio dell'istanza, causando il fallimento delle richieste di caricamento.
Per impostazione predefinita, Datastore alloca le chiavi utilizzando un algoritmo distribuito. Di conseguenza, in genere non riscontrerai hotspot nelle scritture di Datastore se crei nuove entità a una frequenza di scrittura elevata utilizzando il criterio di allocazione degli ID predefinito. Esistono alcuni casi limite in cui puoi riscontrare questo problema:
Se crei nuove entità a una frequenza molto elevata utilizzando il precedente criterio di allocazione degli ID sequenziali.
Se crei nuove entità a una frequenza molto elevata e assegni i tuoi ID in modo monotonicamente crescente.
Se crei nuove entità a un ritmo molto elevato per un tipo che in precedenza aveva pochissime entità esistenti. Bigtable inizierà con tutte le entità sullo stesso server tablet e ci vorrà un po' di tempo per suddividere l'intervallo di chiavi su server tablet separati.
Questo problema si verifica anche se crei nuove entità a una frequenza elevata con una proprietà indicizzata in modo monotonamente crescente, come un timestamp, perché queste proprietà sono le chiavi per le righe nelle tabelle di indicizzazione in Bigtable.
Datastore antepone lo spazio dei nomi e il tipo del gruppo di entità base alla chiave di riga Bigtable. Puoi raggiungere un hotspot se inizi a scrivere in un nuovo ambito o tipo senza aumentare gradualmente il traffico.
Se hai una chiave o una proprietà indicizzata che aumenterà in modo monotonico, puoi anteporre un hash casuale per assicurarti che le chiavi vengano suddivise in più tablet.
Analogamente, se devi eseguire query su una proprietà in aumento (o in diminuzione) monotonica utilizzando un'ordinamento o un filtro, puoi eseguire l'indicizzazione su una nuova proprietà, per la quale anteponi il valore monotonico con un valore che ha una cardinalità elevata nel set di dati, ma è comune a tutte le entità nell'ambito della query che vuoi eseguire. Ad esempio, se vuoi eseguire una query per le voci in base al timestamp, ma devi solo trovare i risultati per un singolo utente alla volta, puoi anteporre al timestamp l'ID dell'utente e indicizzare questa nuova proprietà. In questo modo, saranno comunque consentite le query e i risultati ordinati per l'utente, ma la presenza dell'ID utente garantirà che l'indice stesso sia ben suddiviso in parti.
Per una spiegazione più dettagliata di questo problema, consulta il post del blog di Ikai Lan sul salvataggio di valori in aumento monotonico in Datastore.
Aumento del traffico
Aumentare gradualmente il traffico verso nuovi tipi di Datastore o parti dello spazio chiavi.
Devi aumentare gradualmente il traffico verso i nuovi tipi di Datastore per dare a Bigtable il tempo sufficiente per suddividere i tablet man mano che il traffico cresce. Consigliamo un massimo di 500 operazioni al secondo per un nuovo tipo di datastore, quindi di aumentare il traffico del 50% ogni 5 minuti. In teoria, puoi arrivare a 740.000 operazioni al secondo dopo 90 minuti utilizzando questa pianificazione di aumento. Assicurati che le scritture siano distribuite in modo relativamente uniforme nell'intervallo di chiavi. I nostri SRE la chiamano "regola 500/50/5".
Questo modello di implementazione graduale è particolarmente importante se modifichi il codice per smettere di utilizzare il tipo A e utilizzare invece il tipo B. Un modo ingenuo per gestire questa migrazione è modificare il codice in modo che legga il tipo B e, se non esiste, legga il tipo A. Tuttavia, ciò potrebbe causare un aumento improvviso del traffico verso un nuovo tipo con una porzione molto piccola dello spazio chiavi. Bigtable potrebbe non essere in grado di suddividere in modo efficiente le tabelle se lo spazio delle chiavi è sparso.
Lo stesso problema può verificarsi anche se esegui la migrazione delle entità in modo da utilizzare un intervallo diverso di chiavi all'interno dello stesso tipo.
La strategia utilizzata per eseguire la migrazione delle entità a un nuovo tipo o chiave dipende dal modello dei dati. Di seguito è riportata una strategia di esempio, nota come "Letture parallele". Dovrai stabilire se questa strategia è efficace per i tuoi dati. Un aspetto importante sarà il costo delle operazioni parallele durante la migrazione.
Leggi prima dall'entità o dalla chiave precedente. Se non è presente, puoi leggere dalla nuova entità o chiave. Un tasso elevato di letture di entità non esistenti può portare a hotspot, quindi devi assicurarti di aumentare gradualmente il carico. Una strategia migliore è copiare l'entità vecchia in quella nuova ed eliminare la vecchia. Aumenta gradualmente le letture parallele per assicurarti che lo spazio delle nuove chiavi sia ben suddiviso.
Una possibile strategia per aumentare gradualmente le letture o le scritture di un nuovo tipo è utilizzare un hash deterministico dell'ID utente per ottenere una percentuale aleatoria di utenti che scrivono nuove entità. Assicurati che il risultato dell'hash dell'ID utente non sia distorto dalla funzione random o dal comportamento dell'utente.
Nel frattempo, esegui un job Dataflow per copiare tutti i dati dalle vecchie entità o chiavi alle nuove. Il job batch deve evitare le scritture in chiavi sequenziali per evitare gli hotspot di Bigtable. Al termine del job batch, puoi leggere solo dalla nuova posizione.
Un perfezionamento di questa strategia è eseguire la migrazione di piccoli gruppi di utenti contemporaneamente. Aggiungi un campo all'entità utente che monitora lo stato della migrazione dell'utente. Seleziona un batch di utenti di cui eseguire la migrazione in base a un hash dell'ID utente. Un job MapReduce o Dataflow eseguirà la migrazione delle chiavi per quel batch di utenti. Gli utenti con una migrazione in corso useranno le letture parallele.
Tieni presente che non puoi eseguire facilmente il rollback a meno che non esegui scritture doppie sia delle entità precedenti sia di quelle nuove durante la fase di migrazione. Ciò farebbe aumentare i costi di Datastore sostenuti.
Eliminazioni
Evita di eliminare un numero elevato di entità Datastore in un piccolo intervallo di chiavi.
Bigtable riscrive periodicamente le tabelle per rimuovere le voci eliminate e riorganizzare i dati in modo che le operazioni di lettura e scrittura siano più efficienti. Questo processo è noto come compattazione.
Se elimini un numero elevato di entità Datastore in un piccolo intervallo di chiavi, le query in questa parte dell'indice saranno più lente fino al completamento della compattazione. In casi estremi, le query potrebbero scadere prima di restituire risultati.
È un antipattern utilizzare un valore timestamp per un campo indicizzato per rappresentare la data e l'ora di scadenza di un'entità. Per recuperare le entità scadute, devi eseguire una query su questo campo indicizzato, che probabilmente si trova in una parte sovrapposta dello spazio chiavi con voci di indice per le entità eliminate più di recente.
Puoi migliorare le prestazioni con le "query suddivise in parti", che antepongono una stringa di lunghezza fissa al timestamp di scadenza. L'indice è ordinato sulla stringa completa, in modo che le entità con lo stesso timestamp vengano locate nell'intervallo di chiavi dell'indice. Esegui più query in parallelo per recuperare i risultati da ogni shard.
Una soluzione più completa per il problema del timestamp di scadenza è utilizzare un "numero di generazione", ovvero un contatore globale aggiornato periodicamente. Il numero di generazione viene anteposto al timestamp di scadenza in modo che le query vengano ordinate in base al numero di generazione, allo shard e al timestamp. L'eliminazione delle entità precedenti avviene in una generazione precedente. Il numero di generazione di qualsiasi entità non eliminata deve essere incrementato. Al termine dell'eliminazione, vai alla generazione successiva. Le query relative a una generazione precedente avranno un cattivo rendimento fino al completamento della compattazione. Potresti dover attendere il completamento di diverse generazioni prima di eseguire una query sull'indice per ottenere l'elenco delle entità da eliminare, in modo da ridurre il rischio di risultati mancanti a causa di coerenza finale.
Sharding e replica
Utilizza lo sharding o la replica per le chiavi Datastore attive.
Puoi utilizzare la replica se devi leggere una parte dell'intervallo di chiavi a una frequenza superiore a quella consentita da Bigtable. Con questa strategia, immagazzinerai N copie della stessa entità, consentendo un tasso di letture N volte superiore rispetto a quello supportato da una singola entità.
Puoi utilizzare lo sharding se devi scrivere in una parte dell'intervallo di chiavi a una frequenza superiore a quella consentita da Bigtable. Lo sharding suddivide un'entità in parti più piccole.
Alcuni errori comuni durante lo sharding includono:
Sharding utilizzando un prefisso di tempo. Quando l'ora passa al prefisso successivo, la nuova parte non suddivisa diventa un hotspot. Dovresti invece eseguire gradualmente il roll-over di una parte delle scritture nel nuovo prefisso.
Esegui lo sharding solo delle entità più richieste. Se esegui lo shard di una piccola proporzione del numero totale di entità, le righe tra le entità calde potrebbero non essere sufficienti per garantire che rimangano in suddivisioni diverse.
Passaggi successivi
- Scopri di più sui limiti di Datastore.
- Scopri le best practice per le organizzazioni aziendali della Google Cloud Platform.