Fragmenty sygnatur czasowych

Jeśli kolekcja zawiera dokumenty z sekwencyjnymi wartościami indeksowanymi, Cloud Firestore ogranicza szybkość zapisu do 500 zapisów na sekundę. Na tej stronie opisano, jak podzielić pole dokumentu na fragmenty, aby obejść ten limit. Najpierw zdefiniujmy, co rozumiemy przez „sekwencyjne pola indeksowane”, i wyjaśnijmy, kiedy obowiązuje ten limit.

Sekwencyjne pola indeksowane

„Sekwencyjne pola indeksowane” oznaczają dowolną kolekcję dokumentów, która zawiera monotonicznie rosnące lub malejące pole indeksowane. W wielu przypadkach oznacza to pole timestamp, ale każda monotonicznie rosnąca lub malejąca wartość pola może spowodować przekroczenie limitu 500 zapisów na sekundę.

Limit dotyczy na przykład 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 śledzi losowo rozłożone wartości, limit zapisu nie obowiązuje. Rzeczywista wartość pola również nie ma znaczenia, liczy się tylko to, czy pole rośnie lub maleje monotonicznie. Na przykład oba te zestawy monotonicznie rosnących wartości pól powodują przekroczenie limitu zapisu:

  • 100000, 100001, 100002, 100003, ...
  • 0, 1, 2, 3, ...

Dzielenie pola sygnatury czasowej na fragmenty

Załóżmy, że Twoja aplikacja używa monotonicznie rosnącego pola timestamp. Jeśli Twoja 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 sygnatur czasowych z podziałem na fragmenty:

  1. Dodaj pole shard obok pola timestamp. Użyj 1..n różnych wartości w polu shard. Zwiększy to limit zapisu dla kolekcji do 500*n, ale musisz zagregować zapytania n.
  2. Zaktualizuj logikę zapisu, aby losowo przypisywać wartość shard do każdego dokumentu.
  3. Zmień swoje zapytania, aby agregować podzielone na fragmenty zbiory wyników.
  4. Wyłącz indeksy pojedynczego pola dla pól shard i timestamp. Usuń istniejące indeksy złożone, które zawierają pole timestamp.
  5. Utwórz nowe indeksy złożone, aby obsługiwać zaktualizowane zapytania. Kolejność pól w indeksie ma znaczenie, a pole shard musi znajdować się przed polem timestamp. Wszystkie indeksy zawierające pole timestamp muszą też zawierać pole shard.

Sygnatury czasowe z podziałem na fragmenty należy wdrażać tylko w przypadkach, gdy liczba zapisów na sekundę stale przekracza 500. W przeciwnym razie będzie to przedwczesna optymalizacja. Fragmentowanie 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 wysłać zapytanie do podzielonego na fragmenty zbioru wyników.

Przykładowy model danych i zapytania

Wyobraź sobie na przykład aplikację do analizy instrumentów finansowych, takich jak waluty, akcje zwykłe i ETF-y, 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 porządkuje 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 analizy ustalasz, że aplikacja będzie otrzymywać od 1000 do 1500 aktualizacji instrumentów na sekundę. Przekracza to limit 500 zapisów na sekundę dozwolonych 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 fragmentów x, y i z. Możesz też używać liczb lub innych znaków jako wartości fragmentów.

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 zapytania dotyczącego sygnatury czasowej we fragmentach

Dodanie pola shard wymaga zaktualizowania zapytań, aby agregować wyniki podzielone 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 do 500 zapisów na sekundę, usuń istniejące indeksy jednopolowe i złożone, które używają pola timestamp.

Usuwanie definicji indeksów złożonych

Konsola Firebase

  1. W konsoli Firebase otwórz kartę Databases & Storage (Bazy danych i miejsce na dane) > Firestore > Composite Indexes (Indeksy wieloskładnikowe).

    Otwórz Indeksy złożone

  2. W przypadku każdego indeksu zawierającego pole timestamp kliknij przycisk , a następnie Usuń.

konsola GCP

  1. W konsoli Google Cloud otwórz stronę Bazy danych.

    Otwórz Bazy danych

  2. Wybierz wymaganą bazę danych z listy baz danych.

  3. W menu nawigacyjnym kliknij Indeksy, a następnie kliknij kartę Złożone.

  4. Użyj pola Filtr, aby wyszukać definicje indeksów zawierające pole timestamp.

  5. Przy każdym z tych indeksów kliknij przycisk , a następnie Usuń.

