Dấu thời gian phân đoạn

Nếu một bộ sưu tập chứa các tài liệu có giá trị được lập chỉ mục tuần tự, Cloud Firestore 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 mảnh một trường tài liệu để vượt qua giới hạn này. Trước tiên, hãy xác định ý nghĩa của "các trường được lập chỉ mục tuần tự" và làm rõ thời điểm áp dụng giới hạn này.

Các trường được lập chỉ mục tuần tự

"Các trường được lập chỉ mục tuần tự" có nghĩa là mọi bộ sưu tập tài liệu chứa một trường được lập chỉ mục tăng hoặc giảm đơn điệu. Trong nhiều trường hợp, điều này có nghĩa là trường timestamp, nhưng mọi giá trị trường tăng hoặc giảm đơ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 bộ sưu tậ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 tất cả các trường timestamp đều kích hoạt giới hạn này. Nếu một 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 của trường cũng không quan trọng, chỉ cần trường đó tăng hoặc giảm đơn điệu. Ví dụ: cả hai tập hợp giá trị trường tăng đơn điệu sau đây đều kích hoạt giới hạn ghi:

  • 100000, 100001, 100002, 100003, ...
  • 0, 1, 2, 3, ...

Phân mảnh trường dấu thời gian

Giả sử ứng dụng của bạn sử dụng trường timestamp tăng đơn điệu. 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 bạn cần trường timestamp cho các truy vấn, bạn có thể khắc phục giới hạn này bằng cách sử dụng dấu thời gian được phân mảnh:

  1. Thêm trường shard bên cạnh trường timestamp. Sử dụng các giá trị riêng biệt 1..n cho trường shard. Điều này làm tăng giới hạn ghi cho bộ sưu tập lên 500*n, nhưng bạn phải tổng hợp n truy vấn.
  2. Cập nhật logic ghi để gán ngẫu nhiên giá trị shard cho mỗi tài liệu.
  3. Cập nhật các truy vấn để tổng hợp các tập kết quả được phân mảnh.
  4. Tắt chỉ mục một trường cho cả trường shard và trường timestamp. Xoá các chỉ mục tổng hợp hiện có chứa trường timestamp.
  5. Tạo 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 một chỉ mục rất quan trọng và trường shard phải xuất hiện trước trường timestamp. Mọi chỉ mục bao gồm trường timestamp cũng phải bao gồm trường shard.

Bạn chỉ nên triển khai dấu thời gian được phân mảnh trong các trường hợp sử dụng có tốc độ ghi duy trì 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 mảnh trường timestamp sẽ xoá giới hạn 500 lượt ghi mỗi giây nhưng phải đánh đổi bằng việc cần 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 mảnh trường timestamp và cách truy vấn một tập kết quả được phân mảnh.

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 các công cụ tài chính gần như theo thời gian thực như tiền tệ, cổ phiếu phổ thông và quỹ ETF. Ứng dụng này ghi tài liệu vào 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 sau và sắp xếp 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, bạn xác định rằng ứng dụng sẽ nhận được từ 1.000 đến 1.500 lượt 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 cho 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 công suất 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 mảnh x, yz. 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 mảnh

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 bộ sưu tậ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 được phân mảnh

Việc thêm trường shard yêu cầu bạn phải cập nhật các truy vấn để tổng hợp kết quả được phân mảnh:

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á ràng buộc 500 lượt ghi mỗi giây, hãy xoá các chỉ mục một trường và chỉ mục tổng hợp 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 của Firebase

  1. Trong bảng điều khiển của Firebase, hãy chuyển đến thẻ Databases & Storage > Firestore > Composite Indexes (Cơ sở dữ liệu và bộ nhớ > Firestore > Chỉ mục tổng hợp).

    Chuyển đến Chỉ mục tổng hợp

  2. Đố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

  1. Trong bảng điều khiển Cloud, hãy chuyển đến trang Databases (Cơ sở dữ liệu).

    Chuyển đến Cơ sở dữ liệu

  2. Chọn cơ sở dữ liệu bắt buộc trong danh sách cơ sở dữ liệu.

  3. Trong trình đơn điều hướng, hãy nhấp vào Indexes (Chỉ mục), rồi nhấp vào thẻ Composite (Tổng hợp).

  4. Sử dụng trường Filter để tìm các định nghĩa chỉ mục chứa trường timestamp.

  5. Đối với mỗi 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

  1. Nếu bạn chưa thiết lập Giao diện dòng lệnh (CLI) của Firebase, hãy làm theo hướng dẫn này để cài đặt CLI và chạy lệnh firebase initcommand. Trong lệnh init, hãy nhớ chọn Firestore: Deploy rules and create indexes for Firestore (Firestore: Triển khai quy tắc và tạo chỉ mục cho Firestore).
  2. 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.
  3. 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"
          }
        ]
      },
     ]
    }
    
  4. Triển khai các định nghĩa chỉ mục đã cập nhật:

    firebase deploy --only firestore:indexes
    

