ベスト プラクティス

ここで紹介するベスト プラクティスは、Cloud Datastore を使用するアプリケーションを構築する際に留意すべき点のクイック リファレンスとして使用できます。このページは Cloud Datastore の基本的な使い方を説明するものではないため、Cloud Datastore を使い始める際の出発点としてはおすすめしません。新規ユーザーの方は、Cloud Datastore のスタートガイドをご覧ください。

全般

  • 名前空間名、種類名、プロパティ名、カスタムキー名には常に UTF-8 文字を使用します。これらの名前で非 UTF-8 文字を使用すると、Cloud Datastore の機能に影響する場合があります。たとえばプロパティ名で非 UTF-8 文字を使用すると、そのプロパティを使用するインデックスを作成できない場合があります。
  • 種類名またはカスタムキー名でスラッシュ(/)を使用しないでください。これらの名前でスラッシュを使用すると、将来リリースされる機能に影響する可能性があります。
  • Cloud のプロジェクト ID には機密情報を含めないでください。Cloud のプロジェクト ID は、プロジェクトの有効期間を超えて存続する場合があります。

API 呼び出し

  • 読み取り、書き込み、削除には、単一のオペレーションではなくバッチ オペレーションを使用します。バッチ オペレーションでは、単一オペレーションと同じオーバーヘッドで複数のオペレーションが実行されるため効率的です。
  • トランザクションが失敗した場合はロールバックを試行してください。ロールバックを使用すると、トランザクション内で同じリソースを使用する別のリクエストの再試行レイテンシが最小限に抑えられます。ロールバック自体が失敗する可能性もあるため、ロールバックはベスト エフォート型の試行である点に注意してください。
  • 可能な場合は、同期呼び出しではなく非同期呼び出しを使用します。非同期呼び出しでは、レイテンシの影響が最小限に抑えられます。たとえば、同期 lookup() の結果とクエリの結果に従ってレスポンスをレンダリングするアプリケーションについて考えてみましょう。lookup() とクエリにデータの依存関係がない場合、lookup() が完了するまでクエリの開始を同期的に待機する必要はありません。

エンティティ

  • 関連性の高いデータはエンティティ グループにグループ化します。エンティティ グループを使用すると祖先クエリが可能となり、強整合性のある結果が得られます。エンティティ グループ内のエンティティは Cloud Datastore サーバー上の物理的に近い場所に保存されるため、祖先クエリでは I/O を最小限に抑えて迅速にエンティティ グループをスキャンすることもできます。
  • エンティティ グループへの書き込みは 1 秒間に 1 回を超えないようにします。この制限を超える速度で書き込みを続けると、結果整合性の読み取りはさらに結果的になり、強整合性の読み取りはタイムアウトにつながります。そのためアプリケーションの全体的なパフォーマンスが低下します。エンティティ グループに対するバッチ処理またはトランザクション処理による書き込みは、この制限に対して 1 回の書き込みとしてカウントされます。
  • 1 回の commit に同じエンティティ(キーが同一のエンティティ)を複数回含めないでください。1 回の commit に同じエンティティを複数回含めると、Cloud Datastore のレイテンシに影響する場合があります。

