大規模な読み取りと書き込みについて

このドキュメントでは、優れたパフォーマンスと高い信頼性を備えたアプリケーションを設計する際に役立つ情報を提供します。このドキュメントでは、Firestore の高度なトピックについて説明します。Firestore を使い始めたばかりの方はクイックスタート ガイドをご覧ください。

Firestore は、Firebase と Google Cloud からのモバイル デバイス、ウェブ、サーバー開発に対応した、柔軟でスケーラブルなデータベースです。Firestore の使い方は非常に簡単で、豊富な機能を備えたアプリケーションの作成も簡単です。

データベースのサイズとトラフィックが増加してもアプリケーションのパフォーマンスが維持されるようにするには、Firestore バックエンドでの読み取りと書き込みの仕組みを理解する必要があります。また、読み取り / 書き込みでのストレージ レイヤとのやり取りや、パフォーマンスに影響を与える可能性がある基本的な制約についても理解する必要があります。

アプリケーションを設計する前に、以降のセクションでベスト プラクティスをご確認ください。

コンポーネントの概要

次の図は、Firestore API リクエストに関連するハイレベル コンポーネントを示しています。

コンポーネントの概要

Firestore SDK とクライアント ライブラリ

Firestore は、さまざまなプラットフォームの SDK とクライアント ライブラリをサポートしています。アプリは Firestore API に対して直接 HTTP 呼び出しと RPC 呼び出しを実行できますが、クライアント ライブラリを使用すると、抽象化レイヤが提供され、API の使用が簡単になり、ベスト プラクティスを実装できます。また、オフライン アクセス、キャッシュなどの追加機能を利用することもできます。

Google Front End(GFE)

Google Front End(GFE)は、すべての Google Cloud サービスに共通のインフラストラクチャ サービスです。GFE は受信リクエストを受け入れ、適切な Google サービス(ここでは Firestore サービス)に転送します。また、サービス拒否攻撃からの保護など、その他の重要な機能も提供します。

Firestore サービス

Firestore サービスは、認証、認可、割り当てチェック、セキュリティ ルールなど、API リクエストに対するチェックを実行し、トランザクションを管理します。この Firestore サービスには、データ読み取りと書き込みのためにストレージ レイヤとやり取りするストレージ クライアントが含まれています。

Firestore ストレージ レイヤ

Firestore ストレージ レイヤは、データとメタデータの保存や、Firestore から提供される関連するデータベース機能を処理します。以降のセクションでは、Firestore ストレージ レイヤでデータがどのように編成され、システムがスケーリングされるかについて説明します。データの編成方法を知ることで、スケーラブルなデータモデルを設計し、Firestore のベスト プラクティスをより深く理解できるようになります。

キー範囲とスプリット

Firestore は NoSQL ドキュメント指向データベースです。データはドキュメントに格納され、ドキュメントはコレクションの階層で編成されます。コレクションの階層とドキュメント ID は、ドキュメントごとに単一のキーに変換されます。ドキュメントは論理的に保存され、この単一キーによって名前順に並べられます。「キー範囲」という用語は、辞書順で連続するキーの範囲を指します。

一般的な Firestore データベースは大きすぎるため、1 台の物理マシンには収まりません。また、データのワークロードが 1 台のマシンで処理できないほど大きくなる場合もあります。大規模なワークロードを処理するために、Firestore はデータを複数のマシンに分割し、保存して複数のマシンまたはストレージ サーバーから提供します。これらのパーティションは、データベース テーブル上にスプリットと呼ばれるキー範囲のブロックで作成されます。

同期レプリケーション

データベースは常に自動かつ同期的に複製されます。ゾーンがアクセス不能になった場合でもデータを利用できるように、スプリットのレプリカが異なるゾーンに存在しています。スプリットのレプリカ間で一貫したレプリケーションを実現するため、レプリケーションは Paxos のコンセンサス アルゴリズムによって管理されています。各スプリットで 1 つのレプリカが Paxos リーダーとして選択され、そのレプリカがスプリットへの書き込みを処理します。同期レプリケーションにより、Firestore から常に最新バージョンのデータを読み取ることができます。

