Wenn eine Sammlung Dokumente mit sequentiell indizierten Werten enthält, begrenzt Cloud Firestore die Schreibrate auf 500 Schreibvorgänge pro Sekunde. Auf dieser Seite wird beschrieben, wie Sie ein Dokumentfeld teilen, um dieses Limit zu überwinden. Lassen Sie uns zunächst definieren, was wir unter „sequentiellen indizierten Feldern“ verstehen, und klären, wann diese Grenze gilt.
Sequentielle indizierte Felder
„Sequentiell indizierte Felder“ bezeichnet jede Sammlung von Dokumenten, die ein monoton steigendes oder fallendes indiziertes Feld enthält. In vielen Fällen handelt es sich dabei um ein timestamp
, aber jeder monoton steigende oder fallende Feldwert kann das Schreiblimit von 500 Schreibvorgängen pro Sekunde auslösen.
Der Grenzwert gilt beispielsweise für eine Sammlung von user
mit indizierten Feldbenutzer userid
, wenn die App userid
-Werte wie folgt zuweist:
-
1281, 1282, 1283, 1284, 1285, ...
Andererseits lösen nicht alle timestamp
dieses Limit aus. Wenn ein timestamp
zufällig verteilte Werte verfolgt, gilt die Schreibbeschränkung nicht. Auch der tatsächliche Wert des Feldes spielt keine Rolle, nur dass das Feld monoton ansteigt oder abnimmt. Beispielsweise lösen beide der folgenden Sätze monoton steigender Feldwerte das Schreiblimit aus:
-
100000, 100001, 100002, 100003, ...
-
0, 1, 2, 3, ...
Sharding eines Zeitstempelfeldes
Angenommen, Ihre App verwendet ein monoton steigendes timestamp
. Wenn Ihre App das timestamp
in keiner Abfrage verwendet, können Sie die Beschränkung auf 500 Schreibvorgänge pro Sekunde aufheben, indem Sie das Zeitstempelfeld nicht indizieren. Wenn Sie für Ihre Abfragen ein timestamp
benötigen, können Sie das Limit umgehen, indem Sie Shard-Zeitstempel verwenden:
- Fügen Sie neben dem
timestamp
einshard
Feld hinzu. Verwenden Sie1..n
unterschiedliche Werte für dasshard
Feld. Dadurch wird das Schreiblimit für die Sammlung auf500*n
erhöht, Sie müssen jedochn
Abfragen aggregieren. - Aktualisieren Sie Ihre Schreiblogik, um jedem Dokument zufällig einen
shard
Wert zuzuweisen. - Aktualisieren Sie Ihre Abfragen, um die fragmentierten Ergebnismengen zu aggregieren.
- Deaktivieren Sie Einzelfeldindizes sowohl für das
shard
Feld als auch für dastimestamp
. Löschen Sie vorhandene zusammengesetzte Indizes, die dastimestamp
enthalten. - Erstellen Sie neue zusammengesetzte Indizes, um Ihre aktualisierten Abfragen zu unterstützen. Die Reihenfolge der Felder in einem Index ist wichtig und das
shard
-Feld muss vor demtimestamp
stehen. Alle Indizes, die dastimestamp
enthalten, müssen auch dasshard
Feld enthalten.
Sie sollten Shard-Zeitstempel nur in Anwendungsfällen mit anhaltenden Schreibraten über 500 Schreibvorgängen pro Sekunde implementieren. Ansonsten handelt es sich um eine vorzeitige Optimierung. Durch die Aufteilung eines timestamp
wird die Beschränkung auf 500 Schreibvorgänge pro Sekunde aufgehoben, allerdings mit dem Nachteil, dass clientseitige Abfrageaggregationen erforderlich sind.
Die folgenden Beispiele zeigen, wie ein timestamp
fragmentiert und ein fragmentierter Ergebnissatz abgefragt wird.
Beispieldatenmodell und Abfragen
Stellen Sie sich als Beispiel eine App für die Analyse von Finanzinstrumenten wie Währungen, Stammaktien und ETFs nahezu in Echtzeit vor. Diese App schreibt Dokumente wie folgt in eine instruments
:
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(); }
Diese App führt die folgenden Abfragen und Bestellungen anhand des timestamp
aus:
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]); });
Nach einiger Recherche stellen Sie fest, dass die App zwischen 1.000 und 1.500 Instrumentenaktualisierungen pro Sekunde erhält. Dies übersteigt die zulässigen 500 Schreibvorgänge pro Sekunde für Sammlungen, die Dokumente mit indizierten Zeitstempelfeldern enthalten. Um den Schreibdurchsatz zu erhöhen, benötigen Sie 3 Shard-Werte, MAX_INSTRUMENT_UPDATES/500 = 3
. In diesem Beispiel werden die Shard-Werte x
, y
und z
verwendet. Sie können für Ihre Shard-Werte auch Zahlen oder andere Zeichen verwenden.
Hinzufügen eines Shard-Felds
Fügen Sie Ihren Dokumenten ein shard
-Feld hinzu. Legen Sie das shard
Feld auf die Werte x
, y
oder z
fest, wodurch das Schreiblimit für die Sammlung auf 1.500 Schreibvorgänge pro Sekunde erhöht wird.
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(); }
Abfrage des Shard-Zeitstempels
Das Hinzufügen eines shard
Felds erfordert, dass Sie Ihre Abfragen aktualisieren, um Shard-Ergebnisse zu aggregieren:
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]); });
Indexdefinitionen aktualisieren
Um die Einschränkung von 500 Schreibvorgängen pro Sekunde aufzuheben, löschen Sie die vorhandenen Einzelfeld- und zusammengesetzten Indizes, die das timestamp
verwenden.
Zusammengesetzte Indexdefinitionen löschen
Firebase-Konsole
Öffnen Sie die Seite „Cloud Firestore Composite Indexes“ in der Firebase-Konsole.
Klicken Sie für jeden Index, der das
timestamp
enthält, auf die Schaltfläche und dann auf „Löschen“ .
GCP-Konsole
Gehen Sie in der Google Cloud Platform Console zur Seite „Datenbanken“ .
Wählen Sie aus der Liste der Datenbanken die gewünschte Datenbank aus.
Klicken Sie im Navigationsmenü auf Indizes und dann auf die Registerkarte Zusammengesetzt .
Verwenden Sie das Feld „Filter“ , um nach Indexdefinitionen zu suchen, die das
timestamp
enthalten.Klicken Sie für jeden dieser Indizes auf die Schaltfläche
und dann auf „Löschen“ .
Firebase-CLI
- Wenn Sie die Firebase-CLI nicht eingerichtet haben, befolgen Sie diese Anweisungen, um die CLI zu installieren und den
firebase init
Befehl auszuführen . Stellen Sie während desinit
Befehls sicher, dass SieFirestore: Deploy rules and create indexes for Firestore
auswählen. - Während der Einrichtung lädt die Firebase-CLI Ihre vorhandenen Indexdefinitionen in eine Datei mit dem Standardnamen
firestore.indexes.json
herunter. Entfernen Sie alle Indexdefinitionen, die das
timestamp
enthalten, zum Beispiel:{ "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" } ] }, ] }
Stellen Sie Ihre aktualisierten Indexdefinitionen bereit:
firebase deploy --only firestore:indexes
Aktualisieren Sie die Indexdefinitionen für einzelne Felder
Firebase-Konsole
Öffnen Sie die Seite „Cloud Firestore-Einzelfeldindizes“ in der Firebase-Konsole.
Klicken Sie auf Ausnahme hinzufügen .
Geben Sie als Sammlungs-ID „
instruments
ein. Geben Sie als Feldpfadtimestamp
ein.Wählen Sie unter Abfragebereich sowohl Sammlung als auch Sammlungsgruppe aus.
Weiter klicken
Schalten Sie alle Indexeinstellungen auf Deaktiviert . Klicken Sie auf Speichern .
Wiederholen Sie die gleichen Schritte für das
shard
Feld.
GCP-Konsole
Gehen Sie in der Google Cloud Platform Console zur Seite „Datenbanken“ .
Wählen Sie aus der Liste der Datenbanken die gewünschte Datenbank aus.
Klicken Sie im Navigationsmenü auf Indizes und dann auf die Registerkarte Einzelfeld .
Klicken Sie auf die Registerkarte „Einzelfeld“ .
Klicken Sie auf Ausnahme hinzufügen .
Geben Sie als Sammlungs-ID „
instruments
ein. Geben Sie als Feldpfadtimestamp
ein.Wählen Sie unter Abfragebereich sowohl Sammlung als auch Sammlungsgruppe aus.
Weiter klicken
Schalten Sie alle Indexeinstellungen auf Deaktiviert . Klicken Sie auf Speichern .
Wiederholen Sie die gleichen Schritte für das
shard
Feld.
Firebase-CLI
Fügen Sie dem Abschnitt
fieldOverrides
Ihrer Indexdefinitionsdatei Folgendes hinzu:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }
Stellen Sie Ihre aktualisierten Indexdefinitionen bereit:
firebase deploy --only firestore:indexes
Erstellen Sie neue zusammengesetzte Indizes
Nachdem Sie alle vorherigen Indizes entfernt haben, die den timestamp
enthalten, definieren Sie die neuen Indizes, die Ihre App benötigt. Jeder Index, der das timestamp
enthält, muss auch das shard
Feld enthalten. Um beispielsweise die oben genannten Abfragen zu unterstützen, fügen Sie die folgenden Indizes hinzu:
Sammlung | Felder indiziert | Abfragebereich |
---|---|---|
Instrumente | Shard, Preis.Währung, Zeitstempel | Sammlung |
Instrumente | Shard, Austausch, Zeitstempel | Sammlung |
Instrumente | Shard, Instrumententyp, Zeitstempel | Sammlung |
Fehlermeldungen
Sie können diese Indizes erstellen, indem Sie die aktualisierten Abfragen ausführen.
Jede Abfrage gibt eine Fehlermeldung mit einem Link zum Erstellen des erforderlichen Index in der Firebase-Konsole zurück.
Firebase-CLI
Fügen Sie Ihrer Indexdefinitionsdatei die folgenden Indizes hinzu:
{ "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" } ] }, ] }
Stellen Sie Ihre aktualisierten Indexdefinitionen bereit:
firebase deploy --only firestore:indexes
Verstehen des Schreibens für limitierte sequentiell indizierte Felder
Die Begrenzung der Schreibrate für sequenziell indizierte Felder ergibt sich aus der Art und Weise, wie Cloud Firestore Indexwerte speichert und Indexschreibvorgänge skaliert. Für jeden Indexschreibvorgang definiert Cloud Firestore einen Schlüsselwerteintrag, der den Dokumentnamen und den Wert jedes indizierten Felds verkettet. Cloud Firestore organisiert diese Indexeinträge in Datengruppen, die als Tablets bezeichnet werden. Jeder Cloud Firestore-Server enthält ein oder mehrere Tablets. Wenn die Schreiblast auf ein bestimmtes Tablet zu hoch wird, skaliert Cloud Firestore horizontal, indem es das Tablet in kleinere Tablets aufteilt und die neuen Tablets auf verschiedene Cloud Firestore-Server verteilt.
Cloud Firestore platziert lexikografisch nahestehende Indexeinträge auf demselben Tablet. Wenn die Indexwerte in einem Tablet zu nahe beieinander liegen, beispielsweise bei Zeitstempelfeldern, kann Cloud Firestore das Tablet nicht effizient in kleinere Tablets aufteilen. Dadurch entsteht ein Hotspot, an dem ein einzelnes Tablet zu viel Datenverkehr empfängt und Lese- und Schreibvorgänge am Hotspot langsamer werden.
Durch das Sharding eines Zeitstempelfelds ermöglichen Sie Cloud Firestore, Arbeitslasten effizient auf mehrere Tablets aufzuteilen. Obwohl die Werte des Zeitstempelfelds möglicherweise nahe beieinander bleiben, bietet der verkettete Shard- und Indexwert Cloud Firestore genügend Platz zwischen den Indexeinträgen, um die Einträge auf mehrere Tablets aufzuteilen.
Was kommt als nächstes
- Lesen Sie die Best Practices für maßstabsgetreues Entwerfen
- Informationen zu Fällen mit hohen Schreibraten in ein einzelnes Dokument finden Sie unter Gestörte Zähler
- Sehen Sie sich die Standardlimits für Cloud Firestore an