Jeśli kolekcja zawiera dokumenty z sekwencyjnymi wartościami indeksowanymi, Cloud Firestore ogranicza liczbę operacji zapisu do 500 na sekundę. Z tej strony dowiesz się, jak podzielić pole dokumentu na fragmenty, aby obejść to ograniczenie. Najpierw wyjaśnimy, co rozumiemy przez „sekwencyjne pola indeksowane”, i kiedy to ograniczenie ma zastosowanie.
Sekwencyjne pola indeksowane
„Sekwencyjne pola indeksowane” to dowolna kolekcja dokumentów, która zawiera pole indeksowane rosnąco lub malejąco. W wielu przypadkach jest to pole timestamp, ale dowolna rosnąca lub malejąca wartość pola może spowodować przekroczenie limitu 500 operacji zapisu na sekundę.
Na przykład limit ma zastosowanie do kolekcji dokumentów user z
polem indeksowanym userid jeśli aplikacja przypisuje wartości userid w ten sposób:
1281, 1282, 1283, 1284, 1285, ...
Z drugiej strony nie wszystkie pola timestamp powodują przekroczenie tego limitu. Jeśli pole timestamp śledzi wartości rozproszone losowo, limit operacji zapisu nie ma zastosowania. Rzeczywista wartość pola też nie ma znaczenia – ważne jest tylko to, że pole rośnie lub maleje monotonicznie. Na przykład oba te zestawy rosnących monotonicznie wartości pól powodują przekroczenie limitu operacji zapisu:
100000, 100001, 100002, 100003, ...0, 1, 2, 3, ...
Dzielenie pola sygnatury czasowej na fragmenty
Załóżmy, że Twoja aplikacja używa rosnącego monotonicznie pola timestamp.
Jeśli aplikacja nie używa pola timestamp w żadnych zapytaniach, możesz usunąć limit 500 operacji zapisu na sekundę, nie indeksując pola sygnatury czasowej. Jeśli jednak potrzebujesz pola timestamp w zapytaniach, możesz obejść ten limit, używając sygnatur czasowych podzielonych na fragmenty:
- Dodaj pole
shardobok polatimestamp. Użyj1..nróżnych wartości polashard. Zwiększa to limit operacji zapisu w kolekcji do500*n, ale musisz agregowaćnzapytań. - Zaktualizuj logikę zapisu, aby losowo przypisywać wartość
sharddo każdego dokumentu. - Zaktualizuj zapytania, aby agregować podzielone na fragmenty zestawy wyników.
- Wyłącz indeksy pojedynczych pól zarówno dla pola
shard, jak i polatimestamp. Usuń istniejące indeksy złożone, które zawierają poletimestamp. - Utwórz nowe indeksy złożone, aby obsługiwać zaktualizowane zapytania. Kolejność pól w indeksie ma znaczenie, a pole
shardmusi występować przed polemtimestamp. Wszystkie indeksy, które zawierają poletimestamp, muszą też zawierać poleshard.
Sygnatury czasowe podzielone na fragmenty należy implementować tylko w przypadkach użycia, w których liczba operacji zapisu na sekundę przekracza 500. W przeciwnym razie jest to przedwczesna optymalizacja. Podzielenie pola timestamp na fragmenty usuwa ograniczenie 500 operacji zapisu na sekundę, ale wymaga agregacji zapytań po stronie klienta.
Poniższe przykłady pokazują, jak podzielić pole timestamp na fragmenty i jak wysyłać zapytania do podzielonego na fragmenty zestawu wyników.
Przykładowy model danych i zapytania
Wyobraź sobie aplikację do analizy instrumentów finansowych, takich jak waluty, akcje i fundusze ETF, w czasie zbliżonym do rzeczywistego. Ta aplikacja zapisuje dokumenty w kolekcji instruments w ten sposób:
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(); }
Ta aplikacja uruchamia te zapytania i sortuje je według pola 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]); });
Po przeprowadzeniu badań stwierdzasz, że aplikacja będzie otrzymywać od 1000 do 1500 aktualizacji instrumentów na sekundę. Przekracza to limit 500 operacji zapisu na sekundę dozwolony w przypadku kolekcji zawierających dokumenty z indeksowanymi polami sygnatury czasowej. Aby zwiększyć przepustowość zapisu, potrzebujesz 3 wartości fragmentów: MAX_INSTRUMENT_UPDATES/500 = 3. W tym przykładzie używamy wartości fragmentów x, y i z. Jako wartości fragmentów możesz też używać liczb lub innych znaków.
Dodawanie pola fragmentu
Dodaj do dokumentów pole shard. Ustaw pole shard na wartości x, y lub z, co zwiększa limit operacji zapisu w kolekcji do 1500 operacji na sekundę.
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(); }
Wysyłanie zapytań do podzielonej na fragmenty sygnatury czasowej
Dodanie pola shard wymaga zaktualizowania zapytań, aby agregować podzielone na fragmenty wyniki:
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]); });
Aktualizowanie definicji indeksów
Aby usunąć ograniczenie 500 operacji zapisu na sekundę, usuń istniejące indeksy pojedynczych pól i indeksy złożone, które używają pola timestamp.
Usuwanie definicji indeksów złożonych
Konsola Firebase
W konsoli Firebase otwórz kartę Bazy danych i pamięć masowa > Firestore > Indeksy złożone.
W przypadku każdego indeksu, który zawiera pole
timestamp, kliknij przycisk i kliknij Usuń.
konsola GCP
W konsoli Google Cloud otwórz stronę Bazy danych.
Na liście baz danych wybierz wymaganą bazę danych.
W menu nawigacyjnym kliknij Indeksy, a następnie kartę Złożone.
Użyj pola Filtr , aby wyszukać definicje indeksów, które zawierają pole
timestamp.W przypadku każdego z tych indeksów kliknij przycisk , a następnie Usuń.
wiersz poleceń Firebase
- Jeśli nie masz skonfigurowanego wiersza poleceń Firebase, zainstaluj go
i uruchom polecenie
firebase initzgodnie z tymi instrukcjami. Podczas wykonywania poleceniainitwybierzFirestore: Deploy rules and create indexes for Firestore. - Podczas konfiguracji wiersz poleceń Firebase pobiera istniejące definicje indeksów do pliku o nazwie domyślnej
firestore.indexes.json. Usuń wszystkie definicje indeksów, które zawierają pole
timestamp, na przykład:{ "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" } ] }, ] }Wdróż zaktualizowane definicje indeksów:
firebase deploy --only firestore:indexes
Aktualizowanie definicji indeksów pojedynczych pól
Konsola Firebase
W konsoli Firebase otwórz kartę Bazy danych i pamięć masowa > Firestore > Indeksy pojedynczych pól.
Kliknij Dodaj wykluczenie.
W polu Identyfikator kolekcji wpisz
instruments. W polu Ścieżka pola, wpisztimestamp.W sekcji Zakres zapytania wybierz Kolekcja i Grupa kolekcji.
Kliknij Dalej.
Przełącz wszystkie ustawienia indeksu na Wyłączone. Kliknij Zapisz.
Powtórz te same czynności w przypadku pola
shard.
konsola GCP
W konsoli Google Cloud otwórz stronę Bazy danych.
Na liście baz danych wybierz wymaganą bazę danych.
W menu nawigacyjnym kliknij Indeksy, a następnie kartę Pojedyncze pole.
Kliknij kartę Pojedyncze pole.
Kliknij Dodaj wykluczenie.
W polu Identyfikator kolekcji wpisz
instruments. W polu Ścieżka pola, wpisztimestamp.W sekcji Zakres zapytania wybierz Kolekcja i Grupa kolekcji.
Kliknij Dalej.
Przełącz wszystkie ustawienia indeksu na Wyłączone. Kliknij Zapisz.
Powtórz te same czynności w przypadku pola
shard.
wiersz poleceń Firebase
Dodaj te informacje do sekcji
fieldOverridesw pliku definicji indeksów:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }Wdróż zaktualizowane definicje indeksów:
firebase deploy --only firestore:indexes
Tworzenie nowych indeksów złożonych
Po usunięciu wszystkich poprzednich indeksów zawierających timestamp zdefiniuj nowe indeksy wymagane przez aplikację. Każdy indeks zawierający pole timestamp musi też zawierać pole shard. Aby na przykład obsługiwać powyższe zapytania, dodaj te indeksy:
| Kolekcja | Zindeksowane pola | Zakres zapytania |
|---|---|---|
| instruments | shard, price.currency, timestamp | Kolekcja |
| instruments | shard, exchange, timestamp | Kolekcja |
| instruments | shard, instrumentType, timestamp | Kolekcja |
Komunikaty o błędach
Te indeksy możesz utworzyć, uruchamiając zaktualizowane zapytania.
Każde zapytanie zwraca komunikat o błędzie z linkiem do utworzenia wymaganego indeksu w konsoli Firebase.
wiersz poleceń Firebase
Dodaj te indeksy do pliku definicji indeksów:
{ "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" } ] }, ] }Wdróż zaktualizowane definicje indeksów:
firebase deploy --only firestore:indexes
Informacje o limicie operacji zapisu w przypadku sekwencyjnych pól indeksowanych
Limit operacji zapisu w przypadku sekwencyjnych pól indeksowanych wynika ze sposobu, w jaki Cloud Firestore przechowuje wartości indeksów i skaluje operacje zapisu indeksów. W przypadku każdej operacji zapisu indeksu Cloud Firestore definiuje wpis klucz-wartość, który łączy nazwę dokumentu i wartość każdego pola indeksowanego. Cloud Firestore porządkuje te wpisy indeksu w grupy danych nazywane tabletami. Każdy Cloud Firestore serwer zawiera co najmniej 1 tablet. Gdy obciążenie zapisu na danym tablecie staje się zbyt duże, Cloud Firestore skaluje się poziomo dzieląc tablet na mniejsze tablety i rozpraszając nowe tablety na różnych serwerach Cloud Firestore.
Cloud Firestore umieszcza leksykograficznie bliskie wpisy indeksu na tym samym tablecie. Jeśli wartości indeksu w tablecie są zbyt blisko siebie, np. w przypadku pól sygnatury czasowej, Cloud Firestore nie może skutecznie podzielić tabletu na mniejsze tablety. Powoduje to powstanie gorącego punktu, w którym jeden tablet otrzymuje zbyt duży ruch, a operacje odczytu i zapisu w tym punkcie stają się wolniejsze.
Dzieląc pole sygnatury czasowej na fragmenty, umożliwiasz Cloud Firestore efektywne rozdzielanie obciążeń na wiele tabletów. Chociaż wartości pola sygnatury czasowej mogą pozostać blisko siebie, połączona wartość fragmentu i indeksu daje Cloud Firestore wystarczająco dużo miejsca między wpisami indeksu, aby podzielić wpisy na wiele tabletów.
Co dalej?
- Przeczytaj sprawdzone metody projektowania pod kątem skalowania.
- W przypadku dużej liczby operacji zapisu w jednym dokumencie zapoznaj się z informacjami o licznikach rozproszonych.
- Zapoznaj się ze standardowymi limitami Cloud Firestore.