Best practice

Puoi utilizzare le best practice elencate qui come riferimento rapido per ciò che devi tenere presente quando crei un'applicazione che utilizza Firestore in modalità Datastore. Se hai appena iniziato a utilizzare la modalità Datastore, questa pagina potrebbe non essere la migliore da cui iniziare, perché non spiega le nozioni di base su come utilizzare la modalità Datastore. Se sei un nuovo utente, ti consigliamo di iniziare con Introduzione a Firestore in modalità 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à della modalità 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.

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. Se lookup() e la query non hanno una dipendenza di dati, non è necessario attendere in modo sincrono il completamento di lookup() prima di avviare la query.

Entità

  • 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.

  • 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à. 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 file di esportazione della modalità Datastore in 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, la modalità 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 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.

  • Se specifichi una chiave o memorizzi il nome generato, in un secondo momento puoi eseguire un lookup() su quell'entità senza dover emettere una query per trovarla.

Indici

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.

Progettazione per la scalabilità

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

Aggiornamenti di un'entità

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

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

Frequenze di lettura/scrittura elevate per un intervallo di chiavi ristretto

Evita frequenze di lettura o scrittura elevate per chiudere i documenti in ordine alfabetico, altrimenti la tua applicazione riscontrerà errori di contesa. Questo problema è noto come hotspotting e la tua applicazione può verificarlo se esegue una delle seguenti operazioni:

  • Crea nuove entità a una frequenza molto elevata e assegna i propri ID in modo monotonicamente crescente.

    La modalità Datastore alloca le chiavi utilizzando un algoritmo di dispersione. Non dovresti riscontrare hotspot sulle scritture se crei nuove entità utilizzando la distribuzione automatica degli ID entità.

  • Crea nuove entità a una frequenza molto elevata utilizzando il criterio di allocazione degli ID sequenziali precedente.

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

  • Crea nuove entità con un valore di proprietà indicizzato e in aumento monotonico, ad esempio un timestamp, a una frequenza molto elevata.

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

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

Se si verifica un aumento improvviso della frequenza di scrittura per un piccolo intervallo di chiavi, le scritture possono essere lente a causa di un hotspot. La modalità Datastore suddivide 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.

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

In alcuni casi, un hotspot può avere un impatto più ampio su un'applicazione rispetto all'impedire le letture o le 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.

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 una query su una proprietà monotonicamente crescente (o decrescente) utilizzando un'ordinamento o un filtro, puoi eseguire l'indicizzazione su una nuova proprietà, per la quale anteponi il valore monotonico con un valore con 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 restituire i risultati solo per un singolo utente alla volta, puoi anteporre al timestamp l'ID utente e indicizzare questa nuova proprietà. In questo modo, verrebbero comunque consentite query e risultati ordinati per l'utente, ma la presenza dell'ID utente garantirebbe che l'indice stesso sia ben suddiviso in parti.

Aumento del traffico

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

Devi aumentare gradualmente il traffico verso i nuovi tipi per dare a Firestore in modalità Datastore il tempo sufficiente per prepararsi all'aumento del traffico. Consigliamo un massimo di 500 operazioni al secondo per un nuovo tipo, 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.

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 hotspot. 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ò 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à in modalità Datastore in un piccolo intervallo di chiavi, le query in questa parte dell'indice saranno più lente fino al completamento del consolidamento. 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 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. 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 Firestore in modalità Datastore. 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