Best practice

Puoi utilizzare le best practice elencate di seguito come un riferimento rapido ai concetti da tenere presenti durante la creazione di un'applicazione che utilizza Firestore in modalità Datastore. Se hai appena iniziato con la modalità Datastore, questa pagina potrebbe non essere il punto di partenza migliore, perché non ti insegna le nozioni di base su come utilizzare la modalità Datastore. Se sei un nuovo utente, ti suggeriamo di iniziare dalla pagina Introduzione a Firestore in modalità Datastore.

Generale

  • Utilizza sempre i caratteri UTF-8 per i nomi degli spazi dei nomi, i nomi dei tipi, i nomi delle proprietà e i nomi delle chiavi personalizzate. I caratteri non UTF-8 utilizzati in questi nomi possono interferire con la funzionalità della modalità Datastore. Ad esempio, un carattere non UTF-8 nel nome di una proprietà può impedire la creazione di un indice che utilizza la proprietà.
  • Non utilizzare una barra (/) nei nomi dei tipi o nei nomi delle chiavi personalizzate. Le barre in questi nomi potrebbero interferire con le funzionalità future.
  • Evita di archiviare informazioni sensibili in un ID progetto Cloud. Un ID progetto cloud potrebbe essere conservato per tutta la durata del progetto.

Chiamate API

  • Utilizza le operazioni in batch per operazioni di lettura, scrittura ed eliminazione, anziché singole operazioni. Le operazioni in batch sono più efficienti perché eseguono più operazioni con lo stesso sovraccarico di una singola operazione.
  • Se una transazione non va a buon fine, prova a eseguire il rollback della transazione. Il rollback riduce al minimo la latenza dei nuovi tentativi per una richiesta diversa che compete per le stesse risorse in una transazione. Tieni presente che il rollback potrebbe non riuscire, pertanto il rollback deve essere un tentativo soltanto.
  • Utilizzare le chiamate asincrone, se disponibili, anziché le chiamate sincrone. Le chiamate asincrone riducono al minimo l'impatto della latenza. Ad esempio, prendi in considerazione un'applicazione che ha bisogno del risultato di un elemento lookup() sincrono e dei risultati di una query prima che possa mostrare una risposta. Se lookup() e la query non hanno una dipendenza dei dati, non è necessario attendere in modo sincrono il completamento di lookup() prima di avviare la query.

Entità

  • Non includere più volte la stessa entità (per chiave) nello stesso commit. L'inclusione della stessa entità più volte nello stesso commit potrebbe incidere sulla latenza.

  • Consulta la sezione sugli aggiornamenti a un'entità.

Chiavi

  • I nomi delle chiavi vengono generati automaticamente se non vengono forniti al momento della creazione dell'entità. Vengono allocati in modo da essere distribuiti uniformemente nello spazio dei tasti.
  • Per una chiave che utilizza un nome personalizzato, utilizza sempre caratteri UTF-8 ad eccezione della barra (/). I caratteri non UTF-8 interferiscono con i diversi processi, come l'importazione di un file di esportazione in modalità Datastore in BigQuery. Una barra 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 la modalità Datastore non potrà assegnare uno dei tuoi ID numerici manuali a un'altra entità.
  • Se assegni il tuo ID numerico manuale o il tuo nome personalizzato alle entità che crei, non utilizzare valori che aumentano in modo monotonico, ad esempio:

    1, 2, 3, …,
    "Customer1", "Customer2", "Customer3", ….
    "Product 1", "Product 2", "Product 3", ….
    

    Se un'applicazione genera traffico di grandi dimensioni, tale numerazione sequenziale potrebbe portare a hotspot che influiscono sulla latenza della modalità Datastore. Per evitare il problema degli ID numerici sequenziali, ottieni gli ID numerici dal metodo allocateIds(). Il metodo allocateIds() genera sequenze ben distribuite di ID numerici.

  • Specificando una chiave o memorizzando il nome generato, in un secondo momento puoi eseguire un comando lookup() su quell'entità senza dover inviare una query per trovare l'entità.

Indici

Proprietà

  • Utilizza sempre i caratteri UTF-8 per le proprietà di tipo string. Un carattere non UTF-8 in una proprietà di stringa di tipo 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à dell'entità incorporata.

Query

  • Se devi accedere solo alla chiave dai risultati della query, utilizza una query solo per le chiavi. Una query basata solo su chiavi restituisce risultati con una latenza e un costo inferiori rispetto al recupero di intere entità.
  • Se devi accedere solo a proprietà specifiche da un'entità, utilizza una query di proiezione. Una query di proiezione restituisce i risultati a una latenza e a un costo inferiori rispetto al recupero di intere entità.
  • Allo stesso modo, se devi accedere solo alle proprietà incluse nel filtro di query (ad esempio quelle elencate in una clausola order by), utilizza una query di proiezione.
  • Non utilizzare le compensazioni. Utilizza invece i cursori. L'utilizzo di un offset evita solo di restituire le entità ignorate nell'applicazione, ma queste ultime vengono comunque recuperate internamente. Le entità ignorate influiscono sulla latenza della query e all'applicazione vengono addebitate le operazioni di lettura necessarie per recuperarle.