キー

  • エンティティ作成時にキー名が指定されていない場合、キー名は自動生成されます。これらは、キースペースに均等に分散するように割り当てられます。
  • カスタム名を使用するキーでは、常にスラッシュ(/)を除く UTF-8 文字を使用します。非 UTF-8 文字は、Google BigQuery への Cloud Datastore バックアップのインポートなど、さまざまな処理に影響します。また、スラッシュは将来リリースされる機能に影響する可能性があります。
  • 数値 ID を使用するキーの場合:
    • ID には負の数値を使用しないでください。負の ID は並べ替えに影響する場合があります。
    • ID には値 0(ゼロ)を使用しないでください。使用すると、自動的に ID が割り当てられます。
    • 作成したエンティティに対して独自の数値 ID を手動で割り当てる場合は、アプリケーションで allocateIds() メソッドを使用して ID のブロックを取得します。こうすると、手動の数値 ID が別のエンティティに割り当てられなくなります。
  • 作成したエンティティに独自の手動数値 ID またはカスタム名を割り当てる場合は、次のように単調に増加する値を使用しないでください。

    1, 2, 3, …,
    "Customer1", "Customer2", "Customer3", ….
    "Product 1", "Product 2", "Product 3", ….
    

    アプリケーションが大きなトラフィックを生成する場合、このような連続した番号がホットスポットを生み出し、Cloud Datastore のレイテンシに影響する可能性があります。連続した数値 ID の問題を回避するには、allocateIds() メソッドから数値 ID を取得します。allocateIds() メソッドを使用することで、適度に分散された順序の数値 ID が生成されます。

  • また、キーを指定するか生成された名前を格納することで、エンティティを見つけるためにクエリを発行する必要なしに、後でそのエンティティに対して一貫した lookup() を実行できます。

インデックス

  • クエリがプロパティを必要としない場合は、インデックスからプロパティを除外します。プロパティを不必要にインデックス付けすると一貫性を確保するためのレイテンシが増加するだけでなく、インデックス エントリを保存するコストも増加します。
  • 複合インデックスが多くなりすぎないようにしてください。複合インデックスを過度に使用すると一貫性を確保するためのレイテンシが増加するだけでなく、インデックス エントリを保存するコストも増加します。事前にインデックスを定義せずに大規模なデータセットに対してアドホック クエリを実行する場合は、Google BigQuery を使用します。
  • プロパティにインデックスを付ける際、単調に増加する値(NOW() タイムスタンプなど)を使用しないでください。このようなインデックスを維持するとホットスポットが生じる可能性があり、読み取りレートや書き込みレートが高いアプリケーションでは Cloud Datastore レイテンシに影響します。単調なプロパティの扱いに関する詳細なガイダンスについては、下記の狭いキー範囲への高頻度での読み取りと書き込みをご覧ください。

プロパティ

  • 文字列型のプロパティには常に UTF-8 文字を使用します。文字列型のプロパティで非 UTF-8 文字を使用すると、クエリに影響する場合があります。非 UTF-8 文字でデータを保存する必要がある場合はバイト型文字列を使用します。
  • プロパティ名にはドットを使用しないでください。プロパティ名にドットを使用すると、今後の構造化プロパティのインデックス付けに影響する場合があります。

クエリ

  • クエリ結果のキーだけにアクセスする場合は、キーのみのクエリを使用します。キーのみのクエリでは、エンティティ全体を取得する場合よりも低いレイテンシとコストで結果が返されます。
  • エンティティの特定のプロパティだけにアクセスする場合は、射影クエリを使用します。射影クエリでは、エンティティ全体を取得する場合よりも低いレイテンシとコストで結果が返されます。
  • 同様に、クエリフィルタに含まれるプロパティ(たとえば order by 句でリストされるもの)だけにアクセスする場合も射影クエリを使用します。
  • オフセットは使用しないでください。代わりにカーソルを使用します。オフセットを使用しても、スキップされたエンティティがアプリケーションに返されなくなるだけで、内部的にはそのようなエンティティも引き続き取得されます。スキップされたエンティティはクエリのレイテンシに影響し、そのようなエンティティの取得に必要な読み取りオペレーションに対してアプリケーションに課金されます。
  • クエリに強整合性が必要な場合は、祖先クエリを使用します(祖先クエリを使用するには、まず強整合性に対応するデータ構造にする必要があります)。祖先クエリは強整合性のある結果を返します。キーのみの非祖先クエリに続けて lookup() を実行しても、強整合性のある結果は返されません。これは、キーのみの非祖先クエリはクエリの実行時点で一貫性が保たれていないインデックスから結果を取得する可能性があるためです。

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

