Jeśli kolekcja zawiera dokumenty z sekwencyjnymi wartościami indeksowanymi,Cloud Firestore ogranicza szybkość zapisu do 500 zapisów na sekundę. Na tej stronie dowiesz się, jak podzielić pole dokumentu na fragmenty, aby obejść ten limit. Najpierw wyjaśnijmy, co rozumiemy przez „kolejne pola indeksowane”, i określmy, kiedy obowiązuje ten limit.
Pola indeksowane sekwencyjnie
„Sekwencyjne pola indeksowane” oznaczają dowolną kolekcję dokumentów, która zawiera pole indeksowane o wartościach rosnących lub malejących w sposób monotoniczny. W wielu przypadkach oznacza to pole timestamp
, ale dowolna wartość pola, która rośnie lub maleje monotonicznie, może spowodować osiągnięcie limitu zapisu wynoszącego 500 operacji zapisu na sekundę.
Na przykład limit dotyczy kolekcji user
dokumentów z indeksowanym polem 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
zawiera wartości o rozkładzie losowym, limit zapisu nie obowiązuje. Rzeczywista wartość pola również 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 wywołują limit zapisu:
100000, 100001, 100002, 100003, ...
0, 1, 2, 3, ...
Dzielenie pola sygnatury czasowej
Załóżmy, że aplikacja używa pola timestamp
, które rośnie monotonicznie.
Jeśli aplikacja nie używa pola timestamp
w żadnych zapytaniach, możesz usunąć limit 500 zapisów na sekundę, nie indeksując pola sygnatury czasowej. Jeśli w zapytaniach potrzebujesz pola timestamp
, możesz obejść ten limit, używając podzielonych sygnatur czasowych:
- Dodaj pole
shard
obok polatimestamp
. Użyj1..n
różnych wartości w polushard
. Zwiększa to limit zapisu w kolekcji do500*n
, ale musisz agregowaćn
zapytań. - Zaktualizuj logikę zapisu, aby losowo przypisywać wartość
shard
do każdego dokumentu. - Zaktualizuj zapytania, aby agregować podzielone zbiory 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
shard
musi występować przed polemtimestamp
. Wszystkie indeksy, które zawierają poletimestamp
, muszą też zawierać poleshard
.
Znaczniki czasu z podziałem na fragmenty należy wdrażać tylko w przypadkach użycia, w których utrzymuje się szybkość zapisu powyżej 500 zapisów na sekundę. W przeciwnym razie będzie to przedwczesna optymalizacja. Podział pola timestamp
usuwa ograniczenie 500 zapisów 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 dotyczące podzielonego na fragmenty zbioru wyników.
Przykładowy model danych i zapytania
Wyobraź sobie na przykład aplikację do analizy w czasie rzeczywistym instrumentów finansowych, takich jak waluty, akcje i fundusze ETF. 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 wyniki 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 fragmentu:MAX_INSTRUMENT_UPDATES/500 = 3
W tym przykładzie użyto wartości fragmentu x
, y
i z
. Możesz też używać liczb lub innych znaków jako wartości fragmentu.
Dodawanie pola fragmentu
Dodaj do dokumentów pole shard
. Ustaw pole shard
na wartości x
, y
lub z
, co zwiększy limit zapisu w kolekcji do 1500 zapisów 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(); }
Wykonywanie zapytań dotyczących sygnatury czasowej z podziałem na fragmenty
Dodanie pola shard
wymaga zaktualizowania zapytań w celu agregowania wyników podzielonych na fragmenty:
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 zapisów na sekundę, usuń istniejące indeksy jednopola i złożone, które używają pola timestamp
.
Usuwanie definicji indeksów złożonych
Konsola Firebase
Otwórz stronę Cloud Firestore Indeksy złożone w konsoli Firebase.
W przypadku każdego indeksu zawierającego pole
timestamp
kliknij przycisk i wybierz Usuń.
konsola GCP
W konsoli Google Cloud otwórz stronę Bazy danych.
Na liście baz danych wybierz odpowiednią bazę danych.
W menu nawigacyjnym kliknij Indeksy, a następnie wybierz kartę Złożone.
W polu Filtr wyszukaj definicje indeksu, które zawierają pole
timestamp
.Przy każdym z tych indeksów kliknij przycisk
, a następnie Usuń.
wiersz poleceń Firebase
- Jeśli nie masz skonfigurowanego wiersza poleceń Firebase, postępuj zgodnie z tymi instrukcjami, aby zainstalować wiersz poleceń i uruchomić polecenie
firebase init
. Podczas wykonywania poleceniainit
wybierzFirestore: 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 indeksu:
firebase deploy --only firestore:indexes
Aktualizowanie definicji indeksu pojedynczego pola
Konsola Firebase
Otwórz stronę Cloud Firestore Indeksy pojedynczych pól w konsoli Firebase.
Kliknij Dodaj wyłączenie.
W polu Identyfikator kolekcji wpisz
instruments
. W polu Ścieżka do pola wpisztimestamp
.W sekcji Zakres zapytania wybierz zarówno Kolekcja, jak 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 odpowiednią bazę danych.
W menu nawigacyjnym kliknij Indeksy, a następnie kliknij kartę Pojedyncze pole.
Kliknij kartę Single Field (Pojedyncze pole).
Kliknij Dodaj wyłączenie.
W polu Identyfikator kolekcji wpisz
instruments
. W polu Ścieżka do pola wpisztimestamp
.W sekcji Zakres zapytania wybierz zarówno Kolekcja, jak 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 do sekcji
fieldOverrides
w pliku definicji indeksu te informacje:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }
Wdróż zaktualizowane definicje indeksu:
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 |
---|---|---|
instrumenty | shard, price.currency, timestamp | Kolekcja |
instrumenty | fragment, wymiana, sygnatura czasowa | Kolekcja |
instrumenty | shard, instrumentType, timestamp | Kolekcja |
Komunikaty o błędach
Możesz utworzyć te indeksy, 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 do pliku definicji indeksu te indeksy:
{ "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 indeksu:
firebase deploy --only firestore:indexes
Informacje o limicie zapisu w przypadku sekwencyjnych pól indeksowanych
Ograniczenie szybkości zapisu w przypadku sekwencyjnych pól indeksowanych wynika ze sposobu, w jaki usługa Cloud Firestore przechowuje wartości indeksu i skaluje zapisy indeksu. W przypadku każdego zapisu indeksu Cloud Firestore definiuje wpis klucz-wartość, który łączy nazwę dokumentu i wartość każdego indeksowanego pola. Cloud Firestore organizuje te wpisy indeksu w grupy danych zwane tabletami. Każdy serwerCloud Firestore zawiera co najmniej 1 tablet. Gdy obciążenie zapisem na konkretnym tablecie staje się zbyt duże, Cloud Firestore skaluje się w poziomieCloud Firestore, dzieląc tablet na mniejsze tablety i rozmieszczając je na różnych Cloud Firestore serwerach.
Cloud Firestore umieszcza w tym samym tablecie wpisy indeksu, które są bliskie sobie pod względem leksykograficznym. Jeśli wartości indeksu w tabeli są zbyt blisko siebie, np. w przypadku pól sygnatury czasowej, Cloud Firestore nie może skutecznie podzielić tabeli na mniejsze tabele. Powoduje to powstanie punktu krytycznego, 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, umożliwiasz Cloud Firestore efektywne dzielenie obciążeń na wiele tabletów. Chociaż wartości pola sygnatury czasowej mogą być zbliżone do siebie, połączona wartość fragmentu i indeksu zapewnia Cloud Firestore wystarczającą przestrzeń między wpisami indeksu, aby podzielić wpisy na wiele tabletów.
Co dalej?
- Przeczytaj sprawdzone metody projektowania z myślą o skalowaniu
- W przypadku dużych częstotliwości zapisu w jednym dokumencie zapoznaj się z sekcją Liczniki rozproszone.
- Zobacz limity standardowe dla Cloud Firestore