Progettazione per la scalabilità

Le best practice riportate di seguito descrivono come evitare situazioni che creano problemi di contesa.

Aggiornamenti di un'entità

Quando progetti la tua app, valuta la velocità con cui aggiorna le singole entità. Il modo migliore per caratterizzare le prestazioni del carico di lavoro è eseguire il test del carico. La frequenza massima esatta con cui un'app può aggiornare una singola entità dipende molto dal carico di lavoro. I fattori comprendono la frequenza di scrittura, la contesa tra le richieste e il numero di indici interessati.

Un'operazione di scrittura delle entità aggiorna l'entità e gli eventuali indici associati, mentre Firestore in modalità Datastore applica l'operazione di scrittura in modo sincrono a un quorum delle repliche. A velocità di scrittura sufficientemente elevate, il database inizierà a riscontrare problemi di contesa, latenza più alta o altri errori.

Velocità di lettura/scrittura elevate in un intervallo di chiavi limitato

Per evitare la chiusura dei documenti in modo letterale, evitate percentuali elevate di lettura o scrittura; in caso contrario, la vostra applicazione presenterà errori. Questo problema è noto come hotspot e l'applicazione può riscontrare hotspot se esegue una delle seguenti operazioni:

  • Crea nuove entità a una velocità molto elevata e alloca ID ID che aumentano in modo esplicito.

    La modalità Datastore alloca chiavi utilizzando un algoritmo a dispersione. Non dovresti incontrare hotspot sulle scritture se crei nuove entità utilizzando l'allocazione automatica degli ID entità.

  • Crea nuove entità a una velocità molto elevata utilizzando il criterio di allocazione degli ID sequenziale legacy.

  • Crea nuove entità a una tariffa elevata per un tipo con poche entità.

  • Crea nuove entità con un valore della proprietà indicizzato e che aumenta in modo monotonico, ad esempio un timestamp, a un ritmo molto elevato.

  • Elimina le entità da un tipo a una frequenza elevata.

  • Scrive nel database con una frequenza molto elevata senza aumentare gradualmente il traffico.

Se hai un aumento improvviso della velocità di scrittura fino a un piccolo intervallo di chiavi, puoi ricevere operazioni di scrittura lente a causa di un hotspot. Alla fine, la modalità Datastore dividerà lo spazio delle chiavi per supportare il carico elevato.

Il limite per le letture è in genere molto superiore rispetto a quello per le scritture, a meno che tu non legga da una singola chiave a una frequenza elevata.

Gli hot spot possono essere applicati a intervalli di chiavi utilizzati sia dalle chiavi delle entità che dagli indici.

In alcuni casi, un hotspot può avere un impatto più ampio su un'applicazione rispetto alla prevenzione di letture o scritture su un piccolo intervallo di chiavi. Ad esempio, i tasti di scelta rapida potrebbero essere letti o scritti durante l'avvio dell'istanza, causando errori nelle richieste di caricamento.

Se hai una chiave o una proprietà indicizzata che aumenterà monotonicamente, puoi anteporre un hash casuale per assicurarti che le chiavi vengano sottoposte a sharding su più tablet.

Allo stesso modo, se hai bisogno di eseguire una query su una proprietà che aumenta (o diminuisce) in modo monotonico utilizzando un ordinamento o un filtro, potresti indicizzarla in una nuova proprietà, per la quale il valore monotonico è preceduto da un valore con una cardinalità elevata nell'intero set di dati, ma comune a tutte le entità nell'ambito della query che vuoi eseguire. Ad esempio, se vuoi eseguire una query sulle voci in base al timestamp, ma devi restituire i risultati per un singolo utente alla volta, puoi aggiungere il prefisso all'ID utente e indicizzare la nuova proprietà. In questo modo le query e i risultati ordinati per quell'utente sarebbero comunque consentiti, ma la presenza dello User-ID assicurerebbe che l'indice stesso sia dotato di sharding ben.

Aumento del traffico

Aumenta gradualmente il traffico verso nuovi tipi o parti dello spazio dei tasti.

Dovresti aumentare gradualmente il traffico in nuovi tipi per fornire a Firestore in modalità Datastore tempo sufficiente per prepararti all'aumento del traffico. Consigliamo un massimo di 500 operazioni al secondo a un nuovo tipo, quindi l'aumento del traffico del 50% ogni 5 minuti. In teoria, puoi aumentare fino a 740.000 operazioni al secondo dopo 90 minuti utilizzando questa pianificazione graduale. Assicurati che le scritture siano distribuite in modo relativamente uniforme nell'intervallo di chiavi. I nostri SRE definiscono questa regola 500/50/5".