単一エンティティ グループに対する更新

Cloud Datastore 内の 1 つのエンティティ グループだけを高い頻度で更新することは避けてください。

Cloud Datastore を使用するときは、1 つのエンティティ グループの更新頻度が毎秒 1 回以上となる必要がないようにアプリケーションを設計することをおすすめします。親も子も持たないエンティティは、それ自身がエンティティ グループとなることに留意してください。エンティティ グループの更新頻度が高すぎる場合は、Cloud Datastore の書き込みレイテンシが大きくなり、タイムアウトやその他のタイプのエラーが発生します。このことを、競合(コンテンション)といいます。

単一エンティティ グループへの Cloud Datastore の書き込み頻度は、毎秒 1 回という制限を超えることもあるため、負荷テストではこの問題が現れない可能性があります。エンティティ グループへの書き込み頻度を下げるようにアプリケーションを設計するための推奨事項は、Cloud Datastore の競合の記事をご覧ください。

狭いキー範囲への高頻度での読み取りと書き込み

辞書順が近い一連の Cloud Datastore キーに対する、高頻度での読み取りや書き込みは避けてください。

Cloud Datastore は、Google の NoSQL データベースである Bigtable の上に構築されており、Bigtable のパフォーマンス特性に左右されます。Bigtable のスケーリングは、それぞれ独立したタブレットに行を振り分けるという方法で行われますが、これらの行はキーで辞書順に並べられます。

Cloud Datastore を使用するときは、ホット タブレットが理由で書き込み速度が低下することがあります。これが起きるのは、小さい範囲の一連のキーに対する書き込み頻度が急に上昇し、1 つのタブレット サーバーの処理能力を超えた場合です。高負荷をサポートするために、最終的に Bigtable はキースペースを分割します。

読み取りの場合の上限は一般的に、書き込みの上限を大きく上回りますが、単一のキーを高頻度で読み取る場合を除きます。Bigtable は、単一のキーを複数のタブレットに分割することはできません。

ホット タブレットは、エンティティ キーとインデックスの両方に使用されるキー範囲に対して発生することがあります。

場合によっては、小さい範囲のキーへの読み取りや書き込みができなくなること以外にも、Cloud Datastore ホットスポットがアプリケーションに影響を及ぼすことがあります。たとえば、ホットキーの読み取りや書き込みがインスタンス起動時に行われると、読み込みリクエストが失敗します。

デフォルトでは、Cloud Datastore によるキーの割り当てには分散アルゴリズムが使用されます。したがって、新しいエンティティを作成するための書き込みが高頻度でも、デフォルトの ID 割り当てポリシーを使用していれば、Cloud Datastore への書き込みでホットスポットが発生することは通常ありません。次のようなケースでは、この問題が発生することがあります。

  • 新しいエンティティの作成をきわめて高頻度で行い、このときに以前の順次 ID 割り当てポリシーを使用する場合。

  • 新しいエンティティの作成をきわめて高頻度で行い、独自の ID を割り当てるが、その ID が単調に増加していく場合。

  • ある種類の新しいエンティティの作成をきわめて高頻度で行うが、その種類の既存のエンティティがごく少数であった場合。Bigtable は、最初はすべてのエンティティを同じタブレット サーバーに配置しますが、ある程度の時間が経過するとキー範囲を複数のタブレット サーバーに分割するようになります。

  • この問題は、新しいエンティティを高頻度で作成するときに、タイムスタンプのような、単調に増加していくインデックス付きプロパティがある場合にも発生します。このようなプロパティは、Bigtable のインデックス テーブル内の行のキーであるためです。

  • Cloud Datastore は、ルート エンティティ グループの名前空間と種類を Bigtable 行キーの前に付加します。新しい名前空間または種類への書き込みを開始するときに、トラフィックを徐々に増やしていかなかった場合は、ホットスポットが発生することがあります。

