リアルタイムのクエリを大規模に把握

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

Firestore と Firebase モバイル/ウェブ SDK は、クライアント側のコードがデータベースに直接アクセスするサーバーレス アプリを開発するための強力なモデルを提供します。SDK を使用すると、クライアントはデータの更新をリアルタイムでリッスンできます。リアルタイム アップデートを使用して、サーバー インフラストラクチャを必要としないレスポンシブ アプリを構築できます。コンテナ化したプロジェクトは簡単に実行できますが、Firestore を構成するシステムの制約を理解することで、トラフィックの増加に合わせてサーバーレス アプリのスケーリングとパフォーマンスの向上を実現できます。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

いずれかのモバイル SDK を使用してビルドされたメッセージング アプリから Firestore に接続する 2 人のユーザーを想像してみてください。

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

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

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

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

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

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

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

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

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

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

多くのアプリは、予測可能な有機的成長を予測できます。Firestore は予防策を講じることなく対処できます。ただし、大規模なデータセットのインポートなどのバッチ ワークロードは、書き込みを短時間で増やすことができます。アプリを設計する際は、書き込みトラフィックの発生元に注意してください。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

有効期間が長いリスナーを優先する

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

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

次のステップ