اگر مجموعهای حاوی اسنادی با مقادیر نمایهشده متوالی باشد، Cloud Firestore نرخ نوشتن را به ۵۰۰ نوشتن در ثانیه محدود میکند. این صفحه نحوه قطعهبندی فیلد سند را برای غلبه بر این محدودیت شرح میدهد. ابتدا، بیایید تعریف کنیم که منظورمان از «فیلدهای نمایهشده متوالی» چیست و روشن کنیم که این محدودیت چه زمانی اعمال میشود.
فیلدهای نمایهگذاریشده متوالی
«فیلدهای نمایهشده متوالی» بهمعنای هر مجموعهای از اسناد است که حاوی
فیلد نمایهشده یکنواخت افزایشی یا کاهشی باشد. در بسیاری از موارد، این به معنای فیلد timestamp است، اما هر مقدار فیلد یکنواخت افزایشی یا کاهشی میتواند حد نوشتن ۵۰۰ نوشتن در ثانیه را فعال کند.
برای مثال، اگر برنامه مقادیر userid را بهصورت زیر اختصاص دهد، این محدودیت برای مجموعهای از user سند با فیلد نمایهشده userid اعمال میشود:
1281, 1282, 1283, 1284, 1285, ...
ازسوی دیگر، همه فیلدهای timestamp این محدودیت را فعال نمیکنند. اگر فیلد
timestamp مقادیر توزیعشده تصادفی را ردیابی کند، محدودیت نوشتن اعمال نمیشود. مقدار واقعی فیلد نیز مهم نیست، فقط اینکه فیلد بهصورت یکنواخت درحال افزایش یا کاهش باشد. برای مثال،
هر دو مجموعه مقادیر فیلد افزایشی یکنواخت زیر حد نوشتن را فعال میکنند:
100000, 100001, 100002, 100003, ...0, 1, 2, 3, ...
تکهتکه کردن فیلد مُهر زمان
فرض کنید برنامهتان از فیلد timestamp که بهطور یکنواخت افزایش مییابد استفاده میکند.
اگر برنامهتان از فیلد timestamp در هیچ پُرسمانی استفاده نمیکند، میتوانید با نمایهسازی نکردن فیلد مُهر زمان، محدودیت ۵۰۰ نوشتن در ثانیه را بردارید. اگر برای پُرسمانهایتان به فیلد timestamp نیاز دارید، میتوانید بااستفاده از مُهرهای زمان قطعهبندیشده این محدودیت را دور بزنید:
- فیلد
shardرا در کنار فیلدtimestampاضافه کنید. از1..nمقدار متمایز برای فیلدshardاستفاده کنید. این کار حد نوشتن برای مجموعه را به500*nافزایش میدهد، اما باید پُرسمانهایnرا جمعآوری کنید. - منطق نوشتن خود را بهروز کنید تا بهطور تصادفی مقدار
shardرا به هر سند اختصاص دهد. - پُرسمانهایتان را بهروز کنید تا مجموعههای نتایج بخشبندیشده را تجمیع کنید.
- نمایههای تکفیلدی را هم برای فیلد
shardو هم برای فیلدtimestampغیرفعال کنید. نمایههای ترکیبی موجود را که حاوی فیلدtimestampهستند حذف کنید. - نمایههای ترکیبی جدیدی برای پشتیبانی از پُرسمانهای بهروزشدهتان ایجاد کنید. ترتیب فیلدها در نمایه مهم است و فیلد
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
در کنسول Firebase، به برگه Databases & Storage (پایگاههای داده و فضای ذخیرهسازی) > Firestore > Composite Indexes (نمایههای ترکیبی) بروید.
برای هر نمایهای که حاوی فیلد
timestampاست، روی دکمه کلیک کنید و روی حذف کلیک کنید.
کنسول GCP
در کنسول Google Cloud، به صفحه پایگاههای داده بروید.
پایگاه داده موردنیاز را از فهرست پایگاههای داده انتخاب کنید.
در منو پیمایش، روی نمایهها کلیک کنید، سپس روی برگه ترکیبی کلیک کنید.
از فیلد فیلتر برای جستجوی تعاریف نمایهای که حاوی فیلد
timestampهستند استفاده کنید.برای هرکدام از این نمایهها، روی دکمه کلیک کنید و روی حذف کلیک کنید.
واسط خط فرمان Firebase
- اگر واسط خط فرمان Firebase را راهاندازی نکردهاید، این دستورالعملها را برای نصب
واسط خط فرمان و اجرای فرمان
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
در کنسول Firebase، به برگه Databases & Storage (پایگاههای داده و فضای ذخیرهسازی) > Firestore > Single Field Indexes (نمایههای تکفیلدی) بروید.
روی افزودن معافیت کلیک کنید.
برای شناسه مجموعه،
instrumentsرا وارد کنید. برای مسیر فیلد،timestampرا وارد کنید.در بخش گستره پُرسمان، هم مجموعه و هم گروه مجموعه را انتخاب کنید.
روی بعدی کلیک کنید.
همه تنظیمات نمایه را روی غیرفعال قرار دهید. روی ذخیره کلیک کنید.
همین مراحل را برای فیلد
shardتکرار کنید.
کنسول GCP
در کنسول Google Cloud، به صفحه پایگاههای داده بروید.
پایگاه داده موردنیاز را از فهرست پایگاههای داده انتخاب کنید.
در منو پیمایش، روی نمایهها کلیک کنید، سپس روی برگه تکفیلد کلیک کنید.
روی برگه فیلد تکی کلیک کنید.
روی افزودن معافیت کلیک کنید.
برای شناسه مجموعه،
instrumentsرا وارد کنید. برای مسیر فیلد،timestampرا وارد کنید.در بخش گستره پُرسمان، هم مجموعه و هم گروه مجموعه را انتخاب کنید.
روی بعدی کلیک کنید.
همه تنظیمات نمایه را روی غیرفعال قرار دهید. روی ذخیره کلیک کنید.
همین مراحل را برای فیلد
shardتکرار کنید.
واسط خط فرمان Firebase
موارد زیر را به بخش
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, exchange, timestamp | مجموعه |
| سازها | shard، instrumentType، timestamp | مجموعه |
پیام های خطا
با اجرای پرسمانهای بهروزشده میتوانید این نمایهها را بسازید.
هر پُرسمان پیام خطایی را با پیوندی برای ایجاد نمایه موردنیاز در «کنسول Firebase» برمیگرداند.
واسط خط فرمان Firebase
نمایههای زیر را به فایل تعریف نمایه خود اضافه کنید:
{ "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 را ببینید