単調に増加していくキーまたはインデックス付きプロパティがある場合は、ランダムなハッシュを前に付加すると、キーが確実に複数のタブレットに分割されるようになります。

同様に、並べ替えまたはフィルタを使用して単調に増加する(または減少する)プロパティを照会する必要がある場合は、新しいプロパティにインデックスを付けることができます。単調な値の前には、データセット全体のカーディナリティは高いが、実施するクエリの範囲内のすべてのエンティティに共通するような値を付加します。たとえば、タイムスタンプでエントリを照会したいが、一度に 1 人のユーザーの結果だけを返す場合には、タイムスタンプの前にユーザー ID を付加し、その新しいプロパティにインデックスを付けることができます。これにより、そのユーザーに対するクエリを実行し、順序付きの結果を得ることを可能としつつ、ユーザー ID の存在によってインデックス自体が適切にシャーディングされることを保証できます。

この問題の詳しい説明については、Cloud Datastore での単調に増加する値の保存に関する Ikai Lan のブログ投稿をご覧ください。

トラフィックを増やしていく

Cloud Datastore の新しい種類やキースペースの新しい部分へのトラフィックは、徐々に増やしていきます。

Cloud Datastore の新しい種類へのトラフィックを徐々に増やしていくのは、トラフィックの増加に合わせて Bigtable がタブレットを分割できるよう十分な時間を確保するためです。Cloud Datastore の新しい種類に対するオペレーションは毎秒 500 回を上限とし、その後でトラフィックを 5 分ごとに 50% 増やしていくことをおすすめします。理論上は、この増加スケジュールを使用すると 90 分後に毎秒 740,000 回までオペレーションを増やすことができます。書き込みがキー範囲全体に比較的均等に分散するよう注意してください。Google の SRE は、これを「500/50/5」ルールと呼んでいます。

このような徐々に増やしていくパターンが特に重要になるのは、コードを変更した結果として種類 A の使用が停止し、代わりに種類 B が使用されるという場合です。この移行を単純に処理するには、種類 B を読み取り、存在しなければ種類 A を読み取るようにコードを変更します。ただしこの方法では、キースペースのごく小さい部分を使用する新しい種類へのトラフィックが突然増加するおそれがあります。キースペースが疎である場合は、Bigtable が効率的にタブレットを分割できない可能性があります。

これと同じ問題は、エンティティを移行した結果として同じ種類の中の別のキー範囲が使用される場合にも起きることがあります。

どのような方法でエンティティを新しい種類またはキーに移行するかは、データモデルによって異なります。次の例で示しているのは、「同時読み込み」と呼ばれる方法です。この方法が実際のデータに対して効果的かどうかは、ご自身で判断する必要があります。重要な考慮事項の 1 つとして、移行中の並列オペレーションによる費用面での影響があります。

最初に古いエンティティまたはキーから読み取ります。見つからない場合は、新しいエンティティまたはキーから読み取ります。存在しないエンティティを高頻度で読み取ると、ホットスポット発生につながる可能性があるため、負荷を徐々に増やしていくことが必要です。より良い方法としては、古いエンティティを新しいエンティティにコピーしてから古いほうを削除するというものがあります。同時読み込みを徐々に増やしていくと、新しいキースペースが適切に分割されます。

新しい種類への読み取りや書き込みを徐々に増やしていくための方法として考えられるのは、ユーザー ID の決定論的ハッシュを使用して、新しいエンティティに書き込むユーザーの割合をランダムに決めるというものです。ユーザー ID ハッシュの結果が、ランダム関数によっても、ユーザーの行動によっても、偏ることがないようにしてください。

一方で、Dataflow ジョブを実行して、すべてのデータを古いエンティティまたはキーから新しいほうにコピーします。このバッチジョブでは順次キーへの書き込みを避けてください。Bigtable のホットスポットを防ぐためです。バッチジョブが完了すると、読み取りは新しい場所からのみ可能になります。

