Se una raccolta contiene documenti con valori indicizzati sequenziali, Cloud Firestore limita la frequenza di scrittura a 500 scritture al secondo. Questa pagina descrive come eseguire lo sharding di un campo del documento per superare questo limite. Innanzitutto, definiamo che cosa si intende per "campi indicizzati in sequenza" e chiariamo quando si applica questo limite.
Campi indicizzati in sequenza
Per "campi indicizzati sequenziali" si intende qualsiasi raccolta di documenti che contenga un
campo indicizzato in aumento o in diminuzione in modo monotono. In molti casi, si tratta di un campo timestamp
, ma qualsiasi valore di campo in aumento o in diminuzione monotonica può attivare il limite di scrittura di 500 scritture al secondo.
Ad esempio, il limite si applica a una raccolta di documenti user
con
campo indicizzato userid
se l'app assegna valori userid
come segue:
1281, 1282, 1283, 1284, 1285, ...
D'altra parte, non tutti i campi timestamp
attivano questo limite. Se un campo timestamp
monitora valori distribuiti in modo casuale, il limite di scrittura non si applica. Anche il valore effettivo del campo non è importante, ma solo che il campo sia in aumento o in diminuzione in modo monotonico. Ad esempio, entrambi i seguenti insiemi di valori di campo in aumento monotonico attivano il limite di scrittura:
100000, 100001, 100002, 100003, ...
0, 1, 2, 3, ...
Suddivisione in parti di un campo timestamp
Supponiamo che la tua app utilizzi un campo timestamp
in aumento monotonicamente.
Se la tua app non utilizza il campo timestamp
in nessuna query, puoi rimuovere il limite di 500 scritture al secondo non indicizzando il campo timestamp. Se hai bisogno di un campo timestamp
per le tue query, puoi aggirare il limite utilizzando i timestamp suddivisi in parti:
- Aggiungi un campo
shard
accanto al campotimestamp
. Utilizza1..n
valori distinta per il camposhard
. In questo modo, il limite di scrittura per la raccolta viene aumentato a500*n
, ma devi aggregaren
query. - Aggiorna la logica di scrittura in modo da assegnare in modo casuale un valore
shard
a ogni documento. - Aggiorna le query per aggregare i set di risultati suddivisi in parti.
- Disattiva gli indici a campo singolo sia per il campo
shard
sia per il campotimestamp
. Elimina gli indici composti esistenti che contengono il campotimestamp
. - Crea nuovi indici composti per supportare le query aggiornate. L'ordine
delle voci in un indice è importante e il campo
shard
deve precedere il campotimestamp
. Tutti gli indici che includono iltimestamp
campo devono includere anche il camposhard
.
Devi implementare i timestamp suddivisi in parti solo in casi d'uso con velocità di scrittura sostenute superiori a 500 scritture al secondo. In caso contrario, si tratta di un'ottimizzazione prematura. Lo sharding di un campo timestamp
rimuove la limitazione di 500 scritture
al secondo, ma comporta la necessità di aggregazioni di query lato client.
Gli esempi riportati di seguito mostrano come eseguire lo sharding di un campo timestamp
e come eseguire query su un set di risultati suddiviso in parti.
Modello di dati e query di esempio
Ad esempio, immagina un'app per l'analisi quasi in tempo reale di strumenti finanziari come valute, azioni comuni ed ETF. Questa app scrive
documenti in una raccolta instruments
come segue:
Node.js
async function insertData() { const instruments = [ { symbol: 'AAA', price: { currency: 'USD', micros: 34790000 }, exchange: 'EXCHG1', instrumentType: 'commonstock', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.010Z')) }, { symbol: 'BBB', price: { currency: 'JPY', micros: 64272000000 }, exchange: 'EXCHG2', instrumentType: 'commonstock', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.101Z')) }, { symbol: 'Index1 ETF', price: { currency: 'USD', micros: 473000000 }, exchange: 'EXCHG1', instrumentType: 'etf', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.001Z')) } ]; const batch = fs.batch(); for (const inst of instruments) { const ref = fs.collection('instruments').doc(); batch.set(ref, inst); } await batch.commit(); }
Questa app esegue le seguenti query e ordina in base al campo timestamp
:
Node.js
function createQuery(fieldName, fieldOperator, fieldValue, limit = 5) { return fs.collection('instruments') .where(fieldName, fieldOperator, fieldValue) .orderBy('timestamp', 'desc') .limit(limit) .get(); } function queryCommonStock() { return createQuery('instrumentType', '==', 'commonstock'); } function queryExchange1Instruments() { return createQuery('exchange', '==', 'EXCHG1'); } function queryUSDInstruments() { return createQuery('price.currency', '==', 'USD'); }
insertData() .then(() => { const commonStock = queryCommonStock() .then( (docs) => { console.log('--- queryCommonStock: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); const exchange1Instruments = queryExchange1Instruments() .then( (docs) => { console.log('--- queryExchange1Instruments: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); const usdInstruments = queryUSDInstruments() .then( (docs) => { console.log('--- queryUSDInstruments: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); return Promise.all([commonStock, exchange1Instruments, usdInstruments]); });
Dopo alcune ricerche, stabilisci che l'app riceverà tra 1000 e 1500 aggiornamenti degli strumenti al secondo. Questo valore supera le 500 scritture al
secondo consentite per le raccolte contenenti documenti con campi
indicizzati con timestamp. Per aumentare la velocità effettiva di scrittura, hai bisogno di 3 valori di shard,
MAX_INSTRUMENT_UPDATES/500 = 3
. Questo esempio utilizza i valori del frammento x
,
y
e z
. Puoi anche utilizzare numeri o altri caratteri per i valori del frammento.
Aggiunta di un campo del frammento
Aggiungi un campo shard
ai tuoi documenti. Imposta il campo shard
su valori x
, y
o z
, che aumentano il limite di scrittura della raccolta a 1500 scritture al secondo.
Node.js
// Define our 'K' shard values const shards = ['x', 'y', 'z']; // Define a function to help 'chunk' our shards for use in queries. // When using the 'in' query filter there is a max number of values that can be // included in the value. If our number of shards is higher than that limit // break down the shards into the fewest possible number of chunks. function shardChunks() { const chunks = []; let start = 0; while (start < shards.length) { const elements = Math.min(MAX_IN_VALUES, shards.length - start); const end = start + elements; chunks.push(shards.slice(start, end)); start = end; } return chunks; } // Add a convenience function to select a random shard function randomShard() { return shards[Math.floor(Math.random() * Math.floor(shards.length))]; }
async function insertData() { const instruments = [ { shard: randomShard(), // add the new shard field to the document symbol: 'AAA', price: { currency: 'USD', micros: 34790000 }, exchange: 'EXCHG1', instrumentType: 'commonstock', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.010Z')) }, { shard: randomShard(), // add the new shard field to the document symbol: 'BBB', price: { currency: 'JPY', micros: 64272000000 }, exchange: 'EXCHG2', instrumentType: 'commonstock', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.101Z')) }, { shard: randomShard(), // add the new shard field to the document symbol: 'Index1 ETF', price: { currency: 'USD', micros: 473000000 }, exchange: 'EXCHG1', instrumentType: 'etf', timestamp: Timestamp.fromMillis( Date.parse('2019-01-01T13:45:23.001Z')) } ]; const batch = fs.batch(); for (const inst of instruments) { const ref = fs.collection('instruments').doc(); batch.set(ref, inst); } await batch.commit(); }
Eseguire query sul timestamp frazionato
L'aggiunta di un campo shard
richiede l'aggiornamento delle query per aggregare i risultati suddivisi in parti:
Node.js
function createQuery(fieldName, fieldOperator, fieldValue, limit = 5) { // For each shard value, map it to a new query which adds an additional // where clause specifying the shard value. return Promise.all(shardChunks().map(shardChunk => { return fs.collection('instruments') .where('shard', 'in', shardChunk) // new shard condition .where(fieldName, fieldOperator, fieldValue) .orderBy('timestamp', 'desc') .limit(limit) .get(); })) // Now that we have a promise of multiple possible query results, we need // to merge the results from all of the queries into a single result set. .then((snapshots) => { // Create a new container for 'all' results const docs = []; snapshots.forEach((querySnapshot) => { querySnapshot.forEach((doc) => { // append each document to the new all container docs.push(doc); }); }); if (snapshots.length === 1) { // if only a single query was returned skip manual sorting as it is // taken care of by the backend. return docs; } else { // When multiple query results are returned we need to sort the // results after they have been concatenated. // // since we're wanting the `limit` newest values, sort the array // descending and take the first `limit` values. By returning negated // values we can easily get a descending value. docs.sort((a, b) => { const aT = a.data().timestamp; const bT = b.data().timestamp; const secondsDiff = aT.seconds - bT.seconds; if (secondsDiff === 0) { return -(aT.nanoseconds - bT.nanoseconds); } else { return -secondsDiff; } }); return docs.slice(0, limit); } }); } function queryCommonStock() { return createQuery('instrumentType', '==', 'commonstock'); } function queryExchange1Instruments() { return createQuery('exchange', '==', 'EXCHG1'); } function queryUSDInstruments() { return createQuery('price.currency', '==', 'USD'); }
insertData() .then(() => { const commonStock = queryCommonStock() .then( (docs) => { console.log('--- queryCommonStock: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); const exchange1Instruments = queryExchange1Instruments() .then( (docs) => { console.log('--- queryExchange1Instruments: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); const usdInstruments = queryUSDInstruments() .then( (docs) => { console.log('--- queryUSDInstruments: '); docs.forEach((doc) => { console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`); }); } ); return Promise.all([commonStock, exchange1Instruments, usdInstruments]); });
Aggiorna le definizioni degli indici
Per rimuovere il vincolo di 500 scritture al secondo, elimina gli indici a campo singolo
e composti esistenti che utilizzano il campo timestamp
.
Eliminare le definizioni degli indici composti
Console Firebase
Apri la pagina Cloud Firestore Indici compositi nella Console Firebase.
Per ogni indice che contiene il campo
timestamp
, fai clic sul pulsante e poi su Elimina.
Console di GCP
Nella console Google Cloud, vai alla pagina Database.
Seleziona il database richiesto dall'elenco dei database.
Nel menu di navigazione, fai clic su Indici e poi sulla scheda Composto.
Utilizza il campo Filtra per cercare le definizioni di indici che contengono il campo
timestamp
.Per ciascuno di questi indici, fai clic sul pulsante
e poi su Elimina.
interfaccia a riga di comando di Firebase
- Se non hai configurato l'interfaccia a riga di comando di Firebase, segui queste istruzioni per installarla e per eseguire il comando
firebase init
. Durante il comandoinit
, assicurati di selezionareFirestore: Deploy rules and create indexes for Firestore
. - Durante la configurazione, l'interfaccia a riga di comando di Firebase scarica le definizioni degli indici esistenti in un file denominato, per impostazione predefinita,
firestore.indexes.json
. Rimuovi eventuali definizioni di indici che contengono il campo
timestamp
, ad esempio:{ "indexes": [ // Delete composite index definition that contain the timestamp field { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "exchange", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "instrumentType", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "price.currency", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, ] }
Esegui il deployment delle definizioni degli indici aggiornate:
firebase deploy --only firestore:indexes
Aggiorna le definizioni degli indici a campo singolo
Console Firebase
Apri la pagina Cloud Firestore Indici a campo singolo nella Console Firebase.
Fai clic su Aggiungi esenzione.
In ID raccolta, inserisci
instruments
. In Percorso campo, inseriscitimestamp
.In Ambito della query, seleziona sia Raccolta sia Gruppo di raccolte.
Fai clic su Avanti.
Imposta tutte le impostazioni dell'indice su Disattivato. Fai clic su Salva.
Ripeti gli stessi passaggi per il campo
shard
.
Console di GCP
Nella console Google Cloud, vai alla pagina Database.
Seleziona il database richiesto dall'elenco dei database.
Nel menu di navigazione, fai clic su Indici e poi sulla scheda Campo singolo.
Fai clic sulla scheda Campo singolo.
Fai clic su Aggiungi esenzione.
In ID raccolta, inserisci
instruments
. In Percorso campo, inseriscitimestamp
.In Ambito della query, seleziona sia Raccolta sia Gruppo di raccolte.
Fai clic su Avanti.
Imposta tutte le impostazioni dell'indice su Disattivato. Fai clic su Salva.
Ripeti gli stessi passaggi per il campo
shard
.
interfaccia a riga di comando di Firebase
Aggiungi quanto segue alla sezione
fieldOverrides
del file delle definizioni dell'indice:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }
Esegui il deployment delle definizioni degli indici aggiornate:
firebase deploy --only firestore:indexes
Creare nuovi indici composti
Dopo aver rimosso tutti gli indici precedenti contenenti timestamp
,
definisci i nuovi indici richiesti dalla tua app. Qualsiasi indice contenente il
timestamp
deve contenere anche il campo shard
. Ad esempio, per supportare
le query precedenti, aggiungi i seguenti indici:
Raccolta | Campi indicizzati | Ambito di query |
---|---|---|
strumentazioni | shard, price.currency, timestamp | Raccolta |
strumentazioni | shard, exchange, timestamp | Raccolta |
strumentazioni | shard, instrumentType, timestamp | Raccolta |
Messaggi di errore
Puoi creare questi indici eseguendo le query aggiornate.
Ogni query restituisce un messaggio di errore con un link per creare l'indice richiesto nella Console Firebase.
interfaccia a riga di comando di Firebase
Aggiungi i seguenti indici al file di definizione dell'indice:
{ "indexes": [ // New indexes for sharded timestamps { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "shard", "order": "DESCENDING" }, { "fieldPath": "exchange", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "shard", "order": "DESCENDING" }, { "fieldPath": "instrumentType", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, { "collectionGroup": "instruments", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "shard", "order": "DESCENDING" }, { "fieldPath": "price.currency", "order": "ASCENDING" }, { "fieldPath": "timestamp", "order": "DESCENDING" } ] }, ] }
Esegui il deployment delle definizioni degli indici aggiornate:
firebase deploy --only firestore:indexes
Informazioni sul limite di scrittura per i campi indicizzati sequenziali
Il limite alla frequenza di scrittura per i campi indicizzati sequenziali deriva dal modo in cui Cloud Firestore memorizza i valori dell'indice e scala le scritture dell'indice. Per ogni scrittura dell'indice, Cloud Firestore definisce una voce chiave-valore che concatena il nome del documento e il valore di ogni campo indicizzato. Cloud Firestore organizza queste voci dell'indice in gruppi di dati chiamati tablet. Ogni Cloud Firestore server contiene uno o più tablet. Quando il carico di scrittura su un determinato tablet diventa troppo elevato, Cloud Firestore esegue la scalabilità orizzontale dividendo il tablet in tablet più piccoli e distribuendo i nuovi tablet su diversi server Cloud Firestore.
Cloud Firestore inserisce le voci dell'indice in ordine alfabetico sullo stesso tablet. Se i valori dell'indice in un tablet sono troppo vicini, ad esempio per i campi timestamp, Cloud Firestore non può suddividere in modo efficiente il tablet in tablet più piccoli. In questo modo si crea un hotspot in cui un singolo tablet riceve troppo traffico e le operazioni di lettura e scrittura sull'hotspot diventano più lente.
Se esegui lo sharding di un campo timestamp, consenti a Cloud Firestore di suddividere in modo efficiente i carichi di lavoro su più tablet. Sebbene i valori del campo timestamp possano rimanere vicini, il valore dell'indice e del frammento concatenato offre Cloud Firestore spazio sufficiente tra le voci dell'indice per suddividerle in più tablet.
Passaggi successivi
- Leggi le best practice per la progettazione su larga scala
- Per i casi con tassi di scrittura elevati in un singolo documento, consulta Contatori distribuiti
- Consulta i limiti standard per Cloud Firestore