Cập nhật định nghĩa chỉ mục một trường

Bảng điều khiển của Firebase

  1. Trong bảng điều khiển của Firebase, hãy chuyển đến thẻ Databases & Storage > Firestore > Single Field Indexes (Cơ sở dữ liệu và bộ nhớ > Firestore > Chỉ mục một trường).

    Chuyển đến Chỉ mục một trường

  2. Nhấp vào Thêm trường miễn trừ.

  3. Đố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ập timestamp.

  4. Trong phần Phạm vi truy vấn, hãy chọn cả Bộ sưu tậpNhóm bộ sưu tập.

  5. Nhấp vào Tiếp theo

  6. Chuyển tất cả chế độ cài đặt chỉ mục thành Đã tắt. Nhấp vào Lưu.

  7. Lặp lại các bước tương tự cho trường shard.

Bảng điều khiển Google Cloud Platform

  1. Trong bảng điều khiển Cloud, hãy chuyển đến trang Databases (Cơ sở dữ liệu).

    Chuyển đến Cơ sở dữ liệu

  2. Chọn cơ sở dữ liệu bắt buộc trong danh sách cơ sở dữ liệu.

  3. Trong trình đơn điều hướng, hãy nhấp vào Indexes (Chỉ mục), rồi nhấp vào thẻ Single Field (Một trường).

  4. Nhấp vào thẻ Single Field (Một trường).

  5. Nhấp vào Thêm trường miễn trừ.

  6. Đố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ập timestamp.

  7. Trong phần Phạm vi truy vấn, hãy chọn cả Bộ sưu tậpNhóm bộ sưu tập.

  8. Nhấp vào Tiếp theo

  9. Chuyển tất cả chế độ cài đặt chỉ mục thành Đã tắt. Nhấp vào Lưu.

  10. 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

  1. 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": []
       },
     ]
    }
    
  2. Triển khai các định nghĩa chỉ mục đã 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:

Bộ sưu tập Các trường được lập chỉ mục Phạm vi truy vấn
instruments shard, price.currency, timestamp Bộ sưu tập
instruments shard, exchange, timestamp Bộ sưu tập
instruments shard, instrumentType, timestamp Bộ sưu tậ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 có đường liên kết để tạo chỉ mục bắt buộc trong Bảng điều khiển của Firebase.

Giao diện dòng lệnh (CLI) của Firebase

  1. 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"
             }
           ]
         },
       ]
     }
    
  2. Triển khai các định nghĩa chỉ mục đã cập nhật:

    firebase deploy --only firestore:indexes
    

Tìm hiểu về giới hạn ghi đối với các trường được lập chỉ mục tuần tự

Giới hạn về tốc độ ghi đối với các trường được lập chỉ mục tuần tự xuất phát 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ượt ghi chỉ mục, Cloud Firestore sẽ xác định một mục nhập khoá-giá trị nối tên tài liệu và giá trị của từng trường được lập chỉ mục. Cloud Firestore sắ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à tablet. Mỗi Cloud Firestore máy chủ chứa một hoặc nhiều tablet. Khi tải ghi vào một tablet cụ thể trở nên quá cao, Cloud Firestore sẽ mở rộng theo chiều ngang bằng cách chia tablet thành các tablet nhỏ hơn và phân tán các tablet 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ự từ vựng trên cùng một tablet. Nếu các giá trị chỉ mục trong một tablet quá gần nhau, chẳng hạn như đối với các trường dấu thời gian, Cloud Firestore không thể chia tablet thành các tablet nhỏ hơn một cách hiệu quả. Điều này tạo ra một điểm nóng nơi một tablet nhận được quá nhiều lưu lượng truy cập và các thao tác đọc và ghi vào điểm nóng trở nên chậm hơn.

Bằng cách phân mảnh trường dấu thời gian, bạn có thể giúp Cloud Firestore phân chia khối lượng công việc một cách hiệu quả trên nhiều tablet. Mặc dù các giá trị của trường dấu thời gian có thể vẫn gần nhau, giá trị phân mảnh và chỉ mục được nối sẽ cung cấp cho Cloud Firestore đủ khoảng cách giữa các mục nhập chỉ mục để chia các mục nhập giữa nhiều tablet.

Bước tiếp theo