Comprendi le operazioni di lettura e scrittura su larga scala

Leggi questo documento per prendere decisioni consapevoli sulla progettazione delle tue applicazioni per garantire prestazioni elevate e affidabilità. Questo documento include argomenti avanzati di Firestore. Se hai appena iniziato a utilizzare Firestore, consulta la guida rapida.

Firestore è un database flessibile e scalabile per lo sviluppo di dispositivi mobili, web e server di Firebase e Google Cloud. È molto facile iniziare a utilizzare Firestore e scrivere applicazioni ricche e potenti.

Per assicurarti che le tue applicazioni continuino a funzionare bene con l'aumento delle dimensioni del database e del traffico, è utile comprendere il meccanismo di lettura e scrittura nel backend di Firestore. Devi anche comprendere l'interazione delle operazioni di lettura e scrittura con il livello di archiviazione e i vincoli sottostanti che potrebbero influire sulle prestazioni.

Prima di progettare l'applicazione, consulta le sezioni seguenti per conoscere le best practice.

Comprendere i componenti di alto livello

Il seguente diagramma mostra i componenti di alto livello coinvolti in una richiesta API Firestore.

Componenti di alto livello

SDK e librerie client Firestore

Firestore supporta gli SDK e le librerie client per diverse piattaforme. Mentre un'app può effettuare chiamate HTTP e RPC dirette all'API Firestore, le librerie client forniscono un livello di astrazione per semplificare l'utilizzo dell'API e implementare le best practice. Possono anche fornire funzionalità aggiuntive come l'accesso offline, le cache e così via.

Google Front End (GFE)

Si tratta di un servizio di infrastruttura comune a tutti i servizi Google Cloud. Il GFE accetta le richieste in arrivo e le inoltra al servizio Google pertinente (il servizio Firestore in questo contesto). Fornisce inoltre altre funzionalità importanti, inclusa la protezione dagli attacchi Denial of Service.

Servizio Firestore

Il servizio Firestore esegue controlli sulla richiesta API, inclusi autenticazione, autorizzazione, controlli della quota e regole di sicurezza, e gestisce le transazioni. Questo servizio Firestore include un client di archiviazione che interagisce con il livello di archiviazione per le operazioni di lettura e scrittura dei dati.

Livello di archiviazione Firestore

Il livello di archiviazione Firestore è responsabile dell'archiviazione di dati e metadati e delle funzionalità del database associate fornite da Firestore. Le seguenti sezioni descrivono l'organizzazione dei dati nel livello di archiviazione Firestore e la scalabilità del sistema. Scoprire come sono organizzati i dati può aiutarti a progettare un modello dei dati scalabile e a comprendere meglio le best practice di Firestore.

Intervalli e suddivisioni chiave

Firestore è un database NoSQL orientato ai documenti. I dati vengono memorizzati in documenti, organizzati in gerarchie di raccolte. La gerarchia di raccolta e l'ID documento vengono convertiti in una singola chiave per ogni documento. I documenti vengono archiviati logicamente e ordinati lessicograficamente in base a questa singola chiave. Utilizziamo il termine intervallo di chiavi per fare riferimento a un intervallo lessicograficamente contiguo di chiavi.

Un tipico database Firestore è troppo grande per poter essere posizionato su una singola macchina fisica. Ci sono anche scenari in cui il carico di lavoro sui dati è troppo pesante per essere gestito da una singola macchina. Per gestire carichi di lavoro di grandi dimensioni, Firestore partiziona i dati in parti separate che possono essere archiviate e fornite da più macchine o server di archiviazione. Queste partizioni vengono create nelle tabelle del database in blocchi di intervalli di chiavi denominati suddivisioni.

Replica sincrona

È importante notare che il database viene sempre replicato automaticamente e in modo sincrono. Le suddivisioni dei dati hanno repliche in zone diverse per mantenerle disponibili anche quando una zona diventa inaccessibile. Una replica coerente sulle diverse copie della suddivisione è gestita dall'algoritmo Paxos per consenso. Una replica di ogni suddivisione viene scelta per fungere da leader Paxos, che è responsabile della gestione delle scritture su quella suddivisione. La replica sincrona ti offre la possibilità di leggere sempre la versione più recente dei dati da Firestore.

