مهرهای زمانی خرد شده

اگر مجموعه ای حاوی اسنادی با مقادیر فهرست شده متوالی باشد، Cloud Firestore سرعت نوشتن را به 500 نوشتن در ثانیه محدود می کند. این صفحه نحوه تقسیم یک فیلد سند را برای غلبه بر این محدودیت توضیح می دهد. ابتدا بیایید منظورمان از "فیلدهای نمایه شده متوالی" را تعریف کنیم و مشخص کنیم که این محدودیت چه زمانی اعمال می شود.

فیلدهای نمایه شده متوالی

"فیلدهای نمایه شده متوالی" به معنای هر مجموعه ای از اسناد است که دارای یک فیلد نمایه شده به طور یکنواخت در حال افزایش یا کاهش است. در بسیاری از موارد، این به معنای یک فیلد timestamp است، اما هر مقدار فیلد به صورت یکنواخت افزایش یا کاهش می‌یابد، می‌تواند محدودیت نوشتن 500 نوشتن در ثانیه را ایجاد کند.

به عنوان مثال، اگر برنامه مقادیر userid مانند موارد زیر را اختصاص دهد، این محدودیت برای مجموعه‌ای از اسناد user با userid فیلد نمایه‌شده اعمال می‌شود:

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

از طرف دیگر، همه فیلدهای timestamp این محدودیت را ایجاد نمی کنند. اگر یک فیلد timestamp مقادیر توزیع شده تصادفی را دنبال کند، محدودیت نوشتن اعمال نمی شود. مقدار واقعی فیلد نیز مهم نیست، فقط این که میدان به طور یکنواخت در حال افزایش یا کاهش است. به عنوان مثال، هر دو مجموعه زیر از مقادیر فیلد به طور یکنواخت افزایش می‌یابند، محدودیت نوشتن را فعال می‌کنند:

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

اشتراک گذاری یک فیلد مهر زمانی

فرض کنید برنامه شما از یک فیلد timestamp یکنواخت در حال افزایش استفاده می کند. اگر برنامه شما از فیلد timestamp در هیچ درخواستی استفاده نمی کند، می توانید محدودیت 500 نوشتن در ثانیه را با فهرست نکردن فیلد مهر زمانی حذف کنید. اگر به یک فیلد timestamp برای جستارهای خود نیاز دارید، می‌توانید با استفاده از مهرهای زمانی خرد شده، این محدودیت را دور بزنید:

  1. در کنار فیلد timestamp ، یک فیلد shard اضافه کنید. از 1..n مقدار متمایز برای فیلد shard استفاده کنید. این محدودیت نوشتن برای مجموعه را به 500*n افزایش می دهد، اما باید n پرس و جو را جمع آوری کنید.
  2. منطق نوشتن خود را به روز کنید تا به طور تصادفی یک مقدار shard به هر سند اختصاص دهید.
  3. برای جمع‌آوری مجموعه‌های نتایج خرد شده، درخواست‌های خود را به‌روزرسانی کنید.
  4. ایندکس های تک فیلدی را هم برای فیلد shard و هم برای فیلد timestamp غیرفعال کنید. فهرست های ترکیبی موجود که حاوی فیلد timestamp هستند را حذف کنید.
  5. نمایه های ترکیبی جدیدی ایجاد کنید تا از درخواست های به روز شده خود پشتیبانی کنید. ترتیب فیلدها در یک فهرست مهم است و فیلد shard باید قبل از فیلد timestamp باشد. هر فهرستی که شامل فیلد timestamp باشد باید فیلد shard نیز شامل شود.

شما باید مهرهای زمانی خرد شده را فقط در موارد استفاده با نرخ نوشتن پایدار بالای 500 نوشتن در ثانیه اجرا کنید. در غیر این صورت، این یک بهینه سازی پیش از بلوغ است. به اشتراک گذاری یک فیلد timestamp محدودیت 500 نوشتن در ثانیه را حذف می کند، اما با نیاز به ادغام پرس و جو در سمت مشتری نیاز است.

مثال‌های زیر نشان می‌دهند که چگونه یک فیلد timestamp را به اشتراک بگذارید و چگونه یک مجموعه نتایج خرد شده را پرس و جو کنید.

نمونه داده مدل و پرس و جو

به عنوان مثال، اپلیکیشنی را برای تجزیه و تحلیل در زمان واقعی ابزارهای مالی مانند ارزها، سهام عادی و ETF تصور کنید. این برنامه اسناد را در مجموعه 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]);
    });

پس از مدتی تحقیق، مشخص می کنید که برنامه بین 1000 تا 1500 به روز رسانی ابزار در ثانیه دریافت می کند. این از 500 نوشتن در ثانیه مجاز برای مجموعه های حاوی اسناد با فیلدهای مهر زمانی نمایه شده فراتر می رود. برای افزایش توان نوشتن، به 3 مقدار شارد نیاز دارید، MAX_INSTRUMENT_UPDATES/500 = 3 . این مثال از مقادیر x ، y و z استفاده می کند. همچنین می توانید از اعداد یا کاراکترهای دیگر برای مقادیر خرده خود استفاده کنید.

