大規模なリアルタイム クエリについて理解する

このドキュメントでは、1 秒あたり数千のオペレーション、または数十万の同時実行ユーザーを超えるサーバーレス アプリをスケーリングするためのガイダンスについて説明します。このドキュメントでは、システムを詳細に理解するために役立つ高度なトピックについて説明します。Firestore を使い始めたばかりの方はクイックスタート ガイドをご覧ください。

Firestore と Firebase モバイル / Web SDK は、クライアントサイドのコードがデータベースに直接アクセスする、サーバーレス アプリを開発するための強力なモデルを提供します。SDK を使用すると、クライアントはデータの更新をリアルタイムでリッスンできます。リアルタイムの更新により、サーバー インフラストラクチャを必要としないレスポンシブ アプリを構築できます。すぐに使い始めることもできますが、Firestore を構成するシステムの制約を理解しておくと、トラフィックが増加したときにサーバーレス アプリのスケーリングとパフォーマンスの向上を図ることができます。

アプリのスケーリングに関するアドバイスについては、以降のセクションをご覧ください。

データベースの場所にユーザーに近いロケーションを選択する

次の図は、リアルタイム アプリのアーキテクチャを示しています。

リアルタイム アプリ アーキテクチャの例

ユーザーのデバイスで実行されているアプリ(モバイルまたはウェブ)が Firestore との接続を確立すると、接続はデータベースが配置されているリージョンにある Firestore フロントエンド サーバーにルーティングされます。たとえば、データベースが us-east1 にある場合、us-east1 の Firestore フロントエンドにもルーティングされます。この接続は長期間持続し、アプリによって明示的に閉じられるまで開いた状態を維持します。フロントエンドは、基盤となる Firestore ストレージ システムからデータを読み取ります。

ユーザーの場所から Firestore データベースまでの物理的な距離は、ユーザーが感じるレイテンシに影響します。たとえば、インドのユーザーが、北米の Google Cloud リージョンにあるデータベースと通信すると、データベースが近い場所にあるよりもエクスペリエンスが遅くなり、アプリの速度が遅くなる可能性があります(インドなどアジアでのみ)。

信頼性を考慮して設計する

以下のトピックでは、アプリの信頼性の改善または向上について説明します。

オフライン モードを有効にする

Firebase SDK はオフライン データの永続性に対応しています。ユーザーのデバイス上のアプリは、Firestore に接続できない場合でも、ローカルのキャッシュに保存されているデータで処理を継続できます。これにより、インターネット接続が不安定だったり、数時間または数日間完全にアクセスできなくても、データにアクセスすることができます。オフライン モードの詳細については、オフライン データを有効にするをご覧ください。

自動再試行について理解する

Firebase SDK は、オペレーションの再試行や切断された接続の再確立を処理します。これにより、サーバーの再起動や、クライアントとデータベース間のネットワークの問題に起因する一時的なエラーを回避できます。

リージョン ロケーションかマルチリージョン ロケーションか

リージョン ロケーションとマルチリージョン ロケーションには、いくつかのトレードオフがあります。主な違いはデータを複製する方法です。これはアプリの可用性を保証するものです。マルチリージョン インスタンスの場合、サービス提供の信頼性が高く、データの耐久性も向上しますが、その分コストがかかります。

リアルタイム クエリ システムについて理解する

リアルタイム クエリはスナップショット リスナーとも呼ばれます。これにより、アプリはデータベースの変更をリッスンし、データが変更されると低レイテンシの通知を受け取ります。データベースに対して定期的に更新をポーリングしても同じ結果を得ることができますが、この方法では速度が遅く、コストがかかります。また、多くのコードが必要になります。リアルタイム クエリを設定して使用する方法の例については、リアルタイムの更新を取得するをご覧ください。以降のセクションでは、スナップショット リスナーの仕組みについて詳しく説明します。また、パフォーマンスを維持しながらリアルタイム クエリをスケーリングするためのベスト プラクティスについても説明します。

2 人のユーザーがモバイル SDK で構築されたメッセージ アプリを介して Firestore に接続する場合について考えてみましょう。

クライアント A はデータベースに書き込みを実行し、chatroom というコレクション内にドキュメントを追加して更新します。

collection chatroom:
    document message1:
      from: 'Sparky'
      message: 'Welcome to Firestore!'

    document message2:
      from: 'Santa'
      message: 'Presents are coming'

クライアント B は、スナップショット リスナーを使用して同じコレクションの更新をリッスンします。新しいメッセージが作成されると、クライアント B はすぐに通知を受け取ります。次の図は、スナップショット リスナーの背後にあるアーキテクチャを示しています。

スナップショット リスナー接続のアーキテクチャ