その結果、スケーラビリティと可用性が高くなり、負荷の高いワークロードや非常に大規模な環境間で読み取りと書き込みのレイテンシを抑えることができます。

データ レイアウト

Firestore はスキーマレスのドキュメント データベースです。ただし、内部的には、ストレージ レイヤの次の 2 つのリレーショナル データベース スタイルのテーブルにデータがレイアウトされます。

  • ドキュメント テーブル: このテーブルにはドキュメントが保存されます。
  • インデックス テーブル: 結果を効率的に取得し、インデックス値で並べ替えることができるインデックス エントリがこのテーブルに保存されます。

次の図は、Firestore データベースのテーブルがどのように分割されているのかを示しています。スプリットは 3 つの異なるゾーンに複製され、各スプリットには Paxos リーダーが割り当てられています。

データ レイアウト

シングル リージョンとマルチリージョン

データベースを作成する場合、リージョンまたはマルチリージョンを選択する必要があります。

シングル リージョンのロケーションは、us-west1 などの特定の地理的なロケーションです。前述のように、Firestore データベースのスプリットには、選択したリージョン内の異なるゾーンにレプリカが存在します。

マルチリージョン ロケーションは定義済みのリージョンのセットで構成され、それら複数のリージョンにデータベースのレプリカが保存されます。Firestore のマルチリージョン デプロイでは、2 つのリージョンにデータベースのデータ全体の完全なレプリカが存在します。3 番目のリージョンには、完全なデータセットは維持されず、レプリケーションに参加する監視レプリカが存在します。複数のリージョン間でデータを複製することで、1 つのリージョン全体が失われてもデータの書き込みと読み取りを行うことができます。

リージョンのロケーションの詳細については、Firestore のロケーションをご覧ください。

シングル リージョンとマルチリージョン

Firestore での書き込みのライフサイクルについて

Firestore クライアントは、単一のドキュメントを作成、更新、削除することでデータを書き込むことができます。単一ドキュメントへの書き込みでは、ストレージ レイヤでそのドキュメントとそれに関連付けられたインデックス エントリの両方をアトミックに更新する必要があります。Firestore は、複数のドキュメントに対する複数の読み取り / 書き込みで構成されるアトミック オペレーションもサポートしています。

Firestore は、あらゆる種類の書き込みに対してリレーショナル データベースの ACID 特性(アトミック性、整合性、独立性、永続性)を備えています。Firestore は直列化可能性も備えています。これは、すべてのトランザクションが順次実行されているかのように見えることを意味します。

書き込みトランザクションの概要

Firestore クライアントが前述のいずれかの方法でトランザクションの書き込みまたは commit を行うと、この処理はデータベースの読み取り / 書き込みトランザクションとしてストレージ レイヤで実行されます。このトランザクションにより、Firestore は前述の ACID 特性を提供しています。

トランザクションの最初のステップとして、Firestore は既存のドキュメントを読み取り、Documents テーブルのデータに行うミューテーションを決定します。

また、次のようにインデックス テーブルに必要な更新を行います。

  • ドキュメントにフィールドを追加する場合には、インデックス テーブルで対応する挿入を行います。
  • ドキュメントからフィールドを削除する場合は、インデックス テーブルで対応する削除を行います。
  • ドキュメント内でフィールドを変更する場合は、インテックス テーブルでの削除(古い値)と挿入(新しい値)の両方を行います。

前述のミューテーションを計算するために、Firestore はプロジェクトのインデックス構成を読み取ります。インデックス構成には、プロジェクトのインデックスに関する情報が保存されています。Firestore では、単一フィールド インデックスと複合インデックスという 2 種類のインデックスを使用します。Firestore で作成されるインデックスの詳細については、Firestore のインデックスの種類をご覧ください。

