חותמות זמן מפוצלות

אם אוסף מכיל מסמכים עם ערכי אינדקס עוקבים, Cloud Firestore מגביל את קצב הכתיבה ל-500 כתיבה בשנייה. דף זה מתאר כיצד לגזור שדה מסמך כדי להתגבר על מגבלה זו. ראשית, הבה נגדיר למה אנו מתכוונים ב"שדות שנוספו לאינדקס ברצף" ונבהיר מתי מגבלה זו חלה.

שדות מסודרים באינדקס

"שדות שנוספו לאינדקס ברצף" פירושו כל אוסף של מסמכים המכיל שדה מונוטוני שהוגדל או יורד באינדקס. במקרים רבים, משמעות הדבר היא שדה timestamp , אך כל ערך שדה מונוטוני הגובר או ירידה יכול להפעיל את מגבלת הכתיבה של 500 כתיבה בשנייה.

לדוגמה, המגבלה חלה על אוסף של מסמכי user עם userid בשדה באינדקס אם האפליקציה מקצה ערכי userid כך:

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

מצד שני, לא כל שדות timestamp מפעילים מגבלה זו. אם שדה חותמת timestamp אחר ערכים המופצים באקראי, מגבלת הכתיבה לא חלה. גם הערך האמיתי של השדה לא משנה, רק שהשדה עולה או יורד באופן מונוטוני. לדוגמה, שתי הקבוצות הבאות של ערכי שדות בעלי גידול מונוטוני מפעילות את מגבלת הכתיבה:

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

חלוקת שדה חותמת זמן

נניח שהאפליקציה שלך משתמשת בשדה timestamp הגדל באופן מונוטוני. אם האפליקציה שלך לא משתמשת בשדה timestamp בשאילתות כלשהן, תוכל להסיר את מגבלת 500 הכתיבה לשנייה על ידי אי הוספה לאינדקס של שדה חותמת הזמן. אם אתה אכן דורש שדה timestamp עבור השאילתות שלך, תוכל לעקוף את המגבלה על ידי שימוש בחותמות זמן מרוסקות :

  1. הוסף שדה shard לצד שדה timestamp . השתמש 1..n ערכים ברורים עבור שדה shard . זה מעלה את מגבלת הכתיבה של האוסף ל 500*n , אך עליך לצבור n שאילתות.
  2. עדכן את היגיון הכתיבה שלך כדי להקצות באופן אקראי ערך shard לכל מסמך.
  3. עדכן את השאילתות שלך כדי לצבור את קבוצות התוצאות המרוסקות.
  4. השבת אינדקסים של שדה בודד הן עבור שדה shard והן עבור שדה timestamp . מחק אינדקסים מורכבים קיימים המכילים את שדה חותמת timestamp .
  5. צור אינדקסים מורכבים חדשים כדי לתמוך בשאילתות המעודכנות שלך. יש חשיבות לסדר השדות באינדקס, ושדה shard חייב לבוא לפני שדה timestamp . כל האינדקסים הכוללים את שדה timestamp חייבים לכלול גם את שדה shard .

עליך ליישם חותמות זמן מרוסקות רק במקרים של שימוש עם קצבי כתיבה מתמשכים מעל 500 כתיבה בשנייה. אחרת, מדובר באופטימיזציה מוקדמת. פיצול שדה timestamp מסיר את ההגבלה של 500 כתיבה לשנייה, אך עם הפשרה של צורך בצבירה של שאילתות בצד הלקוח.

הדוגמאות הבאות מראות כיצד לגזור שדה timestamp וכיצד לבצע שאילתה על ערכת תוצאות מפוצלת.

מודל נתונים ושאילתות לדוגמה

כדוגמה, דמיינו אפליקציה לניתוח כמעט בזמן אמת של מכשירים פיננסיים כמו מטבעות, מניות רגילות ותעודות סל. אפליקציה זו כותבת מסמכים לאוסף 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();
}

אפליקציה זו מריץ את השאילתות וההזמנות הבאות לפי שדה 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]);
    });

