Cloud Firestore のベスト プラクティス

ここで紹介するベスト プラクティスは、Cloud Firestore を使用するアプリケーションを構築する際のクイック リファレンスとしてご利用ください。

データベースのロケーション

データベース インスタンスを作成するときは、ユーザーとコンピューティング リソースに最も近いデータベースのロケーションを選択してください。広範囲に及ぶネットワーク ホップはエラーが発生しやすく、クエリのレイテンシを増加させます。

アプリケーションの可用性と耐久性を最大化するには、マルチリージョン ロケーションを選択し、重要なコンピューティング リソースを少なくとも 2 つのリージョンに配置します。

アプリケーションでレイテンシが重要な場合や、他の GCP リソースとのコロケーションが必要な場合は、費用や書き込みレイテンシを抑えるためにリージョン ロケーションを選択します。

ドキュメント ID

  • ドキュメント ID に ... は使用しないでください。
  • ドキュメント ID に /(スラッシュ)は使用しないでください。
  • 次のように、単調に増加するドキュメント ID を使用しないでください。

    • Customer1, Customer2, Customer3, ...
    • Product 1, Product 2, Product 3, ...

    このように連続した ID を使用すると、レイテンシに影響を与えるホットスポットが生じる可能性があります。

フィールド名

  • 追加のエスケープが必要になるため、フィールド名に次の文字は使用しないでください。

    • ピリオド(.
    • 左角かっこ([
    • 右角かっこ(]
    • アスタリスク(*
    • バッククォート(`

インデックス

  • インデックスを過剰に使用しないでください。インデックスの数が多すぎると、書き込みレイテンシが増加し、インデックス エントリのストレージ コストが増加する可能性があります。 インデックス作成を必要としない多くのフィールドを持つドキュメントについては、コレクション レベルのインデックス除外を検討してください。

  • 読み取りレートや書き込みレートが高いアプリケーションの場合、タイムスタンプのように単調に増加する値を持つフィールドにインデックスを作成すると、レイテンシに影響を与えるホットスポットが生じる可能性があります。

インデックスの除外

ほとんどのアプリでは、インデックスを自動的に作成し、エラー メッセージのリンクでインデックスを管理できます。ただし、次のようなケースでは、単一フィールド除外を追加したほうがよい場合があります。

ケース 説明
大きな文字列フィールド

文字列フィールドに長い文字列値を保持することが多く、その文字列値がクエリには使用されない場合、インデックス作成からそのフィールドを除外することでストレージの費用を削減できます。

コレクションへの書き込みレートが高く、連続した値を持つドキュメントが含まれる

コレクション内のドキュメント間で順次に増加または減少するフィールド(タイムスタンプなど)のインデックスを作成する場合は、コレクションへの最大書き込みレートは 500 回/秒です。連続した値を持つフィールドに基づいてクエリを実行することがない場合は、そのフィールドをインデックス作成から除外すれば、この上限を回避できます。

たとえば、書き込みレートが高い IoT のユースケースでは、タイムスタンプ フィールドを持つドキュメントを含むコレクションは、500 回/秒の書き込み上限に近づく可能性があります。

TTL フィールド

TTL(有効期間)ポリシーを使用する場合は、TTL フィールドをタイムスタンプにする必要があります。TTL フィールドのインデックス登録はデフォルトで有効になっており、トラフィック レートが高くなるとパフォーマンスに影響する可能性があります。TTL フィールドには、単一フィールド除外を追加することをおすすめします。

大規模な配列フィールドまたはマップ フィールド

大規模な配列フィールドまたはマップ フィールドの場合、インデックス エントリの上限となっている、ドキュメントあたり 40,000 件に近づく可能性があります。大規模な配列フィールドまたはマップ フィールドに基づいてクエリを実行することがない場合は、そのフィールドをインデックス作成から除外してください。

読み取り / 書き込みオペレーション

  • アプリが単一ドキュメントを更新できる正確な最大レートは、ワークロードによって大きく異なります。詳しくは、同一ドキュメントに対する更新をご覧ください。

  • 可能な場合は、同期呼び出しではなく非同期呼び出しを使用します。非同期呼び出しでは、レイテンシの影響が最小限に抑えられます。たとえば、ドキュメントの検索結果とクエリの結果に従ってレスポンスをレンダリングするアプリケーションについて考えてみましょう。検索とクエリにデータの依存関係がない場合、検索が完了するまでクエリの開始を同期的に待機する必要はありません。

  • オフセットは使用しないでください。その代わりにカーソルを使用します。オフセットを使用すると、スキップされたドキュメントがアプリケーションに返されなくなりますが、内部ではスキップされたドキュメントも引き続き取得されています。スキップされたドキュメントはクエリのレイテンシに影響し、このようなドキュメントの取得に必要な読み取りオペレーションは課金対象になります。

トランザクションの再試行

Cloud Firestore の SDK とクライアント ライブラリは、一時的なエラーに対処するために、失敗したトランザクションを自動的に再試行します。アプリケーションが SDK を使用せずに REST API または RPC API を介して直接 Cloud Firestore にアクセスする場合は、信頼性を高めるために、アプリケーションによるトランザクションの再試行を実装してください。

リアルタイム アップデート

リアルタイム アップデートに関連するベスト プラクティスについては、大規模なリアルタイム クエリを理解するをご覧ください。

スケールを考慮して設計する

ここでは、競合の発生を防ぐためのベスト プラクティスについて説明します。

単一ドキュメントに対する更新

アプリを設計する際は、アプリが単一ドキュメントを更新するのに要する時間を考慮してください。ワークロードのパフォーマンスを判断する最良の方法は、負荷テストを実行することです。アプリが単一ドキュメントを更新できる正確な最大レートは、ワークロードによって大きく異なります。このような要素には、書き込みレート、リクエスト間の競合、影響を受けるインデックスの数などがあります。

ドキュメントの書き込みオペレーションでは、ドキュメントとその関連インデックスを更新します。Cloud Firestore によって、レプリカのクォーラムに書き込みオペレーションが同期的に適用されます。書き込みレートが高いと、データベースで競合、高レイテンシ、その他のエラーが発生するようになります。

狭いドキュメント範囲に対する高頻度の読み取り、書き込み、削除

辞書順で近い一連のドキュメントに対して、高頻度で読み取りや書き込みを行わないでください。この問題はホットスポットといいます。次のいずれかを行うと、アプリケーションにホットスポットが生じる可能性があります。

  • 非常に高い頻度で新しいドキュメントを作成し、単調に増加する ID を割り当てる。

    Cloud Firestore では、散布アルゴリズムでドキュメント ID が割り当てられます。新しいドキュメントのドキュメント ID を自動的に割り当てている場合、書き込み時にホットスポットは生じません。

  • ドキュメント数が少ないコレクションで、新しいドキュメントを頻繁に作成する。

  • タイムスタンプのように単調に増加するフィールドを持つ新しいドキュメントを非常に高い頻度で作成する。

  • コレクション内のドキュメントを高頻度で削除する。

  • トラフィックを徐々に増やすことなく、高頻度でデータベースに書き込みを行う。

削除されたデータをスキップするようなクエリを避ける

最近削除されたデータをスキップするようなクエリの使用を避けてください。前のクエリの結果が直前に削除された場合、その後のクエリで多数のインデックス エントリをスキップしなければならない場合があります。

大量の削除データをスキップしなければならない可能性があるワークロードの例としては、キューに含まれている最も古い作業アイテムを見つけるという操作が挙げられます。そのようなクエリの例を以下に示します。

docs = db.collection('WorkItems').order_by('created').limit(100)
delete_batch = db.batch()
for doc in docs.stream():
  finish_work(doc)
  delete_batch.delete(doc.reference)
delete_batch.commit()

このクエリを実行するたびに、直前に削除されたドキュメントであるかどうかを調べるために、created フィールドのインデックス エントリをスキャンします。これにより、クエリの処理速度が低下します。

パフォーマンスを向上させるには、start_at メソッドを使用して開始するのに適した場所を見つけます。次に例を示します。

completed_items = db.collection('CompletionStats').document('all stats').get()
docs = db.collection('WorkItems').start_at(
    {'created': completed_items.get('last_completed')}).order_by(
        'created').limit(100)
delete_batch = db.batch()
last_completed = None
for doc in docs.stream():
  finish_work(doc)
  delete_batch.delete(doc.reference)
  last_completed = doc.get('created')

if last_completed:
  delete_batch.update(completed_items.reference,
                      {'last_completed': last_completed})
  delete_batch.commit()

注: 上記の例では単調増加するフィールドを使用していますが、これは書き込みレートが高い場合にはアンチパターンです。

トラフィックを徐々に増やす

Cloud Firestore がトラフィックの増加に合わせてドキュメントを準備できるように、新しいコレクションまたは辞書順で近いドキュメントに対するトラフィックを徐々に増やしていく必要があります。新しいコレクションに対するオペレーションは、毎秒 500 回を上限とし、その後、5 分ごとにトラフィックを 50% 増やしていくことをおすすめします。書き込みトラフィックも同様に増やすことができますが、Cloud Firestore の標準アカウントには上限があります。オペレーションがキー範囲全体に比較的均等に分散するよう注意してください。これは「500/50/5」ルールといいます。

新しいコレクションへのトラフィックの移行

アプリのトラフィックをコレクション間で移行する場合、段階的な増加は特に重要になります。この移行を簡単に処理するには、まず古いコレクションから読み取りを行い、ドキュメントが存在しない場合に新しいコレクションから読み取りを行います。ただし、新しいコレクション内で辞書的に近いドキュメントのトラフィックが急増する可能性があります。Cloud Firestore では、トラフィックの増加に合わせて新しいコレクションを効率的に準備できない可能性があります(特にドキュメントが少ない場合)。

同じコレクション内で多くのドキュメントの ID を変更した場合も、同様の問題が発生する可能性があります。

トラフィックを新しいコレクションに移行する最善の方法はデータモデルによって異なります。以下では「同時読み込み」という方法について説明します。この方法が実際のデータに対して効果的かどうかは、ご自身で判断する必要があります。また、移行中の同時操作によるコスト増も考慮する必要があります。

同時読み取り

トラフィックを新しいコレクションに移行するときに同時読み取りを行うには、まず古いコレクションから読み取りを行います。ドキュメントが見つからない場合は、新しいコレクションから読み取ります。存在しないドキュメントを高頻度で読み取るとホットスポットが生じる可能性があるため、新しいコレクションへの負荷を徐々に増やしていく必要があります。より良い方法としては、古いドキュメントを新しいコレクションにコピーしてから古いドキュメントを削除する方法があります。Cloud Firestore が新しいコレクションへのトラフィックを確実に処理できるように、同時読み取りを徐々に増やしていきます。

また、新しいコレクションへの読み取りや書き込みを徐々に増やす方法として、ユーザー ID の決定論的ハッシュを使用して、新しいドキュメントに書き込みを行うユーザーの割合をランダムに選択する方法も考えられます。この場合、ユーザー ID ハッシュの結果が関数やユーザーの行動によって偏らないようにする必要があります。

古いドキュメントから新しいコレクションにすべてのデータをコピーするバッチジョブを実行する方法もあります。ホットスポットを防ぐため、バッチジョブでは連続したドキュメント ID への書き込みを避ける必要があります。バッチジョブが終了すると、新しいコレクションからのみ読み取りが可能になります。

この方法を改善してみましょう。まず、一度に移行するユーザーを小さいバッチにまとめます。そのユーザーの移行ステータスを追跡するフィールドをユーザー ドキュメントに追加します。ユーザー ID のハッシュに基づいて、移行するユーザーのバッチを選択します。このユーザーのバッチのドキュメントを移行するバッチジョブを使用して、移行の途中でユーザーの同時読み取りを行います。

ロールバックは簡単にはできません。ロールバックを行うには、移行段階が完了するまで新旧両方のエンティティに二重に書き込む必要があります。ただし、これを行うと Cloud Firestore の使用料が増加します。

プライバシー

  • Cloud のプロジェクト ID には機密情報を含めないでください。Cloud のプロジェクト ID は、プロジェクトの有効期間を超えて存続する場合があります。
  • データ コンプライアンスのベスト プラクティスとして、ドキュメント名とドキュメントのフィールド名には機密情報を保存しないことをおすすめします。

不正アクセスを防止する

Cloud Firestore セキュリティ ルールを使用して、データベースに対する不正オペレーションを防止します。たとえば、ルールを使用することによって、悪意のあるユーザーがデータベース全体を繰り返しダウンロードする行為を防止できます。

詳しくは、Cloud Firestore セキュリティ ルールの使用をご覧ください。