Comprendere le letture e le scritture su larga scala

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

Per assicurarti che le tue applicazioni continuino a funzionare bene man mano che le dimensioni e il traffico del database aumentano, è utile comprendere il funzionamento delle letture e delle scritture nel backend di Firestore. Devi anche comprendere l'interazione delle letture e delle scritture con il livello di archiviazione e i vincoli sottostanti che potrebbero influire sulle prestazioni.

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

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, librerie client e driver

Firestore supporta SDK, librerie client e driver per piattaforme diverse.

Google Front End (GFE)

Si tratta di un servizio di infrastruttura comune a tutti i Google Cloud servizi. Il GFE accetta le richieste in entrata e le inoltra al servizio Google pertinente (in questo contesto, il servizio Firestore).

Servizio Firestore

Il servizio Firestore esegue controlli sulla richiesta API, tra cui autenticazione, autorizzazione, controlli delle quote e gestione delle transazioni. Questo servizio Firestore include un client di archiviazione che interagisce con il livello di archiviazione per le letture e le scritture dei dati.

Livello di archiviazione di Firestore

Il livello di archiviazione di Firestore è responsabile dell'archiviazione dei dati e dei metadati, nonché delle funzionalità del database associate fornite da Firestore. Le sezioni seguenti descrivono come vengono organizzati i dati nel livello di archiviazione di Firestore e come viene scalato il sistema. Comprendere come vengono organizzati i dati può aiutarti a progettare un modello dei dati scalabile e a comprendere meglio le best practice in Firestore.

Intervalli di chiavi e suddivisioni

Firestore è un database NoSQL orientato ai documenti. I dati vengono archiviati in documenti organizzati in raccolte. Il nome della raccolta e l'ID documento formano una chiave univoca per un documento. I documenti nella stessa raccolta vengono archiviati insieme nello spazio delle chiavi. All'interno di questo spazio delle chiavi, l'ID documento viene sottoposto a hashing. Il termine intervallo di chiavi si riferisce a un intervallo contiguo di chiavi nell'archiviazione.

Firestore partiziona automaticamente i dati all'interno delle raccolte su più server di archiviazione. Queste partizioni sono chiamate suddivisioni.

I documenti possono generare voci di indice ordinate lessicograficamente e partecipare allo stesso tipo di suddivisione e posizionamento dei dati dei documenti.

Replica sincrona

Ogni scrittura viene replicata in modo sincrono nella maggior parte delle repliche utilizzando Paxos. Una replica per suddivisione è considerata un leader e coordina il processo di replica. In caso di errore del leader, viene eletto un nuovo leader. Le repliche si trovano in zone diverse per essere resilienti a potenziali errori di zona. Il risultato complessivo è un sistema scalabile e a elevata disponibilità che fornisce latenze basse sia per le letture che per le scritture, indipendentemente dai carichi di lavoro elevati e su scala molto ampia.

Regione singola o più regioni

Quando crei un database, devi selezionare una singola località regionale o una località a più regioni.

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

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

Per saperne di più sulle località di una regione, consulta Località di Firestore.

Regione singola o multiregionale

Comprendere il ciclo di vita di una scrittura

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

Per tutti i tipi di scritture, Firestore fornisce le proprietà ACID (atomicità, coerenza, isolamento e durabilità) dei database relazionali. Firestore fornisce anche la serializzabilità, il che significa che tutte le transazioni vengono eseguite in ordine seriale.

Passaggi di alto livello in una transazione di scrittura

Quando il driver emette una scrittura o esegue il commit di una transazione utilizzando uno dei metodi menzionati in precedenza, internamente viene eseguita come 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 nel documento.

Sono inclusi anche gli aggiornamenti di tutti gli indici pertinenti:

  • I campi indicizzati che vengono aggiunti ai documenti richiedono inserimenti corrispondenti negli indici.
  • I campi indicizzati che vengono rimossi dai documenti richiedono eliminazioni corrispondenti negli indici.
  • I campi indicizzati che vengono modificati nei documenti richiedono sia eliminazioni (per i valori precedenti) sia inserimenti (per i nuovi valori) negli indici.

Per calcolare le mutazioni menzionate in precedenza, Firestore legge la configurazione di indicizzazione per il progetto. La configurazione di indicizzazione archivia informazioni sugli indici di un progetto.

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

Comprendere una transazione di scrittura nel livello di archiviazione

Come spiegato in precedenza, una scrittura in Firestore comporta una transazione di lettura/scrittura nel livello di archiviazione. A seconda del layout dei dati, una scrittura potrebbe coinvolgere una o più suddivisioni.

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

Suddivisione del database Firestore

Considera un database Firestore con la raccolta Restaurants come segue:

Raccolta di ristoranti

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

Passare a un documento nella raccolta

I seguenti passaggi di alto livello descrivono cosa succede nell'ambito della scrittura:

  1. Crea una transazione di lettura/scrittura.
  2. Leggi il documento restaurant1 nella raccolta Restaurants.
  3. Leggi gli indici del documento.
  4. Calcola le mutazioni da apportare ai dati. In questo caso, ci sono cinque mutazioni:
    • M1: aggiorna la riga per restaurant1 in modo che rifletta la modifica del valore del campo priceCategory.
    • M2 e M3: elimina le voci di indice precedenti per priceCategory.
    • M4 e M5: aggiungi nuove voci di indice per priceCategory.
  5. Esegui il commit di queste mutazioni.