לאחר מחקר מסוים, אתה קובע שהאפליקציה תקבל בין 1,000 ל-1,500 עדכוני מכשירים בשנייה. זה עולה על 500 הכתיבה לשנייה המותרים לאוספים המכילים מסמכים עם שדות חותמת זמן באינדקס. כדי להגדיל את תפוקת הכתיבה, אתה צריך 3 ערכי רסיס, MAX_INSTRUMENT_UPDATES/500 = 3 . דוגמה זו משתמשת בערכי הרסיס x , y ו- z . אתה יכול גם להשתמש במספרים או בתווים אחרים עבור ערכי הרסיס שלך.

הוספת שדה רסיס

הוסף שדה shard למסמכים שלך. הגדר את שדה shard לערכים x , y או z מה שמעלה את מגבלת הכתיבה באוסף ל-1,500 כתיבה בשנייה.

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();
}

חיפוש חותמת הזמן המרוסקת

הוספת שדה shard מחייבת לעדכן את השאילתות שלך כדי לצבור תוצאות פצצות:

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]);
    });

עדכן את הגדרות האינדקס

כדי להסיר את האילוץ של 500 כתיבה לשנייה, מחק את האינדקסים הקיימים של שדה יחיד ואינדקסים מורכבים המשתמשים בשדה timestamp .

מחק הגדרות אינדקס מורכב

מסוף Firebase

  1. פתח את הדף של Cloud Firestore Composite Indexs במסוף Firebase.

    עבור אל אינדקסים מרוכבים

  2. עבור כל אינדקס המכיל את שדה timestamp , לחץ על הלחצן ולחץ על מחק .

מסוף GCP

  1. ב-Google Cloud Platform Console, עבור לדף מסדי נתונים .

    עבור אל מסדי נתונים

  2. בחר את מסד הנתונים הנדרש מרשימת מסדי הנתונים.

  3. בתפריט הניווט, לחץ על אינדקסים ולאחר מכן לחץ על הכרטיסייה Composite .

  4. השתמש בשדה מסנן כדי לחפש הגדרות אינדקס המכילות את שדה timestamp .

  5. עבור כל אחד מהאינדקסים הללו, לחץ על הלחצן ולחץ על מחק .

