はじめに
こんにちは。GS2 CEO の丹羽(@kazutomo)です。
サーバレスアーキテクチャでゲームサーバを開発し、AWSのサービスのように1時間あたり数円〜利用できる Game Server Services を提供しています。
このシステムのバックエンドDBは DynamoDB を中心に利用していますが、DynamoDB が苦手とするユースケースでは Google CloudDatastore を利用しており、Google CloudDataStore に対する知識もサーバレス時代に必要なナレッジであると認識しています。
DynamoDB については昨年のアドベントカレンダーを参照してください。この記事の一部はDynamoDBに関する知識は昨年の記事レベルで理解してる前提に、どのように使い分けるべきかを比較しながら解説します。
この記事ではプログラムのコードを示して使い方を説明するというよりは、
CloudDataStore とはどういうものか?どういうことに気をつけて使う必要があるか?といった情報を文字で説明したいとおもいます。
CloudDatastoreとは
CloudDatastoreはGoogleが提供するNoSQLデータベースです。
DynamoDBよりはRDBMS寄りの設計思想ですが、NoSQLらしくRDBMSとは異なる特性があります。
しかし RDBMS とは異なり、適切に利用すればどこまでもスケールするという魅力があります。
ポケモンGOやスナップチャットのデータベースを支えているという実績もあり、スケーラビリティの面で心配する必要は無いでしょう。
CloudDataStore と RDBMS との用語対比表
RDBMS | CloudDataStore |
---|---|
スキーマ(データベース) | ネームスペース |
テーブル | カインド |
レコード | エンティティ |
プライマリキー | キー + 祖先キー |
料金体系
事前にスループットを予約する DynamoDB とは異なり、完全にIOリクエストに対して課金されます。
エンティティの読み込み 0.06ドル/10万エンティティ
エンティティの書き込み 0.18ドル/10万エンティティ
エンティティの削除 0.02ドル/10万エンティティ
データストレージの利用量に対しても課金されます。
0.18ドル/1GB
DynamoDBのようなキャパシティを事前に予約することで割引を受けるような制度はありません。
DynamoDB との価格比較
DynamoDB は何もしなくても費用が発生しますが、CloudDataStore は何も操作しなかった場合は費用は発生しません。
一見、CloudDataStore は超お得に思えますが、本当にそうでしょうか。
DynamoDB の最小利用料金はグローバルインデックスしか使わず、キャパシティを読み書きそれぞれ1に設定したケースが最小価格です。この時の利用料金は
0.0008904ドル/時 = 0.0213696ドル/日 = 0.6624576ドル/月
です。
これで秒間1回読み書きができるので、最大で 2,678,400回/月 の読み書きができます。
この IO量をCloudDataStoreにあてはめると 読み込み 1.60ドル/月 書き込み 4.8ドル/月 となり、合計で 6.4ドル/月 となります。
現実的にはDynamoDBのキャパシティを一ヶ月使い切るような状態はもはや障害なので、あり得ないのですが、10倍も差があると最適な状態で使用した場合は DynamoDB 優位があるといえます。
しかし、これはあくまで最適な状態での条件での話です。
現実には、キャパシティを綺麗に使い切ることも出来ませんし、DynamoDBでプライマリーキー以外のカラムの値を使ってフェッチをするにはその分のセカンダリーインデックスを作成し、それぞれに読み書きキャパシティコストを払っていく必要がありますが、CloudDataStoreではインデックスを作成しても追加のコストはインデックスデータのストレージ料金だけです。
ユースケースや運用状況によっては CloudDataStore の方が優れたコストパフォーマンスを出すことも十分あり得ます。
Cloud DataStore の特徴
クエリとインデックス
CloudDataStore も DynamoDB 同様に検索条件で使用するクエリにはインデックスを張る必要があります。
しかし、DynamoDB と異なる点として、インデックスを追加したことでかかる費用はインデックスに使用するストレージの容量であることです。
これは読み書きキャパシティを買わなければならない DynamoDB と比較すれば明瞭会計ですし、いろんな条件で検索したい場合などは CloudDataStore の方が圧倒的に有利です。
また、CloudDataStoteは複合インデックスも定義できますので、DynamoDBでおこなっていたような、2つのカラムの値を対象に検索をするために、2つのカラムの値を結合した値を持つ謎のカラムを持つような実装をする必要はありません。
エンティティグループ
CloudDataStore で最も需要な要素は祖先キーです。
エンティティには一つ主キーを指定する必要がある。というのは一般的なデータベースと変わらないのですが、CloudDataStore は祖先キーという値を指定することが出来ます。
祖先キーにはそのエンティティの親となるエンティティの主キーを設定します。
こうすることで、エンティティがツリー構造になります。
このツリーをエンティティグループといいます。
ここで気をつけなければならないことは エンティティグループに対する書き込み処理は1秒間1回しか実行できない ことです。
CloudDataStore とつきあっていく上で一番気をつけなければならないポイントはここでしょう。
誤った設計例
たとえばチャットルームをCloudDataStoreで実装して、チャットルームテーブルのチャットルームIDを祖先キーとするメッセージテーブルを作ってしまうと、1つのチャットルームには参加者が何人であろうと秒間1回以上書き込めない。というバルス耐性の全くないチャットシステムが爆誕します。
エンティティグループの用法
では、書き込みスループットを犠牲にしてまでエンティティグループを作る理由は何でしょうか。
一貫性のあるクエリと結果整合のクエリ
CloudDataStoreの提供するクエリは基本的に 結果整合のクエリ です。
CloudDataStore のデータは複数のクラスタノードに保存されますが、書き込んだ直後は全てのノードに結果が反映されているとは限りません。
結果整合のクエリというのは、データを書き込んだり更新した直後に読み取りクエリを発行したときに書き込みの内容が反映されていない可能性があるということです。
このあたりはRDBMSのリードレプリカを使用している状況に似ています。
アプリケーションの要件によっては絶対に直前の書き込み内容が反映されていて欲しいことがあります。この時に必要となるのが 一貫性のあるクエリ です。
RDBMSであればマスターデータベースに対して直接クエリするようなものです。
CloudDataStoreでは主キーを直接指定した一本釣りクエリ以外で一貫性のあるクエリを発行する手段は祖先キーを指定したクエリしかありません。
そのため、アプリケーションの要件によって、エンティティに祖先キーを使用する必要があることがあります。
ちなみに、祖先キーを使ったクエリをアンセスタークエリと呼びます。
トランザクション
CloudDataStoreではトランザクションが使用できます。
CloudDataStoreのロックは 楽観ロック です。
つまり、トランザクション開始から commit するまでの間に並列処理でデータが更新されてた場合、commit しようときた時にエラーになることがあります。
また、トランザクション内では同一エンティティグループに対しての書き込み処理を同期的に実行できます。
つまり、同期的に複数のテーブルにデータを書き込みたい場合はエンティティグループを使用する必要があります。
祖先キー ≠ リレーションシップ
CloudDataStore を利用するときに勘違いしてはいけないことは、 祖先キーはリレーションシップを定義するものではない ということです。
エンティティグループが適切なサイズ・粒度になるよう、たとえリレーションがあっても不必要な祖先キーは設定しない。という判断がCloudDataStoreのスキーマ設計では必要となります。
インデックス爆発
CloudDataStore は複合インデックスが使用できると説明しました。
しかし、CloudDataStoreで複合インデックスを作成するときには注意しなければならないことがあります。
それは複合インデックスにリスト型のカラムを指定する時に気にする必要があります。
user | items |
---|---|
taro | [hoge, fuga] |
jiro | [fuga, piyo, foo, bar] |
こんなテーブルで user と items の複合インデックスを作った場合
インデックスデータは下記のように展開されて記録されます。
user | items |
---|---|
taro | hoge |
taro | fuga |
jiro | fuga |
jiro | piyo |
jiro | foo |
jiro | bar |
この場合、2個のエンティティに対して6個のインデックスデータが生成されます。
複合インデックスのカラムが両方リスト型だった場合はさらに展開されます。
users | items |
---|---|
[taro] | [hoge, fuga] |
[jiro, hanako, saburo] | [fuga, piyo, foo, bar] |
の場合
user | items |
---|---|
taro | hoge |
taro | fuga |
jiro | fuga |
jiro | piyo |
jiro | foo |
jiro | bar |
habako | fuga |
hanako | piyo |
hanako | foo |
hanako | bar |
saburo | fuga |
saburo | piyo |
saburo | foo |
saburo | bar |
2個のエンティティに対して、14個のインデックスデータが生成されます。
このテーブルが10万個のエンティティを持つテーブルたった場合、インデックスデータはどれだけ大きなものになるでしょうか?
あるいは、リスト型の3カラムのを使用した複合インデックスだった場合はどうなるでしょうか。
以前はインデックスデータの書き込みも1オペレーションとして課金対象だったのですが、今では1エンティティ単位での課金になっているので、インデックス爆発が直接費用の爆発には繋がらなくなりました。
しかし、ストレージの使用量はインデックスデータに対しても課金されます。つまり、インデックス爆発するとストレージは消費しますので、その分は課金しなければならない点を気をつける必要があります。