اضافه کردن یک میدان خرد شده

یک فیلد shard به اسناد خود اضافه کنید. قسمت shard را روی مقادیر x ، y یا z تنظیم کنید که محدودیت نوشتن در مجموعه را به 1500 نوشتن در ثانیه افزایش می‌دهد.

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 Indexes را در کنسول Firebase باز کنید.

    به شاخص های ترکیبی بروید

  2. برای هر فهرستی که حاوی فیلد timestamp است، روی دکمه کلیک کنید و روی Delete کلیک کنید.

کنسول GCP

  1. در Google Cloud Platform Console، به صفحه Databases بروید.

    به پایگاه داده ها بروید

  2. پایگاه داده مورد نیاز را از لیست پایگاه های داده انتخاب کنید.

  3. در منوی پیمایش، روی Indexes کلیک کنید و سپس روی برگه ترکیبی کلیک کنید.

  4. از فیلد فیلتر برای جستجوی تعاریف فهرستی که حاوی فیلد timestamp هستند استفاده کنید.

  5. برای هر یک از این فهرست ها، روی دکمه کلیک کنید و روی Delete کلیک کنید.

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 Indexes را در کنسول Firebase باز کنید.

    به قسمت Single Field Indexes بروید

  2. روی Add Exemption کلیک کنید.

  3. برای شناسه مجموعه ، instruments وارد کنید. برای مسیر فیلد ، timestamp وارد کنید.

  4. در محدوده Query ، هم مجموعه و هم گروه مجموعه را انتخاب کنید.

  5. روی Next کلیک کنید

  6. همه تنظیمات فهرست را روی غیرفعال تغییر دهید. روی ذخیره کلیک کنید.

  7. همین مراحل را برای قسمت shard تکرار کنید.

کنسول GCP

  1. در Google Cloud Platform Console، به صفحه Databases بروید.

    به پایگاه داده ها بروید

  2. پایگاه داده مورد نیاز را از لیست پایگاه های داده انتخاب کنید.

  3. در منوی پیمایش، روی Indexes کلیک کنید و سپس روی زبانه Single Field کلیک کنید.

  4. روی تب Single Field کلیک کنید.

  5. روی Add Exemption کلیک کنید.

  6. برای شناسه مجموعه ، instruments وارد کنید. برای مسیر فیلد ، timestamp وارد کنید.

  7. در محدوده Query ، هم مجموعه و هم گروه مجموعه را انتخاب کنید.

  8. روی Next کلیک کنید

  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، timestamp مجموعه
سازها shard، تبادل، timestamp مجموعه
سازها shard، instrumentType، timestamp مجموعه

پیغام خطا

شما می توانید این فهرست ها را با اجرای پرس و جوهای به روز شده بسازید.

هر پرس و جو یک پیام خطا همراه با پیوندی برای ایجاد نمایه مورد نیاز در کنسول Firebase برمی گرداند.

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
    

درک نوشتن برای محدود کردن فیلدهای نمایه شده متوالی

محدودیت نرخ نوشتن برای فیلدهای نمایه شده متوالی از نحوه ذخیره مقادیر شاخص و نوشتن شاخص مقیاس های Cloud Firestore ناشی می شود. برای هر نوشتن فهرست، Cloud Firestore یک ورودی کلید-مقدار تعریف می کند که نام سند و مقدار هر فیلد نمایه شده را به هم متصل می کند. Cloud Firestore این ورودی‌های فهرست را در گروه‌هایی از داده‌ها به نام تبلت سازمان‌دهی می‌کند. هر سرور Cloud Firestore دارای یک یا چند تبلت است. هنگامی که بار نوشتن در یک رایانه لوحی خاص بسیار زیاد می شود، Cloud Firestore با تقسیم تبلت به تبلت های کوچکتر و پخش تبلت های جدید در سرورهای مختلف Cloud Firestore به صورت افقی مقیاس می شود.

Cloud Firestore ورودی‌های فهرست نزدیک را از نظر لغوی روی همان رایانه لوحی قرار می‌دهد. اگر مقادیر شاخص در رایانه لوحی بسیار نزدیک به هم باشند، مانند فیلدهای مهر زمانی، Cloud Firestore نمی‌تواند تبلت را به تبلت‌های کوچک‌تر تقسیم کند. این یک نقطه داغ ایجاد می کند که در آن یک تبلت تنها ترافیک زیادی دریافت می کند و عملیات خواندن و نوشتن در نقطه داغ کندتر می شود.

با به اشتراک گذاشتن یک فیلد مهر زمانی، این امکان را برای Cloud Firestore فراهم می‌کنید که به طور موثر بارهای کاری را در تبلت‌های مختلف تقسیم کند. اگرچه ممکن است مقادیر فیلد مهر زمانی نزدیک به هم باقی بمانند، اما مقدار شارد و شاخص به هم پیوسته فضای کافی را بین ورودی‌های فهرست به Cloud Firestore می‌دهد تا ورودی‌ها را بین چندین تبلت تقسیم کند.

بعدش چی