Best practice

Puoi utilizzare le best practice elencate qui come riferimento rapido da tenere presenti quando crei un'applicazione che utilizza Firestore in modalità Datastore. Se hai iniziato da poco a utilizzare la modalità Datastore, questa pagina potrebbe non essere il modo migliore per iniziare, perché non ti insegna le nozioni di base su come utilizzare la modalità Datastore. Se sei un nuovo utente, ti suggeriamo di iniziare con Introduzione a Firestore in modalità Datastore.

Generale

  • Utilizza sempre i caratteri UTF-8 per i nomi degli spazi dei nomi, dei nomi dei tipi, dei nomi delle proprietà e dei 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 la barra (/) nei nomi dei tipi o delle chiavi personalizzate. Le barre in avanti 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 operazioni batch per letture, scritture ed eliminazioni anziché singole operazioni. Le operazioni batch 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 dei tentativi per una richiesta diversa in concorrenza con le stesse risorse in una transazione. Tieni presente che il rollback stesso potrebbe non riuscire, quindi il rollback dovrebbe essere soltanto un tentativo basato sul criterio del "best effort".
  • Se disponibili, utilizza le chiamate asincrone anziché quelle sincrone. Le chiamate asincrone riducono al minimo l'impatto della latenza. Ad esempio, considera un'applicazione che ha bisogno del risultato di un lookup() sincrono e dei risultati di una query prima di poter visualizzare una risposta. Se lookup() e la query non hanno una dipendenza dai dati, non è necessario attendere in modo sincrono il completamento di lookup() prima di iniziare la query.

Entità

  • Non includere più volte la stessa entità (per chiave) nello stesso commit. Includere più volte la stessa entità nello stesso commit potrebbe influire sulla latenza.

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

Chiavi

  • I nomi delle chiavi vengono generati automaticamente se non vengono forniti al momento della creazione dell'entità. Sono allocati in modo da essere distribuiti uniformemente nello spazio delle chiavi.
  • Per una chiave che utilizza un nome personalizzato, utilizza sempre i caratteri UTF-8 tranne la barra (/). I caratteri non UTF-8 interferiscono con vari processi, ad esempio l'importazione di un file di esportazione in modalità Datastore in BigQuery. Una barra potrebbe interferire con le funzionalità future.
  • Se una chiave 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 allocato automaticamente.
    • Se vuoi assegnare manualmente i tuoi ID numerici alle entità create, chiedi alla tua applicazione di ottenere un blocco di ID con il metodo allocateIds(). Ciò impedirà alla modalità Datastore di assegnare 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 crescenti monotonicamente come:

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

    Se un'applicazione genera traffico di grandi dimensioni, la numerazione sequenziale potrebbe portare a hotspot che influiscono sulla latenza della modalità Datastore. Per evitare il problema di 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'istanza lookup() su quell'entità senza dover eseguire una query per trovare l'entità.

Indici

Proprietà

  • Utilizza sempre i 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 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 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 di un'entità, utilizza una query di proiezione. Una query di proiezione restituisce risultati con una latenza e 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 offset. Utilizza invece i cursors. L'utilizzo di un offset evita solo di restituire le entità ignorate all'applicazione, ma queste entità vengono comunque recuperate internamente. Le entità ignorate influiscono sulla latenza della query e all'applicazione vengono addebitate le operazioni di lettura richieste per recuperarle.

Progettare per la scalabilità

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

Aggiornamenti a un'entità

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

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

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

Evita elevate frequenze di lettura o scrittura per documenti chiusi lessicograficamente, altrimenti la tua applicazione riscontrerà errori di contesa. Questo problema è noto come hotspot e la tua applicazione può riscontrare hotspot se si verifica una delle seguenti condizioni:

  • Crea nuove entità con una frequenza molto elevata e alloca i propri ID con aumento monotonico.

    La modalità Datastore alloca le chiavi utilizzando un algoritmo a dispersione. Non dovresti riscontrare hotspot durante le scritture se crei nuove entità utilizzando l'allocazione automatica dell'ID entità.

  • Crea nuove entità molto rapidamente utilizzando il precedente criterio di allocazione degli ID sequenziale.

  • Crea con frequenza elevata nuove entità per un tipo con poche entità.

  • Crea nuove entità con un valore della proprietà indicizzato e monotonico in più, come un timestamp, con una frequenza molto elevata.

  • Elimina frequentemente le entità da un tipo.

  • Scrive nel database a una velocità molto elevata senza aumentare gradualmente il traffico.

Se si verifica un aumento improvviso della frequenza di scrittura su un intervallo ridotto di chiavi, puoi ottenere scritture lente a causa di un hotspot. La modalità Datastore suddividerà lo spazio delle chiavi per supportare un carico elevato.

Il limite per le letture è in genere molto più elevato di quello per le scritture, a meno che tu non stia leggendo ad alta velocità da una singola chiave.

Gli hotspot possono essere applicati agli intervalli di chiavi utilizzati sia da chiavi di entità sia da indici.

