اگر مجموعه ای حاوی اسنادی با مقادیر فهرست شده متوالی باشد، 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
برای جستارهای خود نیاز دارید، میتوانید با استفاده از مهرهای زمانی خرد شده، این محدودیت را دور بزنید:
- در کنار فیلد
timestamp
، یک فیلدshard
اضافه کنید. از1..n
مقدار متمایز برای فیلدshard
استفاده کنید. این محدودیت نوشتن برای مجموعه را به500*n
افزایش می دهد، اما بایدn
پرس و جو را جمع آوری کنید. - منطق نوشتن خود را به روز کنید تا به طور تصادفی یک مقدار
shard
به هر سند اختصاص دهید. - برای جمعآوری مجموعههای نتایج خرد شده، درخواستهای خود را بهروزرسانی کنید.
- ایندکس های تک فیلدی را هم برای فیلد
shard
و هم برای فیلدtimestamp
غیرفعال کنید. فهرست های ترکیبی موجود که حاوی فیلدtimestamp
هستند را حذف کنید. - نمایه های ترکیبی جدیدی ایجاد کنید تا از درخواست های به روز شده خود پشتیبانی کنید. ترتیب فیلدها در یک فهرست مهم است و فیلد
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
صفحه Cloud Firestore Composite Indexes را در کنسول Firebase باز کنید.
برای هر فهرستی که حاوی فیلد
timestamp
است، روی دکمه کلیک کنید و روی Delete کلیک کنید.
کنسول GCP
در کنسول Google Cloud، به صفحه پایگاه داده بروید.
پایگاه داده مورد نیاز را از لیست پایگاه های داده انتخاب کنید.
در منوی پیمایش، روی Indexes کلیک کنید و سپس روی برگه ترکیبی کلیک کنید.
از فیلد فیلتر برای جستجوی تعاریف فهرستی که حاوی فیلد
timestamp
هستند استفاده کنید.برای هر یک از این فهرست ها، روی دکمه
کلیک کنید و روی Delete کلیک کنید.
Firebase CLI
- اگر Firebase CLI را راهاندازی نکردهاید، این دستورالعملها را برای نصب CLI دنبال کنید و دستور
firebase init
را اجرا کنید . در طول دستورinit
، مطمئن شوید کهFirestore: Deploy rules and create indexes for Firestore
. - در طول راهاندازی، Firebase CLI تعاریف فهرست موجود شما را در فایلی با نام، بهطور پیشفرض،
firestore.indexes.json
دانلود میکند. هر تعاریف شاخصی که حاوی فیلد
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" } ] }, ] }
تعاریف شاخص به روز شده خود را به کار ببرید:
firebase deploy --only firestore:indexes
تعاریف شاخص تک فیلدی را به روز کنید
کنسول Firebase
صفحه Cloud Firestore Single Field Indexes را در کنسول Firebase باز کنید.
روی Add Exemption کلیک کنید.
برای شناسه مجموعه ،
instruments
وارد کنید. برای مسیر فیلد ،timestamp
وارد کنید.در محدوده Query ، هم مجموعه و هم گروه مجموعه را انتخاب کنید.
روی Next کلیک کنید
همه تنظیمات فهرست را روی غیرفعال تغییر دهید. روی ذخیره کلیک کنید.
همین مراحل را برای قسمت
shard
تکرار کنید.
کنسول GCP
در کنسول Google Cloud، به صفحه پایگاه داده بروید.
پایگاه داده مورد نیاز را از لیست پایگاه های داده انتخاب کنید.
در منوی پیمایش، روی Indexes کلیک کنید و سپس روی زبانه Single Field کلیک کنید.
روی تب Single Field کلیک کنید.
روی Add Exemption کلیک کنید.
برای شناسه مجموعه ،
instruments
وارد کنید. برای مسیر فیلد ،timestamp
وارد کنید.در محدوده Query ، هم مجموعه و هم گروه مجموعه را انتخاب کنید.
روی Next کلیک کنید
همه تنظیمات فهرست را روی غیرفعال تغییر دهید. روی ذخیره کلیک کنید.
همین مراحل را برای قسمت
shard
تکرار کنید.
Firebase CLI
موارد زیر را به قسمت
fieldOverrides
فایل تعاریف فهرست خود اضافه کنید:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }
تعاریف شاخص به روز شده خود را به کار بگیرید:
firebase deploy --only firestore:indexes
نمایه های ترکیبی جدید ایجاد کنید
پس از حذف تمام نمایه های قبلی حاوی timestamp
، نمایه های جدیدی را که برنامه شما نیاز دارد تعریف کنید. هر فهرستی که حاوی فیلد timestamp
باشد باید حاوی فیلد shard
نیز باشد. به عنوان مثال، برای پشتیبانی از کوئری های بالا، ایندکس های زیر را اضافه کنید:
مجموعه | فیلدهای نمایه شده | محدوده پرس و جو |
---|---|---|
سازها | shard، price.currency، timestamp | مجموعه |
سازها | shard، تبادل، timestamp | مجموعه |
سازها | shard، instrumentType، timestamp | مجموعه |
پیام های خطا
شما می توانید این فهرست ها را با اجرای پرس و جوهای به روز شده بسازید.
هر پرس و جو یک پیام خطا همراه با پیوندی برای ایجاد نمایه مورد نیاز در کنسول Firebase برمی گرداند.
Firebase CLI
ایندکس های زیر را به فایل تعریف شاخص خود اضافه کنید:
{ "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" } ] }, ] }
تعاریف شاخص به روز شده خود را به کار ببرید:
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 میدهد تا ورودیها را بین چندین تبلت تقسیم کند.
بعدش چی
- بهترین روش ها برای طراحی برای مقیاس را بخوانید
- برای مواردی با نرخ نوشتن بالا در یک سند، به شمارنده های مختل مراجعه کنید
- محدودیت های استاندارد برای Cloud Firestore را ببینید