ミューテーションが計算されると、Firestore はトランザクション内でミューテーションを収集し、commit します。

ストレージ レイヤでの書き込みトランザクションについて

前述のように、Firestore に書き込みを行うと、ストレージ レイヤで読み取り / 書き込みトランザクションが実行されます。データ レイアウトで説明したように、データのレイアウトによっては、書き込みに 1 つ以上のスプリットが関係します。

次の図では、Firestore データベースに 8 つのスプリット(1 ~ 8 のマーク)が 1 つのゾーンの 3 つの異なるストレージ サーバーでホストされています。また、各スプリットは 3 つ以上のゾーンで複製されています。各スプリットには Paxos リーダーがあり、リーダーは他のスプリットと異なるゾーンに存在しています。

Firestore データベースのスプリット

次のような Restaurants コレクションを含む Firestore データベースを考えてみましょう。

Restaurant コレクション

Firestore クライアントは、priceCategory フィールドの値を更新して、Restaurant コレクション内のドキュメントに対する次の変更をリクエストします。

コレクション内のドキュメントへの変更

書き込みの大まかな流れは次のとおりです。

  1. 読み取り / 書き込みトランザクションを作成します。
  2. ストレージ レイヤのドキュメント テーブルで Restaurants コレクションの restaurant1 ドキュメントを読み込みます。
  3. インデックス テーブルからドキュメントのインデックスを読み取ります。
  4. データに対して行われるミューテーションを計算します。この場合、5 つのミューテーションがあります。
    • M1: ドキュメント テーブルの restaurant1 の行を更新して、priceCategory フィールドの値の変更を反映します。
    • M2 と M3: 降順および昇順インデックスのインデックス テーブルで priceCategory の古い値の行を削除します。
    • M4 と M5: 降順および昇順インデックスのインデックス テーブルに新しい値 priceCategory の行を挿入します。
  5. これらのミューテーションを commit します。

Firestore サービスのストレージ クライアントは、変更される行のキーを所有しているスプリットを検索します。スプリット 3 が M1 に、スプリット 6 が M2 から M5 にサービスを提供する場合について考えてみましょう。分散トランザクションがあり、これらのスプリットはすべて参加者として関係しています。参加者スプリットには、読み取り / 書き込みトランザクションの一部として、先にデータが読み取られたスプリットも含まれる場合があります。

この commit の流れは次のとおりです。

  1. ストレージ クライアントが commit を発行します。commit にはミューテーション M1~M5 が含まれています。
  2. スプリット 3 とスプリット 6 がこのトランザクションの参加者です。参加者の 1 つ(スプリット 3 など)がコーディネーターとして選択されます。コーディネーターは、すべての参加者の間でトランザクションがアトミックに commit または中止されるように調整します。
    • これらのスプリットのリーダー レプリカは参加者とコーディネーターが行う処理を管理します。
  3. 各参加者とコーディネーターは、それぞれのレプリカで Paxos アルゴリズムを実行します。
    • リーダーは、レプリカで Paxos アルゴリズムを実行します。レプリカのほとんどがリーダーに ok to commit レスポンスを返すとクォーラムが達成されます。
    • 各参加者は、準備ができるとコーディネーターに通知します(2 フェーズ commit の第 1 フェーズ)。トランザクションを commit できない参加者がいる場合は、トランザクション全体が aborts 状態になります。
  4. コーディネーターが、自身を含むすべての参加者で準備が完了していることを確認すると、トランザクションの結果として参加者に accept を通知します(2 フェーズ commit の第 2 フェーズ)。このフェーズで、各参加者が commit の決定を安定したストレージに記録し、トランザクションが commit されます。
  5. コーディネーターは、トランザクションが commit されたことを Firestore のストレージ クライアントに応答します。同時に、コーディネーターとすべての参加者がデータにミューテーションを適用します。

commit のライフサイクル

