Nếu một tập hợp chứa các tài liệu có giá trị được lập chỉ mục tuần tự, thì Cloud Firestore sẽ giới hạn tốc độ ghi ở mức 500 lượt ghi mỗi giây. Trang này mô tả cách phân đoạn trường tài liệu để khắc phục giới hạn này. Trước tiên, hãy định nghĩa "trường được lập chỉ mục tuần tự" và làm rõ thời điểm giới hạn này áp dụng.
Các trường được lập chỉ mục tuần tự
"Trường được lập chỉ mục tuần tự" có nghĩa là bất kỳ tập hợp tài liệu nào chứa một trường được lập chỉ mục tăng hoặc giảm một cách liên tục. Trong nhiều trường hợp, điều này có nghĩa là trường timestamp
, nhưng bất kỳ giá trị trường nào tăng hoặc giảm một cách đơn điệu đều có thể kích hoạt giới hạn ghi là 500 lượt ghi mỗi giây.
Ví dụ: giới hạn này áp dụng cho một tập hợp tài liệu user
có trường được lập chỉ mục userid
nếu ứng dụng gán các giá trị userid
như sau:
1281, 1282, 1283, 1284, 1285, ...
Mặt khác, không phải trường timestamp
nào cũng kích hoạt giới hạn này. Nếu trường timestamp
theo dõi các giá trị được phân phối ngẫu nhiên, thì giới hạn ghi sẽ không áp dụng. Giá trị thực tế của trường cũng không quan trọng, chỉ cần trường đó tăng hoặc giảm một cách đơn điệu. Ví dụ: cả hai tập hợp giá trị trường tăng dần sau đây đều kích hoạt giới hạn ghi:
100000, 100001, 100002, 100003, ...
0, 1, 2, 3, ...
Phân đoạn trường dấu thời gian
Giả sử ứng dụng của bạn sử dụng trường timestamp
tăng dần.
Nếu ứng dụng của bạn không sử dụng trường timestamp
trong bất kỳ truy vấn nào, bạn có thể xoá giới hạn 500 lượt ghi mỗi giây bằng cách không lập chỉ mục trường dấu thời gian. Nếu cần một trường timestamp
cho các truy vấn, bạn có thể giải quyết giới hạn này bằng cách sử dụng dấu thời gian phân đoạn:
- Thêm trường
shard
cùng với trườngtimestamp
. Sử dụng các giá trị1..n
khác nhau cho trườngshard
. Thao tác này sẽ tăng giới hạn ghi cho bộ sưu tập lên500*n
, nhưng bạn phải tổng hợp các truy vấnn
. - Cập nhật logic ghi để ngẫu nhiên gán giá trị
shard
cho từng tài liệu. - Cập nhật truy vấn để tổng hợp các tập hợp kết quả được phân đoạn.
- Tắt chỉ mục trường đơn cho cả trường
shard
và trườngtimestamp
. Xoá các chỉ mục tổng hợp hiện có chứa trườngtimestamp
. - Tạo các chỉ mục tổng hợp mới để hỗ trợ các truy vấn đã cập nhật. Thứ tự của các trường trong chỉ mục rất quan trọng và trường
shard
phải đứng trước trườngtimestamp
. Mọi chỉ mục có chứa trườngtimestamp
cũng phải chứa trườngshard
.
Bạn chỉ nên triển khai dấu thời gian phân đoạn trong các trường hợp sử dụng có tốc độ ghi ổn định trên 500 lượt ghi mỗi giây. Nếu không, đây là một hoạt động tối ưu hoá sớm. Việc phân đoạn trường timestamp
sẽ xoá giới hạn 500 lượt ghi
mỗi giây, nhưng bù lại, bạn cần phải tổng hợp truy vấn
ở phía máy khách.
Các ví dụ sau đây cho biết cách phân đoạn trường timestamp
và cách truy vấn tập hợp kết quả được phân đoạn.
Mô hình dữ liệu và truy vấn mẫu
Ví dụ: hãy tưởng tượng một ứng dụng phân tích gần như theo thời gian thực các công cụ tài chính như tiền tệ, cổ phiếu phổ thông và quỹ hoán đổi danh mục (ETF). Ứng dụng này ghi tài liệu vào một bộ sưu tập instruments
như sau:
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(); }
Ứng dụng này chạy các truy vấn và đơn đặt hàng sau theo trường 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]); });
Sau khi nghiên cứu một chút, bạn xác định rằng ứng dụng sẽ nhận được từ 1.000 đến 1.500 bản cập nhật công cụ mỗi giây. Điều này vượt quá 500 lượt ghi mỗi giây được phép đối với các bộ sưu tập chứa tài liệu có trường dấu thời gian được lập chỉ mục. Để tăng tốc độ ghi, bạn cần 3 giá trị phân đoạn, MAX_INSTRUMENT_UPDATES/500 = 3
. Ví dụ này sử dụng các giá trị phân đoạn x
, y
và z
. Bạn cũng có thể sử dụng số hoặc các ký tự khác cho giá trị phân mảnh.
Thêm trường phân đoạn
Thêm trường shard
vào tài liệu. Đặt trường shard
thành các giá trị x
, y
hoặc z
để tăng giới hạn ghi trên tập hợp lên 1.500 lượt ghi mỗi giây.
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(); }
Truy vấn dấu thời gian phân đoạn
Khi thêm trường shard
, bạn cần cập nhật truy vấn để tổng hợp kết quả được phân đoạn:
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]); });
Cập nhật định nghĩa chỉ mục
Để xoá quy tắc ràng buộc 500 lượt ghi mỗi giây, hãy xoá các chỉ mục tổng hợp và trường đơn hiện có sử dụng trường timestamp
.
Xoá định nghĩa chỉ mục tổng hợp
Bảng điều khiển Firebase
Mở trang Cloud Firestore Chỉ mục tổng hợp trong bảng điều khiển của Firebase.
Đối với mỗi chỉ mục chứa trường
timestamp
, hãy nhấp vào nút rồi nhấp vào Xoá.
Bảng điều khiển Google Cloud Platform
Trong Google Cloud Console, hãy chuyển đến trang Databases (Cơ sở dữ liệu).
Chọn cơ sở dữ liệu cần thiết trong danh sách cơ sở dữ liệu.
Trong trình đơn điều hướng, hãy nhấp vào Chỉ mục, rồi nhấp vào thẻ Hợp nhất.
Sử dụng trường Filter (Lọc) để tìm kiếm các định nghĩa chỉ mục chứa trường
timestamp
.Đối với từng chỉ mục này, hãy nhấp vào nút
rồi nhấp vào Xoá.
Giao diện dòng lệnh (CLI) của Firebase
- Nếu bạn chưa thiết lập Firebase CLI, hãy làm theo hướng dẫn sau để cài đặt CLI và chạy lệnh
firebase init
. Trong lệnhinit
, hãy nhớ chọnFirestore: Deploy rules and create indexes for Firestore
. - Trong quá trình thiết lập, Giao diện dòng lệnh (CLI) của Firebase sẽ tải các định nghĩa chỉ mục hiện có xuống một tệp có tên là
firestore.indexes.json
theo mặc định. Xoá mọi định nghĩa chỉ mục chứa trường
timestamp
, ví dụ:{ "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" } ] }, ] }
Triển khai các định nghĩa chỉ mục mới cập nhật:
firebase deploy --only firestore:indexes
Cập nhật định nghĩa chỉ mục trường đơn
Bảng điều khiển Firebase
Mở trang Cloud Firestore Chỉ mục trường đơn trong bảng điều khiển của Firebase.
Nhấp vào Thêm trường hợp miễn trừ.
Đối với Mã bộ sưu tập, hãy nhập
instruments
. Đối với Đường dẫn trường, hãy nhậptimestamp
.Trong phần Phạm vi truy vấn, hãy chọn cả Bộ sưu tập và Nhóm bộ sưu tập.
Nhấp vào Tiếp theo
Chuyển tất cả chế độ cài đặt chỉ mục thành Tắt. Nhấp vào Lưu.
Lặp lại các bước tương tự cho trường
shard
.
Bảng điều khiển Google Cloud Platform
Trong Google Cloud Console, hãy chuyển đến trang Databases (Cơ sở dữ liệu).
Chọn cơ sở dữ liệu cần thiết trong danh sách cơ sở dữ liệu.
Trong trình đơn điều hướng, hãy nhấp vào Chỉ mục, rồi nhấp vào thẻ Trường đơn.
Nhấp vào thẻ Trường đơn.
Nhấp vào Thêm trường hợp miễn trừ.
Đối với Mã bộ sưu tập, hãy nhập
instruments
. Đối với Đường dẫn trường, hãy nhậptimestamp
.Trong phần Phạm vi truy vấn, hãy chọn cả Bộ sưu tập và Nhóm bộ sưu tập.
Nhấp vào Tiếp theo
Chuyển tất cả chế độ cài đặt chỉ mục thành Tắt. Nhấp vào Lưu.
Lặp lại các bước tương tự cho trường
shard
.
Giao diện dòng lệnh (CLI) của Firebase
Thêm nội dung sau vào phần
fieldOverrides
của tệp định nghĩa chỉ mục:{ "fieldOverrides": [ // Disable single-field indexing for the timestamp field { "collectionGroup": "instruments", "fieldPath": "timestamp", "indexes": [] }, ] }
Triển khai các định nghĩa chỉ mục mới cập nhật:
firebase deploy --only firestore:indexes
Tạo chỉ mục tổng hợp mới
Sau khi xoá tất cả các chỉ mục trước đó chứa timestamp
, hãy xác định các chỉ mục mới mà ứng dụng của bạn yêu cầu. Mọi chỉ mục chứa trường timestamp
cũng phải chứa trường shard
. Ví dụ: để hỗ trợ các truy vấn ở trên, hãy thêm các chỉ mục sau:
Thu thập | Các trường được lập chỉ mục | Phạm vi truy vấn |
---|---|---|
nhạc cụ | phân đoạn, price.currency, dấu thời gian | Thu thập |
nhạc cụ | Phân đoạn | , trao đổi , dấu thời gianThu thập |
nhạc cụ | mảnh | , instrumentType , dấu thời gianThu thập |
Thông báo lỗi
Bạn có thể tạo các chỉ mục này bằng cách chạy các truy vấn đã cập nhật.
Mỗi truy vấn sẽ trả về một thông báo lỗi kèm theo đường liên kết để tạo chỉ mục bắt buộc trong Bảng điều khiển Firebase.
Giao diện dòng lệnh (CLI) của Firebase
Thêm các chỉ mục sau vào tệp định nghĩa chỉ mục:
{ "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" } ] }, ] }
Triển khai các định nghĩa chỉ mục mới cập nhật:
firebase deploy --only firestore:indexes
Tìm hiểu về giới hạn ghi cho các trường được lập chỉ mục tuần tự
Giới hạn về tốc độ ghi cho các trường được lập chỉ mục tuần tự đến từ cách Cloud Firestore lưu trữ các giá trị chỉ mục và mở rộng quy mô ghi chỉ mục. Đối với mỗi lần ghi chỉ mục, Cloud Firestore xác định một mục khoá-giá trị nối tên tài liệu và giá trị của mỗi trường được lập chỉ mục. Cloud Firestoresắp xếp các mục nhập chỉ mục này thành các nhóm dữ liệu được gọi là thẻ thông tin. Mỗi máy chủ Cloud Firestore chứa một hoặc nhiều máy tính bảng. Khi tải ghi vào một máy tính bảng cụ thể trở nên quá cao, Cloud Firestore sẽ mở rộng theo chiều ngang bằng cách chia máy tính bảng thành các máy tính bảng nhỏ hơn và phân phối các máy tính bảng mới trên nhiều máy chủ Cloud Firestore.
Cloud Firestore đặt các mục nhập chỉ mục gần nhau theo thứ tự bảng chữ cái trên cùng một máy tính bảng. Nếu các giá trị chỉ mục trong một bảng quá gần nhau, chẳng hạn như đối với các trường dấu thời gian, thì Cloud Firestore không thể phân tách hiệu quả bảng đó thành các bảng nhỏ hơn. Điều này tạo ra một điểm nóng, trong đó một máy tính bảng nhận quá nhiều lưu lượng truy cập, đồng thời các thao tác đọc và ghi vào điểm nóng sẽ chậm hơn.
Bằng cách phân đoạn trường dấu thời gian, bạn có thể cho phép Cloud Firestore phân chia hiệu quả khối lượng công việc trên nhiều máy tính bảng. Mặc dù các giá trị của trường dấu thời gian có thể vẫn gần nhau, nhưng giá trị chỉ mục và mảnh được nối lại sẽ cung cấp cho Cloud Firestore đủ không gian giữa các mục chỉ mục để phân tách các mục đó giữa nhiều máy tính bảng.
Bước tiếp theo
- Đọc các phương pháp hay nhất để thiết kế cho quy mô lớn
- Đối với các trường hợp có tốc độ ghi cao vào một tài liệu, hãy xem phần Bộ đếm phân tán
- Xem các giới hạn tiêu chuẩn cho Cloud Firestore