Timestamp frammentati

Se una raccolta contiene documenti con valori indicizzati sequenziali, Cloud Firestore limita la velocità di scrittura a 500 scritture al secondo. Questa pagina descrive come partizionare un campo del documento per superare questo limite. Innanzitutto definiamo cosa intendiamo per “campi indicizzati sequenziali” e chiariamo quando si applica questo limite.

Campi indicizzati sequenziali

Per "campi indicizzati sequenziali" si intende qualsiasi raccolta di documenti che contiene un campo indicizzato monotonicamente crescente o decrescente. In molti casi, ciò significa un campo timestamp , ma qualsiasi valore di campo monotonicamente crescente o decrescente può attivare il limite di scrittura di 500 scritture al secondo.

Ad esempio, il limite si applica a una raccolta di documenti user con campo indicizzato userid se l'app assegna valori userid in questo modo:

  • 1281, 1282, 1283, 1284, 1285, ...

D'altra parte, non tutti i campi timestamp attivano questo limite. Se un campo timestamp tiene traccia di valori distribuiti casualmente, il limite di scrittura non si applica. Anche il valore effettivo del campo non ha importanza, ma solo che il campo aumenta o diminuisce in modo monotono. Ad esempio, entrambi i seguenti insiemi di valori di campo con aumento monotono attivano il limite di scrittura:

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

Sharding di un campo timestamp

Supponiamo che la tua app utilizzi un campo timestamp ad aumento monotono. Se la tua app non utilizza il campo timestamp in nessuna query, puoi rimuovere il limite di 500 scritture al secondo non indicizzando il campo timestamp. Se hai bisogno di un campo timestamp per le tue query, puoi aggirare il limite utilizzando timestamp frammentati :

  1. Aggiungi un campo shard accanto al campo timestamp . Utilizza 1..n valori distinti per il campo shard . Ciò aumenta il limite di scrittura per la raccolta a 500*n , ma è necessario aggregare n query.
  2. Aggiorna la tua logica di scrittura per assegnare in modo casuale un valore shard a ciascun documento.
  3. Aggiorna le tue query per aggregare i set di risultati partizionati.
  4. Disabilita gli indici a campo singolo sia per il campo shard che per il campo timestamp . Elimina gli indici compositi esistenti che contengono il campo timestamp .
  5. Crea nuovi indici compositi per supportare le tue query aggiornate. L'ordine dei campi in un indice è importante e il campo shard deve precedere il campo timestamp . Tutti gli indici che includono il campo timestamp devono includere anche il campo shard .

Dovresti implementare timestamp partizionati solo nei casi d'uso con velocità di scrittura sostenute superiori a 500 scritture al secondo. Altrimenti si tratta di un'ottimizzazione prematura. Lo partizionamento di un campo timestamp rimuove la restrizione di 500 scritture al secondo, ma con il compromesso di richiedere aggregazioni di query lato client.

Gli esempi seguenti mostrano come partizionare un campo timestamp e come eseguire una query su un set di risultati partizionato.

Esempio di modello dati e query

Ad esempio, immagina un'app per l'analisi quasi in tempo reale di strumenti finanziari 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 seguenti query e ordini 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 alcune ricerche, stabilisci che l'app riceverà tra 1.000 e 1.500 aggiornamenti dello strumento al secondo. Ciò supera le 500 scritture al secondo consentite per le raccolte contenenti documenti con campi timestamp indicizzati. Per aumentare la velocità effettiva di scrittura, sono necessari 3 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 i valori shard.

Aggiunta di un campo shard

Aggiungi un campo shard ai tuoi documenti. Imposta il campo shard sui valori x , y o z che aumentano il limite di scrittura sulla raccolta a 1.500 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();
}

Interrogazione del timestamp condiviso

L'aggiunta di un campo shard richiede l'aggiornamento delle query per aggregare i risultati partizionati:

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 le definizioni degli indici

Per rimuovere il vincolo di 500 scritture al secondo, eliminare gli indici compositi e a campo singolo esistenti che utilizzano il campo timestamp .

Elimina le definizioni dell'indice composito

Console Firebase

  1. Apri la pagina Indici compositi Cloud Firestore nella console Firebase.

    Vai a Indici compositi

  2. Per ogni indice che contiene il campo timestamp , fai clic sul pulsante e poi su Elimina .

Console GCP

  1. Nella console di Google Cloud Platform, vai alla pagina Database .

    Vai a Database

  2. Selezionare il database richiesto dall'elenco dei database.

  3. Nel menu di navigazione, fare clic su Indici e quindi sulla scheda Compositi .

  4. Utilizzare il campo Filtro per cercare le definizioni di indice che contengono il campo timestamp .

  5. Per ciascuno di questi indici, fare clic sul pulsante e quindi su Elimina .