Il risultato complessivo di tutto ciò è un sistema scalabile e ad alta disponibilità che fornisce latenze basse sia per le letture che per le scritture, indipendentemente dai carichi di lavoro pesanti e su larga scala.

Layout dei dati

Firestore è un database di documenti senza schema. Tuttavia, dispone internamente i dati principalmente in due tabelle in stile database relazionale nel proprio livello di archiviazione come segue:

  • Tabella Documenti: i documenti vengono archiviati in questa tabella.
  • Tabella Indici: in questa tabella vengono archiviate le voci di indice che consentono di ottenere risultati in modo efficiente e ordinate per valore di indice.

Il seguente diagramma mostra l'aspetto delle tabelle per un database Firestore con le suddivisioni. Le suddivisioni vengono replicate in tre diverse zone e a ogni suddivisione è assegnata un leader Paxos.

Layout dei dati

Una o più regioni

Quando crei un database, devi selezionare una regione o più regioni.

Una singola località regionale è una località geografica specifica, ad esempio us-west1. Le suddivisioni dei dati di un database Firestore includono repliche in zone diverse all'interno della regione selezionata, come spiegato in precedenza.

Una località a più regioni è composta da un insieme definito di regioni in cui sono archiviate le repliche del database. In un deployment di Firestore in più regioni, due regioni hanno repliche complete di tutti i dati nel database. Una terza regione ha una replica di replica che non gestisce un set completo di dati, ma partecipa alla replica. Replicando i dati tra più regioni, i dati sono disponibili per la scrittura e la lettura anche in caso di perdita di un'intera regione.

Per ulteriori informazioni sulle località di una regione, consulta l'articolo Sedi di Firestore.

Una o più regioni

Informazioni sulla vita di una scrittura in Firestore

Un client Firestore può scrivere dati creando, aggiornando o eliminando un singolo documento. La scrittura in un singolo documento richiede l'aggiornamento sia del documento che delle voci di indice associate a livello atomico nel livello di archiviazione. Firestore supporta anche le operazioni atomiche composte da più letture e/o scritture su uno o più documenti.

Per tutti i tipi di scrittura, Firestore fornisce le proprietà ACID (atomicità, coerenza, isolamento e durabilità) dei database relazionali. Firestore fornisce inoltre la serializzabilità, il che significa che tutte le transazioni vengono visualizzate come se fossero eseguite in un ordine seriale.

Passaggi generali in una transazione di scrittura

Quando il client Firestore invia una scrittura o esegue il commit di una transazione, utilizzando uno dei metodi menzionati in precedenza, internamente viene eseguita una transazione di lettura-scrittura del database nel livello di archiviazione. La transazione consente a Firestore di fornire le proprietà ACID menzionate in precedenza.

Come primo passaggio di una transazione, Firestore legge il documento esistente e determina le mutazioni da apportare ai dati nella tabella Documenti.

Ciò include anche l'applicazione degli aggiornamenti necessari alla tabella Indici come segue:

  • I campi aggiunti ai documenti devono avere gli inserti corrispondenti nella tabella Indici.
  • Ai campi che vengono rimossi dai documenti è necessario che ci siano eliminazioni corrispondenti nella tabella Indici.
  • I campi che vengono modificati nei documenti devono essere eliminati (per i valori precedenti) e inseriti (per i valori nuovi) nella tabella Indici.

Per calcolare le mutazioni menzionate in precedenza, Firestore legge la configurazione di indicizzazione per il progetto. La configurazione di indicizzazione archivia le informazioni sugli indici di un progetto. Firestore utilizza due tipi di indici: a campo singolo e composito. Per una comprensione dettagliata degli indici creati in Firestore, vedi Tipi di indice in Firestore.

Una volta calcolate le mutazioni, Firestore le raccoglie all'interno di una transazione e ne esegue il commit.

