Bir koleksiyon, dizine eklenen sıralı değerlere sahip belgeler içeriyorsa Cloud Firestore, yazma hızını saniyede 500 yazmayla sınırlar. Bu sayfada, bu sınırı aşmak için bir doküman alanının nasıl bölümleneceği açıklanmaktadır. Öncelikle "sıralı dizine eklenen alanlar"dan ne anladığımızı tanımlayalım ve bu sınırın ne zaman geçerli olduğunu açıklığa kavuşturalım.
Sıralı dizine eklenen alanlar
"Sıralı dizine eklenen alanlar", monoton olarak artan veya azalan bir dizine eklenen alan içeren tüm belge koleksiyonlarını ifade eder. Bu, çoğu durumda bir timestamp
alanı anlamına gelir ancak tekdüze şekilde artan veya azalan herhangi bir alan değeri, saniyede 500 yazma işlemi olan yazma sınırını tetikleyebilir.
Örneğin, uygulama userid
değerlerini aşağıdaki gibi atarsa sınır, dizine eklenmiş userid
alanına sahip user
belge koleksiyonu için geçerli olur:
1281, 1282, 1283, 1284, 1285, ...
Öte yandan, tüm timestamp
alanları bu sınırı tetiklemez. Bir timestamp
alanı rastgele dağıtılan değerleri izliyorsa yazma sınırı geçerli olmaz. Alanın gerçek değeri de önemli değildir. Yalnızca alanın tekdüze şekilde artması veya azalması önemlidir. Örneğin, aşağıdaki monoton artan alan değeri gruplarının ikisi de yazma sınırını tetikler:
100000, 100001, 100002, 100003, ...
0, 1, 2, 3, ...
Zaman damgası alanını bölme
Uygulamanızın monoton olarak artan bir timestamp
alanı kullandığını varsayalım.
Uygulamanız herhangi bir sorguda timestamp
alanını kullanmıyorsa zaman damgası alanını dizine eklemeyerek saniyede 500 yazma sınırını kaldırabilirsiniz. Sorgularınız için timestamp
alanına ihtiyacınız varsa bölünmüş zaman damgalarını kullanarak sınırı aşabilirsiniz:
timestamp
alanının yanına birshard
alanı ekleyin.shard
alanı için1..n
farklı değer kullanın. Bu işlem, koleksiyonun yazma sınırını500*n
olarak yükseltir ancakn
sorgusunu toplamanız gerekir.- Yazma mantığınızı, her belgeye rastgele bir
shard
değeri atayacak şekilde güncelleyin. - Sorgularınızı, parçalara ayrılmış sonuç kümelerini toplayacak şekilde güncelleyin.
- Hem
shard
alanı hem detimestamp
alanı için tek alanlı dizinleri devre dışı bırakın.timestamp
alanını içeren mevcut birleşik dizinleri silin. - Güncellenen sorgularınızı desteklemek için yeni bileşik dizinler oluşturun. Dizindeki alanların sırası önemlidir ve
shard
alanıtimestamp
alanından önce gelmelidir.timestamp
alanını içeren tüm dizinlershard
alanını da içermelidir.
Bölünmüş zaman damgalarını yalnızca saniyede 500'den fazla yazma hızına sahip kullanım alanlarında uygulamanız gerekir. Aksi takdirde bu, erken optimizasyondur. timestamp
alanını bölme işlemi, saniye başına 500 yazma işlemi kısıtlamasını kaldırır ancak müşteri tarafında sorgu toplama işlemlerine ihtiyaç duyma dezavantajına sahiptir.
Aşağıdaki örneklerde, bir timestamp
alanının nasıl bölümleneceği ve bölümlenmiş bir sonuç kümesinin nasıl sorgulandığı gösterilmektedir.
Örnek veri modeli ve sorgular
Örneğin, para birimleri, hisse senetleri ve ETF'ler gibi finansal enstrümanların neredeyse gerçek zamanlı analizine yönelik bir uygulama düşünün. Bu uygulama, belgeleri instruments
koleksiyonuna şu şekilde yazar:
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(); }
Bu uygulama aşağıdaki sorguları çalıştırır ve timestamp
alanına göre sıralar:
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]); });
Yaptığınız araştırmalar sonucunda, uygulamanın saniyede 1.000 ila 1.500 cihaz güncellemesi alacağını belirlersiniz. Bu, dizine eklenmiş zaman damgası alanları içeren dokümanlar içeren koleksiyonlar için izin verilen saniye başına 500 yazma işlemini aşar. Yazma işleme hızını artırmak için 3 parça değerine ihtiyacınız vardırMAX_INSTRUMENT_UPDATES/500 = 3
. Bu örnekte x
, y
ve z
parça değerleri kullanılmaktadır. Parça değerleriniz için sayılar veya başka karakterler de kullanabilirsiniz.
Bölme alanı ekleme
Dokümanlarınıza shard
alanı ekleyin. shard
alanını x
, y
veya z
değerlerine ayarlayarak koleksiyondaki yazma sınırını saniyede 1.500 yazma işlemine yükseltin.
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(); }
Parçalı zaman damgasını sorgulama
shard
alanı eklemek için sorgularınızı, bölümlenmiş sonuçları toplayacak şekilde güncellemeniz gerekir:
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]); });
Dizin tanımlarını güncelleme
Saniyede 500 yazma kısıtlamasını kaldırmak için timestamp
alanını kullanan mevcut tek alan ve birleşik dizinleri silin.
Birleşik dizin tanımlarını silme
Firebase Konsolu
Firebase konsolunda Cloud Firestore Kompozit Dizinler sayfasını açın.
timestamp
alanını içeren her dizin için düğmesini ve Sil'i tıklayın.
GCP Console
Google Cloud Console'da Veritabanları sayfasına gidin.
Veritabanları listesinden gerekli veritabanını seçin.
Gezinme menüsünde Dizine eklemeler'i, ardından Kompozit sekmesini tıklayın.
timestamp
alanını içeren dizin tanımlarını aramak için Filtre alanını kullanın.Bu dizinlerin her biri için
düğmesini ve ardından Sil'i tıklayın.
Firebase CLI
- Firebase CLI'yi ayarlamadıysanız CLI'yi yüklemek ve
firebase init
komutunu çalıştırmak için bu talimatları uygulayın.init
komutu sırasındaFirestore: Deploy rules and create indexes for Firestore
'yi seçtiğinizden emin olun. - Firebase CLI, kurulum sırasında mevcut dizin tanımlarınızı varsayılan olarak
firestore.indexes.json
adlı bir dosyaya indirir. timestamp
alanını içeren tüm dizin tanımlarını kaldırın. Örneğin:{ "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" } ] }, ] }
Güncellenen dizin tanımlarınızı dağıtın:
firebase deploy --only firestore:indexes
Tek alanlı dizin tanımlarını güncelleme
Firebase Konsolu
Firebase konsolunda Cloud Firestore Tek Alan Dizinleri sayfasını açın.
Muafiyet Ekle'yi tıklayın.
Koleksiyon Kimliği için
instruments
değerini girin. Alan yolu içintimestamp
yazın.Sorgu kapsamı bölümünde hem Koleksiyon hem de Koleksiyon grubu'nu seçin.
Sonraki'yi tıklayın.
Tüm dizin ayarlarını Devre dışı olarak değiştirin. Kaydet'i tıklayın.
shard
alanı için aynı adımları tekrarlayın.
GCP Console
Google Cloud Console'da Veritabanları sayfasına gidin.
Veritabanları listesinden gerekli veritabanını seçin.
Gezinme menüsünde Dizine Eklenenler'i, ardından Tek Alan sekmesini tıklayın.
Tek Alan sekmesini tıklayın.
Muafiyet Ekle'yi tıklayın.
Koleksiyon Kimliği için
instruments
değerini girin. Alan yolu içintimestamp
yazın.Sorgu kapsamı bölümünde hem Koleksiyon hem de Koleksiyon grubu'nu seçin.
Sonraki'yi tıklayın.
Tüm dizin ayarlarını Devre dışı olarak değiştirin. Kaydet'i tıklayın.
shard
alanı için aynı adımları tekrarlayın.
Firebase CLI
Dizin tanımları dosyanızın
fieldOverrides
bölümüne aşağıdakileri ekleyin:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }
Güncellenen dizin tanımlarınızı dağıtın:
firebase deploy --only firestore:indexes
Yeni birleşik dizinler oluşturma
timestamp
içeren önceki tüm dizineleri kaldırdıktan sonra uygulamanızın gerektirdiği yeni dizinleri tanımlayın. timestamp
alanını içeren tüm dizinler shard
alanını da içermelidir. Örneğin, yukarıdaki sorguları desteklemek için aşağıdaki dizinleri ekleyin:
Toplama | Dizine eklenen alanlar | Sorgu kapsamı |
---|---|---|
enstrümanlar | shard, price.currency, timestamp | Toplama |
enstrümanlar | parçası, exchange, zaman damgası | Toplama |
enstrümanlar | shard, instrumentType, timestamp | Toplama |
Hata Mesajları
Güncellenen sorguları çalıştırarak bu dizinleri oluşturabilirsiniz.
Her sorgu, Firebase Console'da gerekli dizini oluşturma bağlantısını içeren bir hata mesajı döndürür.
Firebase CLI
Dizin tanımı dosyanıza aşağıdaki dizinleri ekleyin:
{ "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" } ] }, ] }
Güncellenen dizin tanımlarınızı dağıtın:
firebase deploy --only firestore:indexes
Sıralı dizine eklenen alanlar için yazma sınırını anlama
Sıralı dizine eklenen alanlar için yazma hızıyla ilgili sınır, Cloud Firestore'ün dizin değerlerini depolama ve dizin yazma işlemlerini ölçeklendirme şeklinden kaynaklanır. Her dizin yazma işlemi için Cloud Firestore, belge adını ve dizine eklenen her alanın değerini birleştiren bir anahtar/değer girişi tanımlar. Cloud Firestore bu dizin girişlerini tabletler adı verilen veri grupları halinde düzenler. Her Cloud Firestore sunucusu bir veya daha fazla tablet barındırır. Belirli bir tablete yönelik yazma yükü çok yüksek olduğunda Cloud Firestore, tableti daha küçük tabletlere bölerek ve yeni tabletleri farklı Cloud Firestore sunucularına dağıtarak yatay ölçeklendirme yapar.
Cloud Firestore, alfabetik olarak yakın dizin girişlerini aynı tablete yerleştirir. Bir tabletteki dizin değerleri birbirine çok yakınsa (ör. zaman damgası alanları) Cloud Firestore, tableti daha küçük tabletlere verimli bir şekilde bölemez. Bu durum, tek bir tabletin çok fazla trafik aldığı ve sıcak noktaya yönelik okuma ve yazma işlemlerinin yavaşladığı bir sıcak nokta oluşturur.
Bir zaman damgası alanını parçalara ayırarak Cloud Firestore'ün iş yüklerini birden fazla tablete verimli bir şekilde dağıtmasını sağlarsınız. Zaman damgası alanının değerleri birbirine yakın kalabilir ancak birleştirilmiş parça ve dizin değeri, girişleri birden fazla tablet arasında bölmek için dizin girişleri arasında Cloud Firestore yeterli alan sağlar.
Sırada ne var?
- Geniş ölçekte tasarım yapmayla ilgili en iyi uygulamaları okuyun.
- Tek bir dokümana yüksek yazma oranlarının olduğu durumlar için Dağıtılmış sayaçlar başlıklı makaleyi inceleyin.
- Cloud Firestore için standart sınırlara bakın.