クライアント B がスナップショット リスナーをデータベースに接続すると、次の一連のイベントが発生します。

  1. クライアント B が Firebase との接続を開き、Firebase SDK を介して onSnapshot(collection("chatroom")) を呼び出し、リスナーを登録します。このリスナーは数時間アクティブな状態になっている場合があります。
  2. Firestore フロントエンドが基盤となるストレージ システムにクエリを実行し、データセットをブートストラップします。一致するドキュメントの結果セット全体を読み込みます。これをポーリング クエリと呼びます。データベースの Firebase セキュリティ ルールを評価し、このデータにユーザーがアクセスできることを確認します。ユーザーが認証されると、データベースからユーザーにデータが返されます。
  3. その後、クライアント B のクエリはリッスンモードに移行します。リスナーがサブスクリプション ハンドラに登録され、データが更新されるまで待機します。
  4. これで、クライアント A がドキュメントを変更する書き込みオペレーションを送信できるようになりました。
  5. データベースで、ドキュメントの変更がストレージ システムに commit されます。
  6. トランザクションでは、同じ更新が内部の変更ログに commit されます。変更ログでは、変更の発生順序が厳密に処理され、
  7. 更新されたデータがサブスクリプション ハンドラのプールにファンアウトされます。
  8. リバースクエリ マッチャーが実行され、更新されたドキュメントが現在登録されているスナップショット リスナーと一致しているかどうか確認されます。この例では、ドキュメントはクライアント B のスナップショット リスナーと一致しています。リバース クエリ マッチャーは通常のデータベース クエリですが、その名のとおり処理が逆になります。ドキュメント内でクエリと一致するものを見つけるのではなく、受信したドキュメントと一致するものをクエリで効率的に検索します。一致が見つかると、該当するドキュメントをスナップショット リスナーに転送します。次に、データベースの Firebase セキュリティ ルールを評価し、許可されたユーザーのみがデータを受信していることを確認します。
  9. ドキュメントの更新がクライアント B のデバイス上の SDK に転送され、onSnapshot コールバックが呼び出されます。ローカル永続性が有効になっている場合、SDK はローカル キャッシュにも更新を適用します。

Firestore の拡張性の重要な部分は、変更ログからサブスクリプション ハンドラおよびフロントエンド サーバーへのファンアウトに依存しています。ファンアウトにより、単一のデータ変更を効率的に伝播し、数百万ものリアルタイム クエリや接続されたユーザーにサービスを提供することが可能になります。Firestore は、これらのコンポーネントのすべてのレプリカを複数のゾーン(マルチリージョン デプロイの場合は複数のリージョン)で実行し、高可用性と拡張性を実現しています。

モバイル SDK と Web SDK から発行されるすべての読み取りオペレーションは、このモデルに準拠しています。整合性を維持するため、ポーリング クエリの後にリッスンモードに切り替わります。これは、リアルタイム リスナー、ドキュメント取得の呼び出し、ワンショット クエリの場合も同様です。1 つのドキュメントの取得とワンショット クエリは、パフォーマンスに関して同様の制約がある短期的なスナップショット リスナーと考えることができます。

リアルタイム クエリ スケーリングのベスト プラクティスを適用する

スケーラブルなリアルタイム クエリを設計するには、次のベスト プラクティスを適用します。

システム内での書き込みトラフィックの増加を把握する

このセクションは、増加する書き込みリクエストに対してシステムがどのように対応しているのかを理解するのに役立ちます。

リアルタイム クエリで使用される Firestore の変更ログは、書き込みトラフィックが増加すると自動的に水平方向にスケーリングされます。データベースの書き込みレートが単一サーバーの処理能力を超えて増加すると、変更ログが複数のサーバーに分割され、クエリ処理で複数のサブスクリプション ハンドラからのデータが使用されるようになります。クライアントと SDK から見ると、この処理は完全に透過的であり、分割の発生時にアプリからアクションを実行する必要はありません。次の図は、リアルタイム クエリがどのようにスケーリングされるかを示しています。

変更ログのファンアウトのアーキテクチャ

自動スケーリングにより、書き込みトラフィックを制限なく増やすことができますが、トラフィックが増加すると、システムの応答が遅くなる可能性があります。書き込みのホットスポットが発生しないように、5-5-5 ルールの推奨事項に従ってください。Key Visualizer は、書き込みのホットスポットを分析するための有用なツールです。

多くのアプリは有機的に成長する可能性を備えていますが、Firestore は予防措置を講じることなく、これに対応できます。ただし、大規模なデータセットをインポートするバッチ ワークロードなどでは、書き込み速度が速すぎる可能性があります。アプリを設計する際は、書き込みトラフィックの発生元を常に意識する必要があります。

書き込みと読み取りの関係について理解する

リアルタイム クエリ システムは、書き込みオペレーションをリーダーに接続するパイプラインと考えることができます。ドキュメントの作成、更新、削除が行われるたびに、ストレージ システムから現在登録されているリスナーに変更が伝播されます。Firestore の変更ログの構造では強整合性が保証されます。つまり、データベースがデータを変更した場合と異なり、アプリに順不同の更新の通知は送信されません。これにより、データの整合性に関連するエッジケースがなくなり、アプリ開発をシンプルに進めることができます。

