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

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

فیلدهای فهرست‌بندی‌شده‌ی متوالی

«فیلدهای فهرست‌بندی‌شده‌ی متوالی» به هر مجموعه‌ای از اسناد اطلاق می‌شود که حاوی یک فیلد فهرست‌بندی‌شده‌ی یکنواخت افزایشی یا کاهشی باشد. در بسیاری از موارد، این به معنای یک فیلد timestamp است، اما هر مقدار فیلد که به طور یکنواخت افزایشی یا کاهشی باشد، می‌تواند محدودیت نوشتن ۵۰۰ نوشتن در ثانیه را فعال کند.

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

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

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

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

تقسیم‌بندی یک فیلد مهر زمانی

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

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

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

مثال‌های زیر نحوه‌ی تقسیم‌بندی یک فیلد timestamp و نحوه‌ی پرس‌وجو از یک مجموعه نتیجه‌ی تقسیم‌بندی‌شده را نشان می‌دهند.

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

به عنوان مثال، برنامه‌ای را برای تجزیه و تحلیل تقریباً بلادرنگ ابزارهای مالی مانند ارزها، سهام عادی و ETFها تصور کنید. این برنامه اسناد را به صورت زیر در مجموعه‌ای instruments می‌نویسد:

نود جی اس
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 اجرا می‌کند:

نود جی اس
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]);
    });

پس از کمی تحقیق، متوجه می‌شوید که برنامه بین ۱۰۰۰ تا ۱۵۰۰ به‌روزرسانی ابزار در ثانیه دریافت می‌کند. این مقدار از ۵۰۰ نوشتن در ثانیه مجاز برای مجموعه‌های حاوی اسناد با فیلدهای timestamp فهرست‌بندی شده، بیشتر است. برای افزایش توان عملیاتی نوشتن، به ۳ مقدار shard نیاز دارید، MAX_INSTRUMENT_UPDATES/500 = 3 این مثال از مقادیر shard x ، y و z استفاده می‌کند. همچنین می‌توانید از اعداد یا کاراکترهای دیگر برای مقادیر shard خود استفاده کنید.

اضافه کردن یک فیلد shard

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

نود جی اس
// 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 مستلزم آن است که کوئری‌های خود را برای تجمیع نتایج sharded به‌روزرسانی کنید:

نود جی اس
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]);
    });

به‌روزرسانی تعاریف شاخص

برای حذف محدودیت ۵۰۰ نوشتن در ثانیه، اندیس‌های تک فیلدی و ترکیبی موجود که از فیلد timestamp استفاده می‌کنند را حذف کنید.

حذف تعاریف شاخص مرکب

کنسول فایربیس

  1. صفحه Cloud Firestore Composite Indexes را در کنسول Firebase باز کنید.

    به فهرست‌های مرکب بروید

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

کنسول GCP

  1. در کنسول گوگل کلود، به صفحه پایگاه‌های داده بروید.

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

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

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

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

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

رابط خط فرمان فایربیس

  1. اگر رابط خط فرمان فایربیس (Firebase 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
    

به‌روزرسانی تعاریف شاخص تک‌فیلدی

کنسول فایربیس

  1. صفحه Cloud Firestore Single Field Indexes را در کنسول Firebase باز کنید.

    به فهرست‌های تک فیلدی بروید

  2. روی افزودن معافیت کلیک کنید.

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

  4. در قسمت Query scope ، هم Collection و هم Collection group را انتخاب کنید.

  5. روی بعدی کلیک کنید

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

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

کنسول GCP

  1. در کنسول گوگل کلود، به صفحه پایگاه‌های داده بروید.

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

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

  3. در منوی پیمایش، روی فهرست‌ها (Indexes ) کلیک کنید و سپس روی تب تک فیلد (Single Field) کلیک کنید.

  4. روی تب تک فیلد کلیک کنید.

  5. روی افزودن معافیت کلیک کنید.

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

  7. در قسمت Query scope ، هم Collection و هم Collection group را انتخاب کنید.

  8. روی بعدی کلیک کنید

  9. تمام تنظیمات فهرست را روی غیرفعال (Disabled) قرار دهید. روی ذخیره (Save) کلیک کنید.

  10. همین مراحل را برای فیلد shard تکرار کنید.

رابط خط فرمان فایربیس

  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 نیز باشد. به عنوان مثال، برای پشتیبانی از کوئری‌های بالا، اندیس‌های زیر را اضافه کنید:

مجموعه فیلدهای ایندکس شده دامنه پرس و جو
سازها تکه ، قیمت.ارز ، مهر زمانی مجموعه
سازها تکه ، تبادل ، مهر زمانی مجموعه
سازها تکه ، نوع ساز ، مهر زمانی مجموعه

پیام‌های خطا

شما می‌توانید این ایندکس‌ها را با اجرای کوئری‌های به‌روزرسانی‌شده بسازید.

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

رابط خط فرمان فایربیس

  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 نمی‌تواند تبلت را به طور موثر به تبلت‌های کوچکتر تقسیم کند. این یک نقطه داغ ایجاد می‌کند که در آن یک تبلت ترافیک زیادی دریافت می‌کند و عملیات خواندن و نوشتن در نقطه داغ کندتر می‌شود.

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

قدم بعدی چیست؟