Jika suatu koleksi berisi dokumen dengan nilai terindeks berurutan, Cloud Firestore akan membatasi tingkat penulisan menjadi 500 penulisan per detik. Halaman ini menjelaskan cara melakukan sharding kolom dokumen untuk mengatasi batas ini. Pertama, mari definisikan arti "kolom terindeks berurutan" dan perjelas kapan batas ini berlaku.
Kolom terindeks berurutan
"Kolom terindeks berurutan" adalah koleksi dokumen apa pun yang berisi
kolom terindeks yang meningkat atau menurun secara monoton. Sering kali, yang dimaksud adalah kolom timestamp
, tetapi nilai kolom apa pun yang meningkat atau menurun secara monoton
dapat memicu batas penulisan sebanyak 500 penulisan per detik.
Misalnya, batas tersebut akan berlaku pada koleksi dokumen user
dengan
kolom terindeks userid
jika aplikasi menetapkan nilai userid
seperti berikut:
1281, 1282, 1283, 1284, 1285, ...
Di sisi lain, tidak semua kolom timestamp
memicu batas ini. Jika kolom timestamp
melacak nilai yang terdistribusi secara acak,
batas penulisan tidak berlaku. Nilai kolom yang sebenarnya juga tidak berpengaruh, yang penting kolom
tersebut meningkat atau menurun secara monoton. Misalnya, kedua set nilai kolom yang meningkat secara monoton berikut ini akan memicu batas penulisan:
100000, 100001, 100002, 100003, ...
0, 1, 2, 3, ...
Sharding kolom stempel waktu
Misalkan aplikasi Anda menggunakan kolom timestamp
yang meningkat secara monoton.
Jika aplikasi Anda tidak menggunakan kolom timestamp
dalam kueri apa pun, Anda dapat menghapus
batas 500 penulisan per detik dengan cara tidak mengindeks kolom stempel waktu. Jika Anda benar-benar
membutuhkan kolom timestamp
untuk kueri, Anda dapat menyiasati batasnya
menggunakan stempel waktu yang di-sharding:
- Tambahkan kolom
shard
di samping kolomtimestamp
. Gunakan nilai1..n
yang berbeda untuk kolomshard
. Hal ini akan meningkatkan batas penulisan untuk koleksi menjadi500*n
, tetapi Anda harus menggabungkan kuerin
. - Perbarui logika penulisan agar secara acak menetapkan nilai
shard
pada setiap dokumen. - Update kueri Anda untuk menggabungkan kumpulan hasil yang di-sharding.
- Nonaktifkan indeks kolom tunggal untuk kolom
shard
dan kolomtimestamp
. Hapus indeks komposit yang ada yang berisi kolomtimestamp
. - Buat indeks komposit baru untuk mendukung kueri Anda yang telah diperbarui. Urutan kolom dalam indeks bersifat penting, dan kolom
shard
harus ditempatkan sebelum kolomtimestamp
. Setiap indeks yang menyertakan kolomtimestamp
juga harus menyertakan kolomshard
.
Anda harus menerapkan stempel waktu yang di-sharding hanya dalam kasus penggunaan
dengan tingkat penulisan berkelanjutan di atas 500 penulisan per detik. Di luar itu, pengoptimalan ini bersifat prematur. Sharding kolom timestamp
akan menghapus batas 500 penulisan per detik, tetapi akibatnya Anda memerlukan penggabungan kueri sisi klien.
Contoh berikut menunjukkan cara melakukan sharding kolom timestamp
dan cara membuat kueri untuk kumpulan hasil yang di-sharding.
Contoh model data dan kueri
Sebagai contoh, bayangkan aplikasi untuk melakukan analisis yang hampir real time bagi instrumen keuangan seperti mata uang, saham biasa, dan ETF. Aplikasi ini menulis
dokumen ke koleksi instruments
seperti berikut:
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(); }
Aplikasi ini menjalankan kueri berikut dan mengurutkan berdasarkan kolom 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]); });
Setelah melakukan riset, Anda menentukan bahwa aplikasi akan menerima antara 1.000 hingga 1.500 pembaruan instrumen per detik. Ini melampaui 500 penulisan per
detik yang diizinkan untuk koleksi yang berisi dokumen dengan kolom stempel
waktu terindeks. Untuk meningkatkan throughput penulisan, Anda memerlukan 3 nilai shard,
MAX_INSTRUMENT_UPDATES/500 = 3
. Contoh ini menggunakan nilai shard x
,
y
, dan z
. Anda juga dapat menggunakan angka atau karakter lain untuk nilai shard
Anda.
Menambahkan kolom shard
Tambahkan kolom shard
ke dokumen Anda. Tetapkan kolom shard
ke nilai x
, y
, atau z
yang menaikkan batas penulisan pada koleksi
menjadi 1.500 penulisan per detik.
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(); }
Membuat kueri untuk stempel waktu yang di-sharding
Untuk menambahkan kolom shard
, Anda harus memperbarui kueri untuk menggabungkan hasil yang di-sharding:
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]); });
Memperbarui definisi indeks
Untuk menghapus batas 500 penulisan per detik, hapus indeks kolom tunggal
dan indeks komposit yang ada yang menggunakan kolom timestamp
.
Menghapus definisi indeks komposit
Firebase Console
Buka halaman Indeks Komposit Cloud Firestore di Firebase console.
Untuk setiap indeks yang berisi kolom
timestamp
, klik tombol dan klik Delete.
GCP Console
Di konsol Google Cloud, buka halaman Databases.
Pilih database yang diperlukan dari daftar database.
Di menu navigasi, klik Indexes, lalu klik tab Composite.
Gunakan kolom Filter untuk mencari definisi indeks yang berisi kolom
timestamp
.Untuk setiap indeks ini, klik tombol
dan klik Hapus.
Firebase CLI
- Jika Anda belum menyiapkan Firebase CLI, ikuti petunjuk ini untuk menginstal CLI dan menjalankan perintah
firebase init
. Selama perintahinit
, pastikan untuk memilihFirestore: Deploy rules and create indexes for Firestore
. - Selama penyiapan, Firebase CLI akan mendownload definisi indeks
yang ada ke file yang secara default bernama
firestore.indexes.json
. Hapus semua definisi indeks yang berisi kolom
timestamp
, misalnya:{ "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" } ] }, ] }
Deploy definisi indeks Anda yang telah diperbarui:
firebase deploy --only firestore:indexes
Memperbarui definisi Indeks kolom tunggal
Firebase Console
Buka halaman Indeks Kolom Tunggal Cloud Firestore di Firebase console.
Klik Add Exemption.
Untuk ID Koleksi, masukkan
instruments
. Untuk Jalur kolom, masukkantimestamp
.Pada Cakupan kueri, pilih Koleksi dan Grup koleksi.
Klik Berikutnya
Alihkan semua setelan indeks menjadi Dinonaktifkan. Klik Simpan.
Ulangi langkah yang sama untuk kolom
shard
.
GCP Console
Di konsol Google Cloud, buka halaman Databases.
Pilih database yang diperlukan dari daftar database.
Di menu navigasi, klik Indexes, lalu klik tab Single Field.
Klik tab Single Field.
Klik Tambahkan Pengecualian.
Untuk ID Koleksi, masukkan
instruments
. Untuk Jalur kolom, masukkantimestamp
.Pada Cakupan kueri, pilih Koleksi dan Grup koleksi.
Klik Berikutnya
Alihkan semua setelan indeks menjadi Dinonaktifkan. Klik Simpan.
Ulangi langkah yang sama untuk kolom
shard
.
Firebase CLI
Tambahkan yang berikut ini ke bagian
fieldOverrides
dari file definisi indeks Anda:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }
Deploy definisi indeks Anda yang telah diperbarui:
firebase deploy --only firestore:indexes
Membuat indeks komposit baru
Setelah menghapus semua indeks sebelumnya yang berisi timestamp
,
tentukan indeks baru yang diperlukan aplikasi Anda. Setiap indeks yang berisi
kolom timestamp
juga harus berisi kolom shard
. Misalnya, untuk mendukung
kueri di atas, tambahkan indeks berikut:
Koleksi | Kolom yang diindeks | Cakupan kueri |
---|---|---|
instruments | shard, price.currency, timestamp | Koleksi |
instruments | shard, exchange, timestamp | Koleksi |
instruments | shard, instrumentType, timestamp | Koleksi |
Pesan Error
Anda dapat mem-build indeks ini dengan menjalankan kueri yang diperbarui.
Setiap kueri akan menampilkan pesan error yang berisi link untuk membuat indeks yang diperlukan di Firebase Console.
Firebase CLI
Tambahkan indeks berikut ke file definisi indeks Anda:
{ "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" } ] }, ] }
Deploy definisi indeks Anda yang telah diperbarui:
firebase deploy --only firestore:indexes
Memahami batas penulisan untuk kolom terindeks berurutan
Batas tingkat penulisan untuk kolom terindeks berurutan berasal dari cara Cloud Firestore menyimpan nilai indeks dan menskalakan penulisan indeks. Untuk setiap penulisan indeks, Cloud Firestore mendefinisikan entri key-value yang menggabungkan nama dokumen dan nilai setiap kolom terindeks. Cloud Firestore mengatur entri indeks ini ke dalam kelompok data yang disebut tablet. Setiap server Cloud Firestore menampung satu tablet atau lebih. Saat beban penulisan ke tablet tertentu menjadi terlalu tinggi, Cloud Firestore akan melakukan penskalaan secara horizontal dengan memecah tablet tersebut menjadi tablet-tablet yang lebih kecil dan menyebarkan tablet-tablet baru tersebut ke berbagai server Cloud Firestore.
Cloud Firestore menempatkan entri indeks yang mirip secara leksikografis pada tablet yang sama. Jika nilai indeks dalam tablet terlalu berdekatan, seperti untuk kolom stempel waktu, Cloud Firestore tidak dapat memecah tablet tersebut menjadi tablet-tablet yang lebih kecil secara efisien. Hal ini menghasilkan hot spot, yaitu satu tablet menerima terlalu banyak traffic, dan operasi baca serta tulis ke hot spot tersebut menjadi lebih lambat.
Dengan sharding kolom stempel waktu, Anda memungkinkan Cloud Firestore untuk secara efisien membagi beban kerja ke beberapa tablet. Meskipun nilai-nilai kolom stempel waktu mungkin tetap berdekatan, gabungan nilai indeks dan shard memberi Cloud Firestore cukup ruang antarentri indeks untuk membagi entri tersebut ke beberapa tablet.
Langkah berikutnya
- Baca praktik terbaik mendesain untuk penskalaan
- Untuk kasus dengan tingkat penulisan yang tinggi ke satu dokumen, lihat Penghitung yang terdistribusi
- Lihat batas standar untuk Cloud Firestore