大規模な読み書きについて

このドキュメントをお読みいただき、パフォーマンスと信頼性を高めるアプリケーション設計について、十分な情報を得てから決定してください。このドキュメントでは、高度な Firestore トピックについて説明します。Firestore を使い始めたばかりの方は、クイックスタート ガイドをご覧ください。

Firestore は、Firebase と Google Cloud のモバイル デバイス、ウェブ、サーバー開発に対応した、柔軟でスケーラブルなデータベースです。Firestore を使い始めて、豊富で強力なアプリケーションを作成するのは非常に簡単です。

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

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

コンポーネントの概要を理解する

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

コンポーネントの概要

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

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

Google Front End(GFE)

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

Firestore サービス

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

Firestore ストレージ レイヤ

Firestore のストレージ レイヤは、データとメタデータの両方、および Firestore が提供する関連データベース機能を保存します。以下のセクションでは、Firestore ストレージ レイヤでデータを整理する方法と、システムのスケーリング方法について説明します。データの編成方法について学習することで、スケーラブルなデータモデルの設計を行い、Firestore のベスト プラクティスをより深く理解できます。

キー範囲と分割

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

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

同期レプリケーション

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

その結果、スケーラブルで可用性が高いシステムで、負荷の高いワークロードや非常に大規模なスケールに関係なく、読み取りと書き込みのレイテンシが低くなります。

Data Layout

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

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

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

データ レイアウト

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

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

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

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

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

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

Firestore における書き込みのライフサイクルを理解する

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

すべての種類の書き込みに対して、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 データベースについて考えてみましょう。

レストラン コレクション

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 データベースが小さい場合、単一のスプリットがミューテーション M1 ~ M5 のすべてのキーを所有している可能性があります。このような場合、トランザクションの参加者は 1 人のみであり、前述の 2 フェーズ commit は不要であるため、書き込みを高速化できます。

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

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

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

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

Firestore の読み取りのライフサイクルを理解する

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

  1. インデックス テーブルに対する単一範囲のスキャン
  2. 前のスキャンの結果に基づいた Documents テーブルのポイント検索
特定のクエリ(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 は、複製された専用のストレージ サーバーのセットを使用して単一ドキュメントを提供するまで、キー範囲を分割できます。そのため、1 つのドキュメントで大量のオペレーションが継続的に実行される場合、そのドキュメントのホットスポットが発生することがあります。単一ドキュメントで高レイテンシが持続されるような場合は、複数のドキュメントにデータを分割または複製するようなデータモデルへの修正を検討しましょう。

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

ホットスポット化のもう 1 つの特殊なケースは、Firestore でドキュメント ID として順次増加または減少するキーが使用されており、1 秒あたりのオペレーションの数が非常に多い場合です。トラフィックが急増しても新しく作成されたスプリットに移動するだけで、より多くのスプリットを作成しても、ここでは役立ちません。Firestore はデフォルトでドキュメント内のすべてのフィールドを自動的にインデックスに登録するため、このような移動ホットスポットは、タイムスタンプのように順次増減する値を含むドキュメント フィールドのインデックス スペースにも作成されます。

上記のプラクティスに従うことで、Firestore は任意の大規模なワークロードを処理できるようにスケーリングでき、構成を調整する必要はありません。

トラブルシューティング

Firestore には、使用パターンの分析とホットスポット化の問題をトラブルシューティングするための専用の診断ツールとして Key Visualizer が用意されています。

次のステップ