CLI Firebase

  1. Se non hai configurato la CLI Firebase, segui queste istruzioni per installare la CLI ed esegui il comando firebase init . Durante il comando init , assicurati di selezionare Firestore: Deploy rules and create indexes for Firestore .
  2. Durante la configurazione, la CLI di Firebase scarica le definizioni degli indici esistenti in un file denominato, per impostazione predefinita, firestore.indexes.json .
  3. Rimuovere eventuali definizioni di indice che contengono il campo timestamp , ad 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"
          }
        ]
      },
     ]
    }
    
  4. Distribuisci le definizioni di indice aggiornate:

    firebase deploy --only firestore:indexes
    

Aggiorna le definizioni dell'indice a campo singolo

Console Firebase

  1. Apri la pagina Indici di campo singolo Cloud Firestore nella console Firebase.

    Vai a Indici di campo singolo

  2. Fare clic su Aggiungi esenzione .

  3. Per ID raccolta , inserisci instruments . Per Percorso campo , inserire timestamp .

  4. In Ambito della query selezionare sia Raccolta che Gruppo di raccolte .

  5. Fare clic su Avanti

  6. Imposta tutte le impostazioni dell'indice su Disabilitato . Fare clic su Salva .

  7. Ripeti gli stessi passaggi per il campo shard .

Console GCP

  1. Nella console di Google Cloud Platform, vai alla pagina Database .

    Vai a Database

  2. Selezionare il database richiesto dall'elenco dei database.

  3. Nel menu di navigazione, fare clic su Indici e quindi sulla scheda Campo singolo .

  4. Fare clic sulla scheda Campo singolo .

  5. Fare clic su Aggiungi esenzione .

  6. Per ID raccolta , inserisci instruments . Per Percorso campo , inserire timestamp .

  7. In Ambito della query selezionare sia Raccolta che Gruppo di raccolte .

  8. Fare clic su Avanti

  9. Imposta tutte le impostazioni dell'indice su Disabilitato . Fare clic su Salva .

  10. Ripeti gli stessi passaggi per il campo shard .

CLI Firebase

  1. Aggiungi quanto segue alla sezione fieldOverrides del file delle definizioni dell'indice:

    {
     "fieldOverrides": [
       // Disable single-field indexing for the timestamp field
       {
         "collectionGroup": "instruments",
         "fieldPath": "timestamp",
         "indexes": []
       },
     ]
    }
    
  2. Distribuisci le definizioni di indice aggiornate:

    firebase deploy --only firestore:indexes
    

Creare nuovi indici compositi

Dopo aver rimosso tutti gli indici precedenti contenenti il timestamp , definisci i nuovi indici richiesti dalla tua app. Qualsiasi indice contenente il campo timestamp deve contenere anche il campo shard . Ad esempio, per supportare le query precedenti, aggiungi i seguenti indici:

Collezione Campi indicizzati Ambito della query
strumenti frammento , prezzo.valuta , timestamp Collezione
strumenti frammento , scambio , timestamp Collezione
strumenti frammento , tipo strumento , timestamp Collezione

Messaggio di errore

È possibile creare questi indici eseguendo le query aggiornate.

Ogni query restituisce un messaggio di errore con un collegamento per creare l'indice richiesto nella console Firebase.

CLI Firebase

  1. 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"
             }
           ]
         },
       ]
     }
    
  2. Distribuisci le definizioni di indice aggiornate:

    firebase deploy --only firestore:indexes
    

Comprensione della scrittura per limitare i campi indicizzati sequenziali

Il limite alla velocità di scrittura per i campi indicizzati sequenziali deriva dal modo in cui Cloud Firestore archivia i valori dell'indice e ridimensiona le scritture degli indici. Per ogni scrittura dell'indice, Cloud Firestore definisce una voce valore-chiave che concatena il nome del documento e il valore di ciascun campo indicizzato. Cloud Firestore organizza queste voci di indice in gruppi di dati chiamati tablet . Ciascun server Cloud Firestore contiene uno o più tablet. Quando il carico di scrittura su un particolare tablet diventa troppo elevato, Cloud Firestore si ridimensiona orizzontalmente suddividendo il tablet in tablet più piccoli e distribuendo i nuovi tablet su diversi server Cloud Firestore.

Cloud Firestore inserisce voci di indice lessicograficamente vicine sullo stesso tablet. Se i valori dell'indice in un tablet sono troppo vicini tra loro, ad esempio per i campi timestamp, Cloud Firestore non può suddividere in modo efficiente il tablet in tablet più piccoli. Ciò crea un hot spot in cui un singolo tablet riceve troppo traffico e le operazioni di lettura e scrittura nell'hot spot diventano più lente.

Suddividendo un campo timestamp, consenti a Cloud Firestore di suddividere in modo efficiente i carichi di lavoro su più tablet. Anche se i valori del campo timestamp potrebbero rimanere vicini, lo shard concatenato e il valore dell'indice forniscono a Cloud Firestore spazio sufficiente tra le voci dell'indice per suddividere le voci tra più tablet.

Qual è il prossimo