Firebase CLI

  1. אם לא הגדרת את Firebase CLI, עקוב אחר ההנחיות הבאות כדי להתקין את ה-CLI ולהפעיל את פקודת firebase init . במהלך הפקודה init , הקפד לבחור Firestore: Deploy rules and create indexes for Firestore .
  2. במהלך ההגדרה, Firebase CLI מוריד את הגדרות האינדקס הקיימות שלך לקובץ בשם, כברירת מחדל, firestore.indexes.json .
  3. הסר כל הגדרות אינדקס המכילות את שדה חותמת timestamp , לדוגמה:

    {
    "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. פרוס את הגדרות האינדקס המעודכנות שלך:

    firebase deploy --only firestore:indexes
    

עדכון הגדרות אינדקס של שדה בודד

מסוף Firebase

  1. פתח את הדף של Cloud Firestore Single Field Indexs במסוף Firebase.

    עבור אל אינדקסים של שדה בודד

  2. לחץ על הוסף פטור .

  3. עבור מזהה אוסף , הזן instruments . עבור נתיב שדה , הזן timestamp .

  4. תחת היקף שאילתה , בחר גם אוסף וגם קבוצת אוסף .

  5. הקש "הבא

  6. החלף את כל הגדרות האינדקס למצב מושבת . לחץ על שמור .

  7. חזור על אותם שלבים עבור שדה shard .

מסוף GCP

  1. ב-Google Cloud Platform Console, עבור לדף מסדי נתונים .

    עבור אל מסדי נתונים

  2. בחר את מסד הנתונים הנדרש מרשימת מסדי הנתונים.

  3. בתפריט הניווט, לחץ על אינדקסים ולאחר מכן לחץ על הכרטיסייה שדה יחיד .

  4. לחץ על הכרטיסייה שדה יחיד .

  5. לחץ על הוסף פטור .

  6. עבור מזהה אוסף , הזן instruments . עבור נתיב שדה , הזן timestamp .

  7. תחת היקף שאילתה , בחר גם אוסף וגם קבוצת אוסף .

  8. הקש "הבא

  9. החלף את כל הגדרות האינדקס למצב מושבת . לחץ על שמור .

  10. חזור על אותם שלבים עבור שדה shard .

Firebase CLI

  1. הוסף את הדברים הבאים למקטע fieldOverrides בקובץ הגדרות האינדקס שלך:

    {
     "fieldOverrides": [
       // Disable single-field indexing for the timestamp field
       {
         "collectionGroup": "instruments",
         "fieldPath": "timestamp",
         "indexes": []
       },
     ]
    }
    
  2. פרוס את הגדרות האינדקס המעודכנות שלך:

    firebase deploy --only firestore:indexes
    

צור אינדקסים מרוכבים חדשים

לאחר הסרת כל האינדקסים הקודמים המכילים את timestamp , הגדר את האינדקסים החדשים שהאפליקציה שלך דורשת. כל אינדקס המכיל את שדה חותמת timestamp חייב להכיל גם את שדה shard . לדוגמה, כדי לתמוך בשאילתות למעלה, הוסף את האינדקסים הבאים:

אוסף שדות הוספו לאינדקס היקף שאילתה
כלים shard, price.currency, חותמת זמן אוסף
כלים shard, exchange, חותמת זמן אוסף
כלים shard, instrumentType, חותמת זמן אוסף

הודעות שגיאה

אתה יכול לבנות את האינדקסים האלה על ידי הפעלת השאילתות המעודכנות.

כל שאילתה מחזירה הודעת שגיאה עם קישור ליצירת האינדקס הנדרש ב-Firebase Console.

Firebase CLI

  1. הוסף את האינדקסים הבאים לקובץ הגדרות האינדקס שלך:

     {
       "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. פרוס את הגדרות האינדקס המעודכנות שלך:

    firebase deploy --only firestore:indexes
    

הבנת שדות ה-write for limit ברצף באינדקס

המגבלה על קצב הכתיבה של שדות עוקבים שנוספו לאינדקס נובעת מהאופן שבו Cloud Firestore מאחסנת ערכי אינדקס ומקנה קנה מידה של כתיבת אינדקס. עבור כל כתיבת אינדקס, Cloud Firestore מגדירה ערך מפתח-ערך שמשרשר את שם המסמך והערך של כל שדה שנוסף לאינדקס. Cloud Firestore מארגנת את כניסות האינדקס הללו לקבוצות נתונים הנקראות טאבלטים . כל שרת Cloud Firestore מכיל טאבלט אחד או יותר. כאשר עומס הכתיבה לטאבלט מסוים הופך גבוה מדי, Cloud Firestore משתנה לרוחב על ידי פיצול הטאבלט לטאבלטים קטנים יותר והפצת הטאבלטים החדשים על פני שרתי Cloud Firestore שונים.

Cloud Firestore מציבה ערכי אינדקס קרובים מבחינה לקסיקוגרפית באותו טאבלט. אם ערכי האינדקס בטאבלט קרובים מדי זה לזה, כגון עבור שדות חותמת זמן, Cloud Firestore לא יכולה לפצל ביעילות את הטאבלט לטאבלטים קטנים יותר. זה יוצר נקודה חמה שבה טאבלט בודד מקבל יותר מדי תעבורה, ופעולות הקריאה והכתיבה לנקודה החמה נעשות איטיות יותר.

על ידי פיצול שדה חותמת זמן, אתה מאפשר ל-Cloud Firestore לפצל ביעילות עומסי עבודה על פני מספר טאבלטים. למרות שהערכים של שדה חותמת הזמן עשויים להישאר קרובים זה לזה, הרסיס המשורשר וערך האינדקס מעניקים ל-Cloud Firestore מספיק מקום בין ערכי האינדקס כדי לפצל את הערכים בין מספר טאבלטים.

מה הלאה