In alcuni casi, un hotspot può avere un impatto più ampio su un'applicazione rispetto a impedire le letture o le scritture su un intervallo ridotto di chiavi. Ad esempio, le hotkey potrebbero essere lette o scritte durante l'avvio dell'istanza, causando un errore delle richieste di caricamento.

Se hai una chiave o una proprietà indicizzata con aumento monotonico, puoi anteporre un hash casuale per assicurarti che le chiavi vengano suddivise in più tablet.

Allo stesso modo, se devi eseguire query su una proprietà con aumento (o decrescente) monotonicamente utilizzando un ordinamento o un filtro, potresti invece indicizzare una nuova proprietà, per la quale aggiungi al valore monotonico un valore con cardinalità elevata nel set di dati, ma comune a tutte le entità nell'ambito della query che vuoi eseguire. Ad esempio, se vuoi eseguire query sulle voci in base al timestamp, ma devi restituire solo i risultati per un singolo utente alla volta, puoi anteporre al timestamp l'ID utente e indicizzare la nuova proprietà. Ciò consentirebbe comunque l'esecuzione di query e risultati ordinati per l'utente, ma la presenza dell'ID utente garantirà che l'indice stesso venga partizionato correttamente.

Aumento del traffico

Aumenta gradualmente il traffico verso nuovi tipi o parti dello spazio delle chiavi.

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

Questa sequenza di applicazione graduale è particolarmente importante se modifichi il tuo codice per smettere di usare il tipo A e invece usare il tipo B. Un modo ingenuo per gestire questa migrazione è modificare il codice in modo da leggere il tipo B e, se non esiste, leggere il tipo A. Tuttavia, questo potrebbe causare un improvviso aumento del traffico verso un nuovo tipo con una porzione molto ridotta dello spazio delle chiavi.

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

La strategia che utilizzi per migrare le entità a un nuovo tipo o chiave dipenderà dal modello dei dati. Di seguito è riportato un esempio di strategia, nota come "Letture parallele". Dovrai capire se questa strategia è efficace o meno per i tuoi dati. Una considerazione importante sarà l'impatto sui costi delle operazioni parallele durante la migrazione.

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

Una possibile strategia per incrementare gradualmente le letture o le scritture su 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 a quelle nuove. Il job batch dovrebbe evitare le scritture in chiavi sequenziali per prevenire gli hotspot. Quando il job batch è completo, puoi leggere solo dalla nuova posizione.

Un perfezionamento di questa strategia consiste nell'eseguire la migrazione di piccoli gruppi di utenti contemporaneamente. Aggiungi un campo all'entità utente per monitorare lo stato della migrazione dell'utente. Seleziona un gruppo 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 gruppo di utenti. Gli utenti la cui migrazione è in corso utilizzeranno le letture parallele.

Tieni presente che non puoi eseguire facilmente il rollback a meno che non esegui una doppia scrittura delle entità vecchie e nuove durante la fase di migrazione. Ciò comporterebbe un aumento dei costi in modalità Datastore sostenuti.

Eliminazioni

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

Firestore in modalità Datastore riscrive periodicamente le proprie 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à in modalità Datastore su 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.

È un anti-pattern per utilizzare un valore timestamp per un campo indicizzato per rappresentare la 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 delle chiavi con voci di indice per le entità eliminate più di recente.

Puoi migliorare le prestazioni con le "query con sharding", che antepongono una stringa di lunghezza fissa al timestamp di scadenza. L'indice viene ordinato in base all'intera stringa, in modo che le entità con lo stesso timestamp vengano posizionate in tutto l'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 che viene aggiornato periodicamente. Il numero di generazione viene anteposto al timestamp di scadenza in modo che le query vengano ordinate per numero di generazione, shard e timestamp. L'eliminazione delle vecchie entità avviene in una generazione precedente. Per le entità non eliminate dovrebbe essere incrementato il numero di generazione. Una volta completata l'eliminazione, si passa alla generazione successiva. Le query su una generazione precedente avranno prestazioni scarse fino al completamento della compattazione. Potrebbe essere necessario attendere il completamento di diverse generazioni prima di eseguire query sull'indice per ottenere l'elenco di entità da eliminare, al fine di ridurre il rischio di mancanti dei 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 superiore a quella consentita da Firestore in modalità Datastore. Utilizzando questa strategia, archivierai N copie della stessa entità, consentendo una frequenza di letture N volte superiore a quella supportata da una singola entità.

Puoi utilizzare lo sharding se devi scrivere in una parte dell'intervallo di chiavi a una frequenza superiore rispetto a quanto consentito da Firestore in modalità Datastore. Lo sharding suddivide un'entità in parti più piccole.

Ecco alcuni errori comuni che possono verificarsi durante lo sharding:

  • Eseguire il partizionamento orizzontale con un prefisso temporale. Quando passi al prefisso successivo, la nuova parte non suddivisa diventa un hotspot. Devi invece trasferire gradualmente su una parte delle scritture al nuovo prefisso.

  • Eseguire il sharding solo delle entità più calde. Se esegui lo sharding di una piccola parte del numero totale di entità, potrebbero non esserci abbastanza righe tra le entità calde per garantire che rimangano in suddivisioni diverse.

Passaggi successivi