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

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

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

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

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

コンポーネントの概要

次の図は、Cloud Firestore API リクエストに関連するコンポーネントの概要を示しています。

コンポーネントの概要

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

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

Google Front End(GFE)

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

Cloud Firestore サービス

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

Cloud Firestore ストレージ レイヤ

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

キー範囲とスプリット

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

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

同期レプリケーション

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

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

データ レイアウト

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

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

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

データ レイアウト

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Restaurant コレクション

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

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

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

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

Cloud 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 されたことを Cloud Firestore のストレージ クライアントに応答します。同時に、コーディネーターとすべての参加者がデータにミューテーションを適用します。

commit のライフサイクル

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

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

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

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

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

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

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

  1. インデックス テーブルに対する単一範囲のスキャン
  2. 以前のスキャンの結果に基づくドキュメント テーブルのポイント検索
特定のクエリ(IN クエリなど)では、Cloud Firestore で必要な処理が多くなる、または少なる場合があります。

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

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

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

強力な読み込み

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

単一スプリット読み取り

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

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

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

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

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

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

ステイル読み取り

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

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

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

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

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

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

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

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

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

トラブルシューティング

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

次のステップ