この接続されたパイプラインでは、ホットスポットやロック競合を引き起こす書き込みオペレーションによって、読み取りオペレーションに悪影響が生じる可能性があります。書き込みオペレーションが失敗したり、スロットリングが発生すると、変更ログから一貫したデータが取得できるまで、読み取りオペレーションが待機することがあります。アプリでこのエラーが発生すると、書き込みオペレーションが遅くなり、クエリのレスポンス時間が遅くなる可能性があります。ホットスポットの回避がこの問題を解決する鍵となります。

ドキュメントと書き込みオペレーションを小さくする

通常、スナップショット リスナーを使用するアプリでは、データの変更をユーザーに迅速に知らせる必要があります。これを実現するため、すべてを小さくするようにしてください。フィールド数が数十個の小さなドキュメントは非常に迅速に push されますが、数百のフィールドや大量のデータを使用する大規模なドキュメントでは処理に時間がかかります。

同様に、レイテンシを低く抑えるため、commit と書き込みオペレーションを短期間で迅速に実行します。ライターから見ると、バッチサイズが大きいほうがスループットの向上につながる可能性がありますが、スナップショット リスナーから通知を受け取るまでの時間は長くなります。これは、パフォーマンス向上のためにバッチ処理を行う場合がある他のデータベース システムとは逆に感じるかもしれません。

効率的なリスナーを使用する

データベースの書き込みレートが増加すると、Firestore はデータ処理を多くのサーバーに分割します。Firestore のシャーディング アルゴリズムでは、同じコレクションまたはコレクション グループ内のデータを同じサーバーに配置しようとします。システムは、クエリ処理に関与するサーバーの数を最小限に抑えながら、書き込みスループットを最大にしようとします。

ただし、特定のパターンでは、依然としてスナップショット リスナーに最適でない処理が行われている可能性があります。たとえば、アプリがほとんどのデータを 1 つの大規模なコレクションに保存している場合、必要なすべてのデータを受信するために、リスナーが多くのサーバーに接続しなければならないことがあります。これは、クエリフィルタを適用しても変わりません。多くのサーバーに接続すると、レスポンスが遅くなるリスクが高くなります。

このようなレスポンスの遅さを回避するには、システムがさまざまなサーバーを経由せずにリスナーにサービスを提供できるように、スキーマとアプリを設計してください。書き込みレートの小さい、小さなコレクションにデータを分割することをおすすめします。

これは、テーブル全体のスキャンが必要なリレーショナル データベースでパフォーマンス クエリを考える場合に似ています。リレーショナル データベースでテーブル全体のスキャンが必要なクエリは、チャーンレートが高いコレクションを監視するスナップショット リスナーに相当します。具体的なインデックスを使用して処理できるクエリと比較して、実行速度が遅くなる可能性があります。より具体的なインデックスを使用するクエリは、単一ドキュメントを参照するスナップショット リスナーや、変更頻度の低いコレクションに似ています。アプリの負荷をテストして、ユースケースの動作と必要な対応を詳細に把握する必要があります。

ポーリング クエリを高速化する

レスポンシブ リアルタイム クエリでは、データをブートストラップするポーリング クエリを高速化し、効率的にすることも重要です。新しいスナップショット リスナーが初めて接続したとき、リスナーは結果セット全体を読み込み、ユーザーのデバイスに送信する必要があります。クエリが遅いと、アプリの応答性が低下します。これには、多くのドキュメントを読み取るクエリや、適切なインデックスを使用しないクエリなどが含まれます。

状況によっては、リスナーがリスニング状態からポーリング状態に戻ることもあります。この処理は自動的に行われ、SDK とアプリから透過的です。ポーリング状態のトリガーは以下の条件で発生することがあります。

  • 負荷の変化に合わせてシステムが変更ログを再調整した場合。
  • ホットスポットにより、データベースへの書き込みが失敗したり、遅延した場合。
  • 一時的なサーバーの再起動が一時的にリスナーに影響を与えた場合。

ポーリング クエリが十分に高速であれば、アプリのユーザーがポーリング状態を意識することはありません。

リスナーの存続期間を長くする

Firestore を使用するアプリを構築するときに、多くの場合、リスナーを開いて可能な限り長く稼働させるのが最も費用対効果の高い方法となります。Firestore を使用する場合、開いている接続を維持するためではなく、アプリに返されたドキュメントに対して料金が発生します。存続期間の長いスナップショット リスナーは、存続期間中にクエリを提供するのに必要なデータのみを読み取ります。これには、最初のポーリング オペレーションと、その後にデータが実際に変更されたときの通知が含まれます。一方、ワンショット クエリは、アプリが最後にクエリを実行してから変更されていないデータも再度読み取る可能性があります。

大量のデータを消費するアプリでは、スナップショット リスナーは適切でない可能性があります。たとえば、接続を介して 1 秒間に多くのドキュメントを push するユースケースの場合、低い頻度で実行されるワンショット クエリのほうが良いかもしれません。

次のステップ