Questo pattern di incremento 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 è cambiare il codice in modo che sia di tipo B e, se non esiste, di leggere il tipo A. Tuttavia, questo potrebbe causare un improvviso aumento del traffico verso un nuovo tipo con una porzione molto ridotta dello spazio dei tasti.

Lo stesso problema può verificarsi anche se esegui la migrazione delle entità per utilizzare un intervallo diverso di chiavi all'interno dello stesso tipo.

La strategia da utilizzare per eseguire la migrazione delle entità a un nuovo tipo o a una nuova chiave dipenderà dal modello dei dati. Di seguito è riportata una strategia di esempio nota come "Letture parallele". Dovrai determinare se questa strategia è efficace per i tuoi dati. Una considerazione importante sarà l'impatto sui costi delle operazioni parallele durante la migrazione.

Leggi prima dall'entità o dalla chiave precedente. Se manca, puoi leggere dalla nuova entità o chiave. Un'elevata frequenza di letture di entità inesistenti può portare all'hotspotting, quindi devi assicurarti di aumentare gradualmente il carico. Una strategia migliore è copiare la vecchia entità nella nuova e poi eliminare la vecchia. Incrementa gradualmente le letture parallele per assicurarti che il nuovo spazio delle chiavi sia ben suddiviso.

Una possibile strategia per aumentare gradualmente le letture o scritture a un nuovo tipo è utilizzare un hash deterministico dello User-ID per ottenere una percentuale casuale di utenti che scrivono nuove entità. Assicurati che il risultato dell'hash dell'ID utente non sia alterato dalla funzione casuale o dal comportamento dell'utente.

Nel frattempo, esegui un job Dataflow per copiare tutti i dati dalle entità o chiavi precedenti in quelle nuove. Il job batch dovrebbe evitare scritture su chiavi sequenziali per evitare hotspot. Una volta completato il job batch, potrai leggere solo dalla nuova posizione.

Un perfezionamento di questa strategia è la migrazione di piccoli gruppi di utenti alla volta. Aggiungi un campo all'entità utente che monitori lo stato della migrazione di tale utente. Seleziona un gruppo di utenti di cui eseguire la migrazione in base a un hash dello User-ID. Un job Mapreduce o Dataflow eseguirà la migrazione delle chiavi per quel gruppo di utenti. Gli utenti che hanno una migrazione in corso utilizzeranno le letture parallele.

Tieni presente che non puoi eseguire il rollback facilmente, a meno che tu non esegua la doppia scrittura di entità vecchie e nuove durante la fase di migrazione. Questo aumenterebbe i costi della modalità Datastore sostenuti.

Eliminazioni

Evita di eliminare un numero elevato di entità in un piccolo intervallo di chiavi.

Firestore in modalità Datastore 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à della modalità 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 i risultati.

È anti-pattern per utilizzare un valore di timestamp per un campo indicizzato per rappresentare la data di scadenza di un'entità. Per recuperare le entità scadute, dovresti eseguire query su questo campo indicizzato, che probabilmente si trova in una parte sovrapposta dello spazio delle chiavi con voci di indice per le entità eliminate più di recente.

Puoi migliorare il rendimento con "query vincolate", che anteponino una stringa con lunghezza fissa al timestamp di scadenza. L'indice viene ordinato nella stringa completa, in modo che le entità con lo stesso timestamp si trovino 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 relativo al timestamp di scadenza è utilizzare un "numero di generazione", ovvero un contatore globale che viene aggiornato periodicamente. Il numero di generazione viene anteposto al timestamp di scadenza in modo che le query siano ordinate per numero di generazione, quindi shard e quindi timestamp. L'eliminazione delle vecchie entità avviene in una generazione precedente. Il numero di generazione deve essere incrementato per qualsiasi entità non eliminata. Una volta completata l'eliminazione, si passa alla generazione successiva. Le query su una generazione precedente verranno eseguite male fino al completamento della compattazione. Potrebbe essere necessario 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 perdere i risultati a causa della coerenza finale.

sharding e replica

Utilizza lo sharding o la replica per gestire gli hotspot.

Puoi utilizzare la replica se devi leggere una parte dell'intervallo di chiavi a una frequenza maggiore rispetto a quella consentita da Firestore in modalità Datastore. Utilizzando questa strategia, conserveresti 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 porzione dell'intervallo di chiavi a una velocità maggiore rispetto a quella consentita da Firestore in modalità Datastore. Lo sharding suddivide un'entità in parti più piccole.

Alcuni errori comuni dello sharding includono:

  • sharding con un prefisso di tempo. Quando l'ora passa al prefisso successivo, la nuova porzione non suddivisa diventa un hotspot. Dovresti passare gradualmente una parte delle scritture al nuovo prefisso.

  • Esegui il partizionamento orizzontale delle sole entità più calde. Se esegui il partizionamento orizzontale di una piccola parte del numero totale di entità, potrebbero non esserci righe sufficienti tra le entità calde per garantire che rimangano su segmenti diversi.

Passaggi successivi