wiersz poleceń Firebase

  1. Jeśli nie masz skonfigurowanego interfejsu wiersza poleceń Firebase, wykonaj te instrukcje, aby go zainstalować i uruchomić polecenie firebase init. Podczas wykonywania polecenia init wybierz Firestore: Deploy rules and create indexes for Firestore.
  2. Podczas konfiguracji wiersz poleceń Firebase pobiera istniejące definicje indeksów do pliku o domyślnej nazwie firestore.indexes.json.
  3. Usuń wszystkie definicje indeksów zawierające 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"
          }
        ]
      },
     ]
    }
    
  4. Wdróż zaktualizowane definicje indeksów:

    firebase deploy --only firestore:indexes
    

Aktualizowanie definicji indeksów pojedynczego pola

Konsola Firebase

  1. W konsoli Firebase otwórz kartę Bazy danych i miejsce na dane > Firestore > Indeksy jednopolowe.

    Otwórz stronę Indeksy jednopolowe

  2. Kliknij Dodaj zwolnienie.

  3. W polu Identyfikator kolekcji wpisz instruments. W polu Ścieżka pola wpisz timestamp.

  4. W sekcji Zakres zapytania wybierz Kolekcja i Grupa kolekcji.

  5. Kliknij Dalej.

  6. Ustaw wszystkie ustawienia indeksu na Wyłączone. Kliknij Zapisz.

  7. Powtórz te same czynności w przypadku pola shard.

konsola GCP

  1. W konsoli Google Cloud otwórz stronę Bazy danych.

    Otwórz Bazy danych

  2. Wybierz wymaganą bazę danych z listy baz danych.

  3. W menu nawigacyjnym kliknij Indeksy, a następnie kartę Pojedyncze pole.

  4. Kliknij kartę Pojedyncze pole.

  5. Kliknij Dodaj zwolnienie.

  6. W polu Identyfikator kolekcji wpisz instruments. W polu Ścieżka pola wpisz timestamp.

  7. W sekcji Zakres zapytania wybierz Kolekcja i Grupa kolekcji.

  8. Kliknij Dalej.

  9. Ustaw wszystkie ustawienia indeksu na Wyłączone. Kliknij Zapisz.

  10. Powtórz te same czynności w przypadku pola shard.

wiersz poleceń Firebase

  1. Dodaj do sekcji fieldOverrides pliku definicji indeksu te informacje:

    {
     "fieldOverrides": [
       // Disable single-field indexing for the timestamp field
       {
         "collectionGroup": "instruments",
         "fieldPath": "timestamp",
         "indexes": []
       },
     ]
    }
    
  2. 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, których wymaga aplikacja. 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  fragment,  price.currency,  sygnatura czasowa Kolekcja
instrumenty  fragment,  wymiana wiadomości,  sygnatura czasowa Kolekcja
instrumenty  fragment, instrumentType,  sygnatura czasowa 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

  1. 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"
             }
           ]
         },
       ]
     }
    
  2. Wdróż zaktualizowane definicje indeksów:

    firebase deploy --only firestore:indexes
    

Omówienie limitu zapisu dla sekwencyjnych pól indeksowanych

Limit szybkości zapisu w przypadku pól indeksowanych sekwencyjnie wynika ze sposobu, w jaki 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 porządkuje te wpisy indeksu w grupy danych nazywane tabletami. Każdy serwer Cloud Firestore zawiera co najmniej 1 tablet. Gdy obciążenie zapisem na określonym tablecie staje się zbyt duże, Cloud Firestore skaluje się poziomo, dzieląc tablet na mniejsze tablety i rozprzestrzeniając nowe tablety na różne serwery Cloud Firestore.

Cloud Firestore umieszcza wpisy indeksu, które są blisko siebie pod względem leksykograficznym, 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. Powstaje wtedy punkt krytyczny, w którym pojedyncza tabela otrzymuje zbyt duży ruch, a operacje odczytu i zapisu w tym punkcie stają się wolniejsze.

Dzięki fragmentowaniu pola sygnatury czasowej Cloud Firestore może efektywnie rozdzielać zadania między wiele tabletów. Chociaż wartości pola sygnatury czasowej mogą być zbliżone, połączone wartości fragmentu i indeksu dają usłudze Cloud Firestore wystarczająco dużo miejsca między wpisami indeksu, aby podzielić je między wiele tabletów.

Co dalej?