Informazioni su una transazione di scrittura nel livello di archiviazione

Come già detto, una scrittura in Firestore prevede una transazione di lettura-scrittura nel livello di archiviazione. A seconda del layout dei dati, una scrittura potrebbe comportare una o più suddivisioni come indicato nel layout dei dati.

Nel diagramma seguente, il database Firestore ha otto suddivisioni (contrassegnate da 1-8) ospitate su tre diversi server di archiviazione in un'unica zona e ogni suddivisione viene replicata in tre o più zone diverse. Ogni segmento ha un leader Paxos, che potrebbe trovarsi in una zona diversa a seconda dei segmenti.

Suddivisione del database Firestore

Considera un database Firestore con la raccolta Restaurants come segue:

Raccolta di ristoranti

Il client Firestore richiede la seguente modifica a un documento nella raccolta Restaurant aggiornando il valore del campo priceCategory.

Passa a un documento nella raccolta

I seguenti passaggi generali descrivono cosa succede durante la scrittura:

  1. Creare una transazione di lettura-scrittura.
  2. Leggi il documento restaurant1 nella raccolta Restaurants dalla tabella Documenti del livello di archiviazione.
  3. Leggi gli indici del documento dalla tabella Indici.
  4. Calcola le mutazioni da apportare ai dati. In questo caso, ci sono cinque mutazioni:
    • M1: aggiorna la riga di restaurant1 nella tabella Documenti per riflettere la modifica del valore del campo priceCategory.
    • M2 e M3: elimina le righe relative al vecchio valore di priceCategory nella tabella Indici per indici in ordine decrescente e crescente.
    • M4 e M5: inserisci le righe per il nuovo valore di priceCategory nella tabella Indici per gli indici decrescente e crescente.
  5. Comincia queste mutazioni.

Il client di archiviazione nel servizio Firestore cerca le suddivisioni proprietarie delle chiavi delle righe da modificare. Consideriamo un caso in cui Split 3 serve M1 e Split 6 serve M2-M5. Esiste una transazione distribuita, che coinvolge tutti questi suddivisioni come partecipanti. I suddivisioni dei partecipanti possono includere anche qualsiasi altra suddivisione da cui i dati sono stati letti in precedenza nell'ambito della transazione di lettura/scrittura.