この方法に改良を加えるとすれば、一度に移行するユーザーを小さいバッチにまとめるというものがあります。ユーザー エンティティにフィールドを追加して、そのユーザーの移行ステータスを記録します。1 つのバッチとして移行するユーザーを選ぶ基準となるのは、ユーザー ID のハッシュです。MapReduce または Dataflow のジョブで、そのバッチのユーザーのキーを移行します。移行進行中のユーザーは、同時読み取りを使用します。

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

削除

小さいキー範囲の大量の Cloud Datastore エンティティを削除することは避けてください。

Bigtable は定期的にテーブルをリライトしますが、その目的は削除済みのエントリを除去することと、読み取りと書き込みの処理効率が向上するようにデータを再編成することです。このプロセスは「コンパクション」と呼ばれます。

大量の Cloud Datastore エンティティを削除するときに、そのキーの範囲が小さい場合は、コンパクションが完了するまでの間、インデックスのこの部分に対するクエリが遅くなります。極端なケースでは、クエリの結果が返される前にタイムアウトする可能性があります。

エンティティの有効期限を表すために、タイムスタンプをインデックス付きフィールドの値として使用するのは、不適切な方法です。有効期限切れのエンティティを取り出すには、このインデックス付きフィールドに対してクエリを実行することになりますが、該当するキースペースは、最近削除されたエンティティのインデックス エントリのスペースと重なっている可能性が高くなります。

「分割クエリ」のパフォーマンスを向上させるには、固定長の文字列を有効期限タイムスタンプの前に付加します。インデックスはこの文字列全体の順に並べられるので、同じタイムスタンプを持つ複数のエンティティがインデックスのキー範囲全体に分散されます。複数のクエリを同時に実行して各シャードから結果を取得することができます。

有効期限タイムスタンプの問題を解決するための、より完成度の高い方法は、定期的に更新されるグローバル カウンタである「世代番号」を使用するというものです。この世代番号を有効期限タイムスタンプの前に付加すると、クエリの結果は世代番号、シャード、タイムスタンプの順に並べ替えられます。古いエンティティの削除は、前の世代で行われます。削除されないエンティティは、その世代番号に 1 が加算されます。削除が完了したら、次の世代に進みます。古い世代に対するクエリのパフォーマンスは、コンパクションが完了するまでの間は低くなります。削除するエンティティのリストを取得するためにインデックスに対するクエリを実行する前に、何世代かの完了を待つことが必要になることがあります。これは、結果整合性が理由で結果が欠落するリスクを軽減するためです。

シャーディングとレプリケーション

Cloud Datastore のホットキーに対しては、シャーディングまたはレプリケーションを使用します。

レプリケーションを使用するのは、キー範囲の一部分の読み取りを、Bigtable の許容限度を超えて高頻度で行う必要がある場合です。この方法を使用すると、同じエンティティのコピーが N 個保存されるので、単一エンティティの場合に比べて読み取り頻度を N 倍にすることができます。

Bigtable の許容限度を超える高頻度でキー範囲の一部に書き込む必要がある場合は、シャーディングを使用できます。シャーディングによって 1 つのエンティティが小さく分割されます。この説明については、カウンタの分割の記事をご覧ください。

シャーディングするときによくある誤りとしては、次のものがあります。

  • シャーディングに時間接頭辞を使用している。時間が次の接頭辞に移ると、新しい未分割の部分がホットスポットになります。代わりに、書き込みの部分を徐々に新しい接頭辞に移すようにしてください。

  • 最もホットなエンティティのみをシャーディングしている。エンティティ総数のうち、小さな割合だけをシャーディングすると、ホットなエンティティの間の行数が十分ではなくなるため別々の分割に維持できなくなる可能性がります。

次のステップ

このページは役立ちましたか?評価をお願いいたします。

フィードバックを送信...

Cloud Datastore ドキュメント