Firestore データベースが小さい場合、1 つのスプリットがミューテーション M1~M5 のすべてのキーを所有していることがあります。その場合、トランザクションに参加するのは 1 つだけであり、前述の 2 フェーズでの commit は不要になり、書き込み時間が短縮されます。

マルチリージョンでの書き込み

マルチリージョン デプロイでは、レプリカを複数のリージョンに分散させると可用性が向上しますが、パフォーマンス コストが発生します。異なるリージョンのレプリカ間の通信はラウンドトリップ時間が長くなります。このため、Firestore オペレーションのベースライン レイテンシは、シングル リージョン デプロイよりも若干高くなります。

スプリットのリーダーが常にプライマリ リージョンに存在するようにレプリカが構成されています。プライマリ リージョンは、Firestore サーバーがトラフィックを受信するリージョンです。このようにリーダーを決定することで、Firestore のストレージ クライアントとレプリカリーダー(またはマルチスプリット トランザクションのコーディネーター)間の通信でのラウンドトリップの遅延が減少します。

Firestore の書き込みには、Firestore のリアルタイム エンジンとの連携も含まれます。リアルタイム クエリの詳細については、大規模なリアルタイム クエリについてをご覧ください。

Firestore での読み取りのライフサイクルについて

このセクションでは、Firestore でのスタンドアロンの非リアルタイム読み取りについて説明します。内部的には、Firestore サーバーはこれらのクエリのほとんどを次の 2 つのステージで処理します。

  1. インデックス テーブルに対する単一範囲のスキャン
  2. 以前のスキャンの結果に基づくドキュメント テーブルのポイント検索
特定のクエリ(Datastore モードのキーのみのクエリなど)やより多くの処理を必要とするクエリ( IN クエリなど)を Firestore で実行できます。

ストレージ レイヤからのデータ読み取りは、整合性のある読み取りを確保するために、データベース トランザクションを使用して内部で行われます。ただし、書き込みに使用されるトランザクションとは異なり、これらのトランザクションはロックされません。その代わりに、トランザクションはタイムスタンプを選択して、そのタイムスタンプですべての読み込みを行います。ロックを行わないため、スナップショット トランザクションは同時読み書きトランザクションをブロックしません。このトランザクションを実行するために、Firestore のストレージ クライアントはタイムスタンプの範囲を指定し、ストレージ レイヤに読み取りタイムスタンプの選択方法を通知します。Firestore でストレージ クライアントによって選択されるタイムスタンプ範囲の種類は、読み取りリクエストの読み取りオプションによって決まります。

ストレージ レイヤでの読み取りトランザクションについて

このセクションでは、読み取りの種類と、Firestore のストレージ レイヤでの読み取り方法について説明します。

強力な読み込み

デフォルトでは、Firestore の読み取りは強整合性を持ちます。強整合性とは、Firestore の読み取りが、読み取りの開始時までに commit されたすべての書き込みを反映した最新バージョンのデータを返すということです。

単一スプリット読み取り

Firestore のストレージ クライアントは、読み取る行のキーを所有しているスプリットを検索します。前のセクションのスプリット 3 からの読み取りを行う場合について考えてみましょう。クライアントは、ラウンドトリップ レイテンシを短縮するため、読み取りリクエストを最も近いレプリカに送信します。

この時点で、選択されたレプリカに応じて次のようなケースが考えられます。

  • 読み取りリクエストがリーダー レプリカ(ゾーン A)に送信される。
    • リーダーは常に最新の状態になっているため、読み取りがすぐに実行されます。
  • 読み取りリクエストがリーダー以外のレプリカ(ゾーン B など)に送信される。
    • スプリット 3 が、内部状態からそのスプリットに読み取りの実行に十分な情報があることを認識できた場合は、スプリットから読み取りを行います。
    • スプリット 3 が最新のデータが存在することを認識できなかった場合は、リーダーにメッセージを送信して、読み取りの実行に必要な最新のトランザクションのタイムスタンプを取得します。トランザクションが適用されると、読み取りが実行されます。