I passaggi seguenti descrivono cosa succede nell'ambito del commit:

  1. Il client di archiviazione invia un commit. Il commit contiene le mutazioni M1-M5.
  2. I segmenti 3 e 6 sono i partecipanti a questa transazione. Uno dei partecipanti viene scelto come coordinatore, ad esempio Dividi 3. Il compito del coordinatore è verificare che la transazione venga confermata o interrotta a livello atomico per tutti i partecipanti.
    • Le repliche leader di queste frazioni sono responsabili del lavoro svolto dai partecipanti e dai coordinatori.
  3. Ciascun partecipante e coordinatore esegue un algoritmo Paxos con le rispettive repliche.
    • Il leader esegue un algoritmo Paxos con le repliche. Il quorum si raggiunge se la maggior parte delle repliche risponde con una risposta ok to commit al leader.
    • Ciascun partecipante avvisa il coordinatore quando viene preparato (prima fase dell'impegno in due fasi). Se un partecipante non può effettuare la transazione, l'intera transazione aborts.
  4. Quando il coordinatore sa che tutti i partecipanti, incluso se stesso, sono pronti, comunica il risultato della transazione accept a tutti i partecipanti (seconda fase del commit in due fasi). In questa fase, ogni partecipante registra la decisione sull'impegno in uno spazio di archiviazione stabile e la transazione viene confermata.
  5. Il coordinatore risponde al client di archiviazione in Firestore che è stato eseguito il commit della transazione. In parallelo, il coordinatore e tutti i partecipanti applicano le mutazioni ai dati.

Commit ciclo di vita

Quando il database Firestore è di piccole dimensioni, può succedere che una singola divisione possieda tutte le chiavi delle mutazioni M1-M5. In tal caso, c'è un solo partecipante alla transazione e il commit in due fasi menzionato in precedenza non è richiesto, rendendo così le scritture più veloci.

Scrive in più regioni

In un deployment su più regioni, la distribuzione delle repliche tra regioni aumenta la disponibilità, ma comporta un costo in termini di prestazioni. La comunicazione tra le repliche in regioni diverse richiede tempi di round trip più lunghi. Di conseguenza, la latenza di base per le operazioni di Firestore è leggermente maggiore rispetto ai deployment a livello di singola regione.

Le repliche sono configurate in modo che la leadership per le suddivisioni rimanga sempre nella regione principale. La regione principale è quella da cui il traffico arriva al server Firestore. Questa decisione della dirigenza riduce il ritardo di round trip nelle comunicazioni tra il client di archiviazione in Firestore e il leader della replica (o il coordinatore per le transazioni multi-split).

Ogni scrittura in Firestore prevede anche un'interazione con il motore in tempo reale di Firestore. Per saperne di più sulle query in tempo reale, consulta Comprendere le query in tempo reale su larga scala.

Comprendi l'andamento di una lettura in Firestore

Questa sezione approfondisce le letture autonome non in tempo reale in Firestore. Internamente, il server Firestore gestisce la maggior parte di queste query in due fasi principali:

  1. Una scansione a un singolo intervallo della tabella Indici.
  2. Ricerche di punti nella tabella Documenti in base al risultato della scansione precedente
Alcune query potrebbero richiedere meno elaborazione (ad esempio, query keys-only per la modalità Datastore) o più elaborazione (ad esempio, query IN) in Firestore.

Le letture dei dati dal livello di archiviazione vengono eseguite internamente utilizzando una transazione del database per garantire letture coerenti. Tuttavia, a differenza delle transazioni utilizzate per le scritture, queste non accettano blocchi. Funzionano invece scegliendo un timestamp ed eseguendo tutte le letture in quel timestamp. Poiché non acquisiscono blocchi, non bloccano le transazioni simultanee di lettura/scrittura. Per eseguire questa transazione, il client di archiviazione in Firestore specifica un timestamp associato, che indica al livello di archiviazione come scegliere un timestamp di lettura. Il tipo di timestamp vincolato scelto dal client di archiviazione in Firestore è determinato dalle opzioni di lettura per la richiesta di lettura.

Informazioni su una transazione di lettura nel livello di archiviazione

Questa sezione descrive i tipi di letture e come vengono elaborate nel livello di archiviazione di Firestore.

Letture forti

Per impostazione predefinita, le letture di Firestore sono a elevata coerenza. Questa elevata coerenza significa che una lettura Firestore restituisce la versione più recente dei dati, che riflette tutte le scritture di cui è stato eseguito il commit fino all'inizio della lettura.

Lettura a divisione singola

Il client di archiviazione in Firestore cerca le suddivisioni proprietarie delle chiavi delle righe da leggere. Supponiamo che debba eseguire una lettura dalla sezione Segmento 3 della sezione precedente. Il client invia la richiesta di lettura alla replica più vicina per ridurre la latenza di round trip.

A questo punto, potrebbero verificarsi i seguenti casi a seconda della replica scelta:

  • La richiesta di lettura va a una replica leader (zona A).
    • Poiché la leader è sempre aggiornata, la lettura può procedere direttamente.
  • La richiesta di lettura va a una replica non leader (ad esempio, zona B)
    • La suddivisione 3 potrebbe sapere in base al suo stato interno di avere informazioni sufficienti per elaborare la lettura e la suddivisione lo fa.
    • La suddivisione 3 non è sicura di aver visualizzato gli ultimi dati. Invia un messaggio al leader per richiedere il timestamp dell'ultima transazione da applicare per pubblicare la lettura. Una volta applicata la transazione, la lettura può continuare.

Firestore restituisce quindi la risposta al client.

Lettura multi-diviso

Nella situazione in cui le letture devono essere eseguite da più suddivisioni, lo stesso meccanismo avviene in tutte le suddivisioni. Una volta restituiti i dati da tutte le suddivisioni, il client di archiviazione in Firestore combina i risultati. Firestore risponde quindi al client con questi dati.

Letture obsolete

Le letture efficaci sono la modalità predefinita in Firestore. Tuttavia, ciò ha un costo potenzialmente più elevato di latenza dovuta alla comunicazione che potrebbe essere richiesta al leader. Spesso l'applicazione Firestore non ha bisogno di leggere l'ultima versione dei dati e la funzionalità è adatta con dati che potrebbero essere inattivi di alcuni secondi.

In tal caso, il client può scegliere di ricevere letture obsolete utilizzando le opzioni di lettura read_time. In questo caso, le letture vengono eseguite poiché i dati si trovavano in read_time ed è molto probabile che la replica più vicina abbia già verificato la presenza di dati in read_time specificato. Per ottenere prestazioni notevolmente migliori, 15 secondi rappresentano un ragionevole valore di inattività. Anche per letture inattive, le righe ottenute sono coerenti tra loro.

Evita hotspot

I split in Firestore vengono suddivisi automaticamente in parti più piccole per distribuire il lavoro di gestione del traffico a più server di archiviazione quando è necessario o quando lo spazio delle chiavi si espande. Le suddivisioni create per gestire il traffico in eccesso vengono conservate per circa 24 ore circa, anche se il traffico scompare. Quindi, in caso di picchi di traffico ricorrenti, le suddivisioni vengono mantenute e, se necessario, vengono introdotte altre suddivisioni. Questi meccanismi aiutano i database Firestore a scalare automaticamente in base all'aumento del carico del traffico o delle dimensioni del database. Tuttavia, esistono alcune limitazioni da tenere presenti, come spiegato di seguito.

La suddivisione dello spazio di archiviazione e del carico richiede tempo e un incremento del traffico troppo rapido potrebbe causare errori di latenza elevata o superamento di scadenze, comunemente denominati hotspot, mentre il servizio si adegua. La best practice consiste nel distribuire le operazioni nell'intervallo di chiavi, aumentando al contempo il traffico su una raccolta in un database con 500 operazioni al secondo. Dopo questo incremento graduale, puoi aumentare il traffico fino al 50% ogni cinque minuti. Questo processo è chiamato regola 500/50/5 e posiziona il database in modo da garantire una scalabilità ottimale per soddisfare il carico di lavoro.

Anche se le suddivisioni vengono create automaticamente con l'aumento del carico, Firestore può suddividere un intervallo di chiavi solo fino a quando non pubblica un singolo documento utilizzando un set dedicato di server di archiviazione replicati. Di conseguenza, volumi elevati e sostenuti di operazioni simultanee su un singolo documento possono portare a un hotspot su tale documento. Se riscontri latenze elevate e costanti in un singolo documento, ti consigliamo di modificare il modello dei dati per suddividere o replicare i dati in più documenti.

Gli errori di contesa si verificano quando più operazioni tentano di leggere e/o scrivere lo stesso documento contemporaneamente.

Un altro caso speciale di hotspotting si verifica quando una chiave che aumenta o diminuisce in sequenza viene utilizzata come ID documento in Firestore ed esiste un numero notevolmente elevato di operazioni al secondo. In questo caso, la creazione di più suddivisioni non è di aiuto, poiché l'impennata del traffico si sposta semplicemente nella suddivisione appena creata. Poiché Firestore indicizza automaticamente tutti i campi del documento per impostazione predefinita, è possibile creare hotspot in movimento anche nello spazio dell'indice di un campo documento contenente un valore crescente/decrescente in sequenza, ad esempio un timestamp.

Tieni presente che, seguendo le procedure descritte sopra, Firestore è in grado di scalare per gestire carichi di lavoro di grandezza arbitraria senza che tu debba modificare alcuna configurazione.

Risoluzione dei problemi

Firestore fornisce il Key Visualizer come strumento di diagnostica appositamente progettato per analizzare i pattern di utilizzo e risolvere i problemi di hotspot.

Passaggi successivi