2017年11月17日金曜日

Datastore と組み込みインデックス

GoogleCloudFlatform(GoogleAppEngine)のNoSQLデータベースである、Datastoreを使用していると、奇妙なことに気づくと思います。それは、想定していた保存データ量に比べて、課金時に算出されたデータ量が大幅に多いという点です。ここで、Datastoreの料金を示してみます。

※ 米国/ヨーロッパ - Cloud Datastore 料金と割り当て

1 日あたりの
無料利用枠
無料利用枠を
超えた場合の料金(単価)
料金
保存データ 1 GB ストレージ $0.18 GB/月
エンティティの読み込み数 50,000 $0.06 /10 万エンティティ
エンティティの書き込み数 20,000 $0.18 /10 万エンティティ
エンティティの削除数 20,000 $0.02 /10 万エンティティ
小規模な操作 無制限 無料 -

料金的には読み込み数や書き込み数の方が通常は大きいと思いますが、いつの間にか保存データが何十GB・何百GBとなり、課金されている場合があります。

保存データが増大する要因
アプリケーションの設計時に、事前にデータ量を推定していると思います。しかし何十・何百GBと、想定外のデータが増えるのでしょうか?。 一例を挙げてみましょう。

あるエンティティのデータ使用状況(データストア・ダッシュボード)です。全体で80MBを使用していますが、エンティティ(実データ)は、10MBしかありません。残り70MBを使用しているのが、組み込みインデックスと呼ばれるデータです。

GoogleCloudPlatformのドキュメントには、次のように記述されています。

インデックスは次の 2 種類に分けられます。
組み込みインデックス
デフォルトでは、エンティティの種類ごとに各プロパティのインデックスが自動的に事前定義されます。このような単一プロパティのインデックスは単純な種類のクエリに適しています。
複合インデックス
複合インデックスでは、インデックス付けされたエンティティごとに複数のプロパティ値がインデックス付けされます。複雑なクエリをサポートする複合インデックスは、インデックス設定ファイルindex.yaml)で定義されます。
参考:インデックス

組み込みインデックスは複合インデックスとは違い、自動で生成されるインデックスです。しかし、実データの7倍ものデータ(このデータの場合)を生成するのも問題ですね。

組み込みインデックスを生成しない

組み込みインデックスを生成しないためには、エンティティ定義時にプロパティのパラメータとして、indexed=False を設定すればよいです。

You declare a property unindexed by setting indexed=False in the property constructor:

class Person(db.Model):
  name
= db.StringProperty()
  age
= db.IntegerProperty(indexed=False)

You can later change the property back to indexed by calling the constructor again with indexed=True:

class Person(db.Model):
  name
= db.StringProperty()
  age
= db.IntegerProperty(indexed=True)
参考: Cloud Datastore Indexes

NDBでモデル定義する場合は、次のようにプロパティのパラメータとして記述します。indexed=Falseを記述しない場合は、indexed=Trueの見做され、インデックスが生成されます。

class Person(db.Model):
  name = db.StringProperty()
  age = db.IntegerProperty(indexed=False)

pydal(web2pyフレームワーク)で記述する場合は、フィールドのcustom_qualifierパラメータに辞書形式で指定します。指定しない場合は、インデックスが生成されます。

db.define_table('person',
    Field('name'),
    Field('age', 'integer', custom_qualifier={'indexed':False}))
組み込みインデックス設定の注意点

組み込みインデックスを生成しないという indexed=False は、どのプロパティ(フィールド)に対して設定してよいという訳ではありません。
組み込みインデックスは、クエリのフィルタ(条件)や単純なソートで必要です。このためアプリケーションで、該当するプロパティがクエリのフィルタやソートに使用していないか確認する必要があります。もしインデックスが無いのにアプリケーションで使用すると、次のようなエラーがログに記録されます。

BadFilterError: invalid filter: Cannot query for unindexed property xxxxxxx.

複合インデックスの場合、index.yamlをデプロイすれば自動にインデックスを再構築します。しかし組み込みインデックスの場合、indexed=False と変更しデプロイしても、インデックスが削除される訳では無いようです。
組み込みインデックスを削除するには、モデルの既存のエンティティ全ての更新処理が必要です。
このため、インデックス削除による保存データ課金額のコスト削減効果と、全エンティティの更新処理でのコスト(インスタンス・エンティティ読み込み・エンティティ書き込みの各課金額)を比較し吟味する必要があります。

最初にサンプルとして示したエンティティですが、組み込みインデックスが不必要なプロパティに indexed=False と設定し、更に全エンティティの更新処理をした結果、次のように変化しました。

1・2・4番目のプロパティの組み込みインデックスがゼロになっています。3番めのプロパティは検索条件に使用しているため、indexed=True になりインデックスはそのままです。これでも保存データは6割削減されました。

複合インデックスも整理

複合インデックスでも、不要なインデックスをチェックしましょう。
index.yaml の記述を見て、使用していないインデックスを記述から除外します。その後、index.yaml をデプロイします。
appcfgコマンドの時代は、アプリをデプロイすると自動的に index.yaml も更新されましたが、gcloudでは自動更新されません。次のように index.yaml をデプロイするためのコマンドを発行する必要があります。

gcloud app deploy index.yaml

その後、index.yaml から除外されたインデックスをアプリからクリーンアップする必要があります。コマンドは次の通りです。

gcloud datastore cleanup-indexes index.yaml


以上です。今回は Datasoreのデータ使用量削減という観点から、記事を書いてみました。