مُهرهای زمان تکالیف اشتراکی

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

فیلدهای نمایه‌گذاری‌شده متوالی

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

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

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

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

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

تکه‌تکه کردن فیلد مُهر زمان

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

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

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

مثال‌های زیر نشان می‌دهد که چگونه یک فیلد 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]);
    });

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

افزودن فیلد قطعه

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

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

به‌روزرسانی تعاریف نمایه

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

حذف تعریف‌های نمایه ترکیبی

کنسول Firebase

  1. در کنسول Firebase، به برگه Databases & Storage (پایگاه‌های داده و فضای ذخیره‌سازی) > Firestore > Composite Indexes (نمایه‌های ترکیبی) بروید.

    رفتن به نمایه‌های ترکیبی

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

کنسول GCP

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

    رفتن به پایگاه‌های داده

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

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

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

  5. برای هرکدام از این نمایه‌ها، روی دکمه کلیک کنید و روی حذف کلیک کنید.

واسط خط فرمان Firebase

  1. اگر واسط خط فرمان Firebase را راه‌اندازی نکرده‌اید، این دستورالعمل‌ها را برای نصب واسط خط فرمان و اجرای فرمان 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. در کنسول Firebase، به برگه Databases & Storage (پایگاه‌های داده و فضای ذخیره‌سازی) > Firestore > Single Field Indexes (نمایه‌های تک‌فیلدی) بروید.

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

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

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

  4. در بخش گستره پُرسمان، هم مجموعه و هم گروه مجموعه را انتخاب کنید.

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

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

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

کنسول GCP

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

    رفتن به پایگاه‌های داده

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

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

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

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

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

  7. در بخش گستره پُرسمان، هم مجموعه و هم گروه مجموعه را انتخاب کنید.

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

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

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

واسط خط فرمان Firebase

  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, exchange, timestamp مجموعه
سازها ‫ shard،‏ instrumentType،‏ timestamp مجموعه

پیام های خطا

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

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

واسط خط فرمان Firebase

  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 فضای کافی بین ورودی‌های نمایه می‌دهد تا ورودی‌ها را بین چندین تبلت تقسیم کند.

حالا چه می‌شود