Il client di archiviazione nel servizio Firestore cerca le suddivisioni che possiedono le chiavi delle righe da modificare. Considera un caso in cui la suddivisione 3 gestisce M1 e la suddivisione 6 gestisce M2-M5. Esiste una transazione distribuita che coinvolge tutte queste suddivisioni come partecipanti. Le suddivisioni partecipanti possono includere anche qualsiasi altra suddivisione da cui sono stati letti i dati in precedenza nell'ambito della transazione di lettura/scrittura.

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

  1. Il client di archiviazione emette un commit. Il commit contiene le mutazioni M1-M5.
  2. Le suddivisioni 3 e 6 sono i partecipanti a questa transazione. Uno dei partecipanti viene scelto come coordinatore, ad esempio la suddivisione 3. Il compito del coordinatore è assicurarsi che la transazione esegua il commit o l'interruzione in modo atomico in tutti i partecipanti.
    • Le repliche leader di queste suddivisioni sono responsabili del lavoro svolto dai partecipanti e dai coordinatori.
  3. Ogni partecipante e coordinatore esegue un algoritmo Paxos con le rispettive repliche.
    • Il leader esegue un algoritmo Paxos con le repliche. Il quorum viene raggiunto se la maggior parte delle repliche risponde al leader con una risposta ok to commit.
    • Ogni partecipante notifica quindi al coordinatore quando è preparato (prima fase del commit a due fasi). Se un partecipante non può eseguire il commit della transazione, l'intera transazione aborts.
  4. Una volta che 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 a due fasi). In questa fase, ogni partecipante registra la decisione di commit nell'archiviazione stabile e viene eseguito il commit della transazione.
  5. Il coordinatore risponde al client di archiviazione in Firestore che è stato eseguito il commit della transazione. Parallelamente, il coordinatore e tutti i partecipanti applicano le mutazioni ai dati.

Ciclo di vita del commit

Quando il database Firestore è piccolo, può succedere che una singola suddivisione possieda tutte le chiavi nelle mutazioni M1-M5. In questo caso, nella transazione è presente un solo partecipante e il commit a due fasi menzionato in precedenza non è necessario, il che rende le scritture più veloci.

Scritture in più regioni

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

Configuriamo le repliche in modo che la leadership per le suddivisioni rimanga sempre nella regione principale. La regione principale è quella da cui il traffico in entrata raggiunge il server Firestore. Questa decisione di leadership riduce il ritardo di andata e ritorno nella comunicazione tra il client di archiviazione in Firestore e il leader della replica (o il coordinatore per le transazioni a più suddivisioni).

Comprendere il ciclo di vita di una lettura

Questa sezione approfondisce le letture in Firestore. Le query, in particolare, sono costituite da un mix di letture di documenti e letture di voci di indice.

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

Comprendere una transazione di lettura nel livello di archiviazione

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

Letture a elevata coerenza

Per impostazione predefinita, le letture di Firestore sono a elevata coerenza. Questa elevata coerenza significa che una lettura di 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 di una singola suddivisione

Il client di archiviazione in Firestore cerca le suddivisioni che possiedono le chiavi delle righe da leggere. Supponi che debba eseguire una lettura dalla suddivisione 3 della sezione precedente. Il client invia la richiesta di lettura alla replica più vicina per ridurre la latenza di andata e ritorno.

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

  • La richiesta di lettura va a una replica leader (zona A).
    • Poiché il leader è sempre aggiornato, la lettura può procedere direttamente.
  • La richiesta di lettura va a una replica non leader (ad esempio, la zona B).
    • La suddivisione 3 potrebbe sapere dal suo stato interno di avere informazioni sufficienti per gestire la lettura e lo fa.
    • La suddivisione 3 non è sicura di aver visto gli ultimi dati. Invia un messaggio al leader per chiedere il timestamp dell'ultima transazione che deve applicare per gestire la lettura. Una volta applicata la transazione, la lettura può procedere.

Firestore restituisce quindi la risposta al client.

Lettura di più suddivisioni

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

Evitare gli hotspot

Le suddivisioni in Firestore vengono suddivise 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, anche se il traffico scompare. Pertanto, se si verificano picchi di traffico ricorrenti, le suddivisioni vengono mantenute e vengono introdotte altre suddivisioni quando necessario. Questi meccanismi aiutano i database Firestore a scalare automaticamente in base all'aumento del carico di traffico o delle dimensioni del database. Tuttavia, esistono alcune limitazioni di cui tenere conto.

La suddivisione dell'archiviazione e del carico richiede tempo e l'aumento troppo rapido del traffico può causare latenza elevata o errori di superamento del termine, comunemente chiamati hotspot, mentre il servizio si adatta. La best practice consiste nel distribuire le operazioni nell'intervallo di chiavi, aumentando gradualmente il traffico su una raccolta in un database.

Sebbene le suddivisioni vengano create automaticamente con l'aumento del carico, Firestore può suddividere un intervallo di chiavi solo fino a quando non gestisce un singolo documento utilizzando un insieme 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 quel documento. Se riscontri latenze elevate e sostenute su 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 scrivere contemporaneamente lo stesso documento.

Tieni presente che, seguendo le pratiche descritte in questa pagina, Firestore può scalare per gestire workload di dimensioni arbitrarie senza che tu debba modificare alcuna configurazione.