Timestamp sbozzati
Se una raccolta contiene documenti con valori indicizzati sequenziali, Firestore limita la frequenza di scrittura a 500 scritture al secondo. Questa pagina descrive come eseguire lo sharding del campo di un documento per superare questo limite. Per prima cosa, definiamo cosa intendiamo per "campi indicizzati sequenziali" e chiarire quando questo limite .
Campi indicizzati sequenziali
"Campi indicizzati sequenziali" indica qualsiasi raccolta di documenti contenente
aumentando o diminuendo in modo monotonico il campo indicizzato. In molti casi, questo significa
un campo timestamp
, ma qualsiasi valore del campo che aumenta o diminuisce monotonicamente
può attivare il limite di scrittura di 500 scritture al secondo.
Ad esempio, il limite si applica a una raccolta di user
documenti con
campo indicizzato userid
se l'app assegna valori userid
in questo modo:
1281, 1282, 1283, 1284, 1285, ...
Al contrario, non tutti i campi timestamp
attivano questo limite. Se
Il campo timestamp
monitora valori distribuiti casualmente, il limite di scrittura non
. Anche il valore effettivo del campo non ha importanza, ma solo che il campo
aumenta o diminuisce monotonicamente. Ad esempio:
entrambi i seguenti insiemi di valori dei campi che aumentano monotonicamente attivano
il limite di scrittura:
100000, 100001, 100002, 100003, ...
0, 1, 2, 3, ...
Partizionamento orizzontale di un campo timestamp
Supponi che la tua app utilizzi un campo timestamp
che aumenta in modo monotonico.
Se la tua app non utilizza il campo timestamp
nelle query, puoi rimuovere il campo
Limite di 500 scritture al secondo perché non indicizza il campo del timestamp. Se sì
un campo timestamp
per le query, puoi aggirare il limite
utilizzando i timestamp con frazionamento:
- Aggiungi un campo
shard
accanto al campotimestamp
. Usa1..n
distinto per il camposhard
. Questo aumenta la capacità di scrittura limite per la raccolta 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 con sharding.
- Disattiva gli indici a campo singolo sia per il campo
shard
che pertimestamp
. Elimina gli indici composti esistenti che contengonotimestamp
. - Crea nuovi indici composti per supportare le query aggiornate. L'ordine di
i campi di un indice sono importanti e il campo
shard
deve precedere iltimestamp
. Tutti gli indici che includono Il campotimestamp
deve includere anche il camposhard
.
Dovresti implementare i timestamp con sharding solo nei casi d'uso con
oltre le 500 scritture al secondo. Altrimenti, si tratta di un
per un'ottimizzazione prematura. Lo sharding di un campo timestamp
rimuove le 500 scritture
una limitazione al secondo, ma con il compromesso di dover eseguire query lato client
aggregazioni.
I seguenti esempi mostrano come eseguire lo sharding di un campo timestamp
e come eseguire query su
set di risultati con sharding.
Esempio di modello dei dati e query
Ad esempio, immagina un'app per un'analisi quasi in tempo reale
come valute, azioni ordinarie ed ETF. Questa app scrive
documenti in una raccolta instruments
in questo modo:
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 query e gli ordini seguenti 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 qualche ricerca, determini che l'app riceverà tra
1000 e 1500 aggiornamenti degli strumenti al secondo. Questo supera le 500 scritture per
secondo consentito per le raccolte contenenti documenti con timestamp indicizzato
campi. Per aumentare la velocità effettiva di scrittura, sono necessari tre valori di shard:
MAX_INSTRUMENT_UPDATES/500 = 3
. In questo esempio vengono utilizzati i valori di shard x
,
y
e z
. Puoi anche utilizzare numeri o altri caratteri per lo shard
e i relativi valori.
Aggiunta di un campo shard
Aggiungi un campo shard
ai documenti. Imposta il campo shard
ai valori x
, y
o z
, aumentando il limite di scrittura sulla raccolta
fino 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(); }
Esecuzione di query sul timestamp con sharding
L'aggiunta di un campo shard
richiede l'aggiornamento delle query per l'aggregazione
risultati con sharding:
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 definizioni dell'indice
Per rimuovere il vincolo di 500 scritture al secondo, elimina il campo singolo esistente
e indici composti che usano il campo timestamp
.
Elimina definizioni di indici composti
Console Firebase
Apri la pagina Indici composti Firestore nella console Firebase.
Per ogni indice che contiene il campo
timestamp
, fai clic sull'icona e fai clic 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 Composito.
Utilizza il campo Filtro per cercare definizioni di indice che contengono i seguenti elementi:
timestamp
.Per ciascuno di questi indici, fai clic sul
e fai clic 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 installare
nell'interfaccia a riga di comando ed esegui il comando
firebase init
. Durante il comandoinit
, esegui assicurati di selezionareFirestore: Deploy rules and create indexes for Firestore
. - Durante la configurazione, l'interfaccia a riga di comando di Firebase scarica le definizioni di indice esistenti in
un file denominato, per impostazione predefinita,
firestore.indexes.json
. Rimuovi tutte le definizioni di indice contenenti il campo
timestamp
, per 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 di indice aggiornate:
firebase deploy --only firestore:indexes
Aggiorna definizioni di indice a campo singolo
Console Firebase
Apri la pagina Indici a campo singolo di Firestore nella Console Firebase.
Fai clic su Aggiungi esenzione.
In ID raccolta, inserisci
instruments
. Per Percorso campo: inseriscitimestamp
.In Ambito della query, seleziona sia Raccolta che Gruppo di raccolte:
Fai clic su Avanti.
Imposta tutte le impostazioni indice su Disabilitato. 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
. Per Percorso campo: inseriscitimestamp
.In Ambito della query, seleziona sia Raccolta che Gruppo di raccolte:
Fai clic su Avanti.
Imposta tutte le impostazioni indice su Disabilitato. 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
delle definizioni dell'indice file:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }
Esegui il deployment delle definizioni di indice aggiornate:
firebase deploy --only firestore:indexes
Crea nuovi indici composti
Dopo aver rimosso tutti gli indici precedenti contenenti timestamp
,
definire i nuovi indici richiesti dall'app. Qualsiasi indice contenente
Il campo timestamp
deve contenere anche il campo shard
. Ad esempio, per supportare
alle query precedenti, aggiungi i seguenti indici:
Raccolta | Campi indicizzati | Ambito di query |
---|---|---|
strumentazioni | shard | , price.currency, timestampRaccolta |
strumentazioni | shard, scambio, 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 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 di indice aggiornate:
firebase deploy --only firestore:indexes
Comprensione della scrittura per limitare i campi indicizzati sequenziali
Il limite alla frequenza di scrittura per i campi indicizzati sequenziali deriva dal modo in cui Firestore archivia i valori degli indici e scala le scritture degli indici. Per ogni sull'indice, Firestore definisce una voce chiave-valore che concatena il nome del documento e il valore di ogni campo indicizzato. Firestore organizza queste voci di indice in gruppi di dati denominati tablet. Ciascuna Il server Firestore contiene uno o più tablet. Quando il carico di scrittura un determinato tablet diventa troppo alto, Firestore scala orizzontalmente dividendo il tablet in tablet più piccoli e diffondendo i nuovi tablet tra diversi server Firestore.
Firestore inserisce le voci di indice meno significative sullo stesso tablet. Se i valori di indice su una tavoletta sono troppo vicini, ad esempio per con i campi timestamp, Firestore non può suddividere in modo efficiente il tablet in tablet più piccoli. Viene creato un hotspot in cui un singolo tablet riceve troppo traffico e le operazioni di lettura e scrittura sul potrebbero rallentare.
Lo sharding di un campo timestamp, rende possibile per consentire a Firestore di suddividere in modo efficiente i carichi di lavoro tablet. Anche se i valori del campo timestamp potrebbero rimanere vicini, il valore concatenato di shard e indice dà a Firestore spazio sufficiente tra le voci di indice per suddividere le voci tra più tablet.
Passaggi successivi
- Leggi le best practice per la progettazione per la scalabilità
- Per i casi con velocità di scrittura elevate su un singolo documento, consulta Contatori distrutti
- Vedi i limiti standard per Firestore