Firestore がクライアントにレスポンスを返します。

マルチスプリット読み取り

複数のスプリットから読み取りを行う状況では、すべてのスプリットで同じメカニズムが発生します。すべてのスプリットからデータが返されると、Firestore のストレージ クライアントは結果を結合します。Firestore はこのデータを使用してクライアントにレスポンスを返します。

ステイル読み取り

強力な読み取りは、Firestore のデフォルト モードです。ただし、リーダーとの通信が必要になるため、レイテンシが増加する可能性があります。多くの場合、Firestore アプリケーションは最新バージョンのデータを読み取る必要はなく、数秒前のデータでも問題なく機能します。

そのような場合、クライアントは read_time 読み取りオプションを使用して、ステイル読み取りを選択することもできます。この場合、read_time のデータが読み取られます。また、最も近いレプリカが、指定された read_time にデータが存在していることをすでに確認している可能性が非常に高くなります。パフォーマンスを著しく向上させるには、ステイルネスの値として 15 秒を使用することが妥当です。ステイル読み取りでも、生成される行の整合性は維持されます。

ホットスポットを回避する

Firestore のスプリットは、必要に応じて自動的に分割され、トラフィックをより多くのストレージ サーバーに配信したり、キースペースが拡張されたりするときにトラフィックを分散します。過剰なトラフィックの処理のために作成されたスプリットが、トラフィックがなくなっても約 24 時間ほど保持されます。そのため、トラフィックの急増が繰り返し発生する場合、スプリットは維持され、必要に応じて追加されます。このメカニズムにより、Firestore データベースは、トラフィック負荷またはデータベース サイズの増加に応じて自動スケーリングできます。ただし、以下で説明するように、いくつかの制限があります。

ストレージと負荷の分割に時間がかかり、トラフィックが急増すると、サービスの調整中に高レイテンシや期限超過エラー(一般的にはホットスポット)が発生する可能性があります。ベスト プラクティスは、オペレーション数が 1 秒あたり 500 となるデータベース上でコレクションへのトラフィックが増加している間に、キー範囲全体にオペレーションを分散させ、その後で 5 分ごとに 50% までトラフィックを増加させることです。このプロセスは 500/50/5 ルールと呼ばれ、ワークロードに合わせたデータベースの最適なスケーリングを可能にします。

スプリットは負荷が増加すると自動的に作成されますが、Firestore は、複製された専用のストレージ サーバーのセットを使用して単一ドキュメントを提供するまで、キー範囲を分割できます。結果として、単一ドキュメント上で同時実行するオペレーションの量が高いままで維持され、そのドキュメントでホットスポットが発生する場合があります。単一ドキュメントで高レイテンシが持続されるような場合は、複数のドキュメントにデータを分割または複製するようなデータモデルへの修正を検討しましょう。

競合エラーは、複数のオペレーションで同じドキュメントを同時に読み書きしようとした場合に発生します。

また、Firestore のドキュメント ID として連続的に増加 / 減少するキーが使用されていて、1 秒あたりのオペレーション数が非常に多い場合にも、ホットスポット化が発生します。急増したトラフィックは新しく作成されたスプリットに移動するだけで済むため、スプリットの数を増やしても意味がありません。デフォルトでは、Firestore はドキュメント内のすべてのフィールドに自動的にインデックスを作成するため、タイムスタンプのように連続的に増加または減少する値を含むドキュメント フィールドのインデックス スペースでもホットスポットの移動が発生する可能性があります。

前述の方法に従うことによって、Firestore は構成を調整しなくても、任意の大きなワークロードに合わせてスケーリングできます。

トラブルシューティング

Firestore では、使用状況のパターン分析とホットスポット化の問題のトラブルシューティング用に設計された診断ツール Key Visualizer を利用できます。

次のステップ