はじめに
これは ドリコム Advent Calendar 2021 の25日目です。24日目は 吉岡ひろき さんの DynamoDBをゲームアプリで使う際の課題と対策(前編) です。
前編では理論を説明しましたので、この後編では実際の適用例について説明していきます。
適用例
ここまでスキーマ設計のための観点を記して来ましたが、アプリケーションの要件によってはすべての観点で最適な選択をできない場合があるので、優先順位の高い条件を満たすスキーマを選択することになります。以下では上述の観点を実際に利用して設計した例をいくつか示します。例1. ランキング
ユーザ間でスコアを競うイベントのランキングについて考えます。アクセスパターンは次の3つとします。- 個々のユーザのスコアを加算する
- 上位100ユーザのスコアとニックネームと使用中のキャラクタを取得する
- ニックネームまたは使用するキャラクタ変更
PK UserID |
attribute 1 Nickname |
attribute 2 CharacterID |
---|---|---|
1560789 | Alice | 45 |
1123642 | Bob | 98 |
スコアの加算
まずスコアを保存する場所ですが、イベント期間中にアクセスが集中し、一定期間が過ぎるとアクセスされなくなる偏りがあるデータと言えますので、イベント期間専用のテーブルを設けることが考えられます。PKとして使えるエンティティは、ここまでユーザしか登場していませんので、ユーザとします。次にSKですが、スコアはパーティション内の最初のデータであり、他のデータと区別する必要がないので指定しないことにします。ここまでのスキーマを表にすると下のようになります。
PK UserID |
attribute 1 Score |
---|---|
1560789 | 1230 |
1123642 | 780 |
上位100ユーザ(案1. イベントテーブル、正規化)
上位100ユーザを取得するには何らかのSKとしてスコアを指定する必要があります。スコアは更新される値なのでプライマリキーにはできないことと、パーティションを横断してソートが必要なことから、GSIを使うことになります。GSIのPKとしては何らかの固定値を指定すれば1パーティションで全ユーザのスコアをソートできるので、1アクセスで上位100ユーザのUserIDを取得できます。例えばイベントのIDをPKに指定すれば下のようなスキーマになります。
PK EventID |
SK Score |
attribute 1 UserID |
---|---|---|
Event1 | 1230 | 1560789 |
1080 | 1123642 |
ここまでで上位100ユーザのスコアとユーザIDは取得する目処が立ちましたが、要件ではニックネームと使用中のキャラクタも取得しなければなりません。この要件を満たすためにユーザテーブルを引くことが考えられますが、100item分なので最低でも100CUかかります。ランキングをリアルタイムに更新しなくてよいならどこかにキャッシュすることも考えられますが、DynamoDB以外のシステムを使うことになり本稿の範囲を越えるので、別の方法を検討してみます。
上位100ユーザ(案2. ユーザテーブル)
スコアとニックネーム、使用中のキャラクタをすべて同時に取得するには、それらを1itemに保存すれば済むので、ユーザテーブルにスコアを保存することを考えてみます。つまり下のようなスキーマです。PK UserID |
attribute 1 Nickname |
attribute 2 CharacterID |
attribute 3 Event1-Score |
---|---|---|---|
1560789 | Alice | 45 | 1230 |
1123642 | Bob | 98 | 1080 |
PK EventID |
SK Event1-Score |
attribute 1 UserID |
attribute 2 Nickname |
attribute 3 CharacterID |
---|---|---|---|---|
Event1 | 1230 | 1560789 | Alice | 45 |
1080 | 1123642 | Bob | 98 |
一方でこのスキーマの欠点としては下の3点を挙げられます:
- イベント関連のアクセスがユーザテーブルのCUを消費してしまう
- 1回のイベントに特化しており、別のイベントのランキングを管理するには別のGSIを作らなければならない
- ユーザのitemサイズが増えるので、イベントと無関係なアクセスのコストが上がる可能性がある
上位100ユーザ(案3. イベントテーブル、非正規化)
イベントテーブルを使いながら1itemに必要な情報を詰めることを考えると、下のようなスキーマが考えられます。イベントも複数回の開催を想定し、attributeにします。例としてAliceがEventID=2のイベントで2,690点を取得した場合の値を入れてあります。PK UserID |
SK EventID |
attribute 1 Score |
attribute 2 Nickname |
attribute 3 CharacterID |
---|---|---|---|---|
1560789 | 1 | 1230 | Alice | 45 |
2 | 2690 | Alice | 45 | |
1123642 | 1 | 1080 | Bob | 98 |
PK EventID |
SK Score |
attribute 1 UserID |
attribute 2 Nickname |
attribute 3 CharacterID |
---|---|---|---|---|
1 | 1230 | 1560789 | Alice | 45 |
1080 | 1123642 | Bob | 98 | |
2 | 2690 | 1560789 | Alice | 45 |
以上のようにランキングの実装について3つの案を挙げましたが、それぞれ利点と欠点があり、最適な解法はアプリケーションによります。
例2. ギルド加入申請とメンバー一覧
この例の前提は次の通りです。- ギルドと呼ばれるユーザのグループがある
- ギルドにはリーダーがおり、メンバーの加入を認可または拒否できる
- ユーザは1つのギルドに対してのみ加入を申請、または所属できる
- 例1と同様のユーザテーブルがある
- ギルドを区別するIDがある
- 特定ギルドに加入申請している全ユーザのニックネームと使用中のキャラクタを取得する
- 特定ギルドに所属している全ユーザのニックネームと使用中のキャラクタを取得する
- ニックネームまたは使用するキャラクタの変更
加入申請一覧
まず、あるユーザが特定のギルドに加入申請していることを表すデータの保存場所について考えます。時期によるアクセスの偏りはない想定なので、このデータ専用のテーブルは不要です。よって既存のテーブル、つまりユーザテーブルに保存することを最初に検討します。PKとして利用できるエンティティとしては、ユーザとギルドが考えられます。ユーザが加入申請できるギルドは1つという前提なので、パーティションの容量については心配がなさそうです。ユーザが加入申請する頻度を想像すると、アクセスの集中についても問題なさそうです。仮にPKにギルドIDを利用して、人気のギルドができたとしても、レスポンスが悪化することはないでしょう。よってPKにつかうエンティティは他の条件次第でユーザでもギルドでも選べそうです。
最後にSK、つまり保存先のitemについて考えます。PKにユーザIDを使った場合はニックネームなどと同じitemに保存するか、別のitemにするかの選択になります。加入申請一覧のアクセスパターンではニックネームと使用中のキャラクタを読み出すので、同じitemに保存するメリットがあります。デメリットとしては、一つのギルドへの加入申請情報が複数のユーザのパーティションに分散するので、GSIが必要なことが挙げられます。
申請先ギルドIDを表すattributeをAppliedGuildとした場合の例を下に示します。
PK UserID |
attribute 1 Nickname |
attribute 2 CharacterID |
attribute 3 AppliedGuild |
---|---|---|---|
1560789 | Alice | 45 | 7 |
1123642 | Bob | 98 | 65 |
PK AppliedGuild |
attribute 1 UserID |
attribute 2 Nickname |
attribute 3 CharacterID |
---|---|---|---|
7 | 1560789 | Alice | 45 |
65 | 1123642 | Bob | 98 |
PKにギルドIDを使った場合、ユーザ毎に別のitemに申請情報を保存するか、1つのitemに保存するか検討することになります。ユーザ毎に別のitemの場合は、SKにユーザIDを使い、itemの有無で加入申請を表せるでしょう。itemが分かれているので同時に複数の申請があった場合でも競合を気にする必要がありません。一方で申請者の数だけitemができるので、比例してコストが増えます。
1つのitemに保存する場合は、セットというデータ型を利用できます。セットは重複なく複数の値を保存する型です。セットに値を追加/削除するAPIがあるので処理は十分シンプルになりますし、申請者が増えても別itemの場合に比べてコストの増加は遥かにゆるやかですが、同時に複数の申請を処理する場合の競合に注意する必要があります。
ここでは1つのitemに保存した場合の例を示します(
[]
はセットを表します)。
PK GuildID |
attribute 1 ApplicantIDs |
---|---|
7 | [1560789] |
65 | [1123642] |
所属ユーザ一覧
特定ギルドに所属している全ユーザの取得は、前項の加入申請一覧とよく似た課題で、同様のスキーマ案が考えられます。しかし加入申請のGSIが既に存在する場合、同じGSIにデータを保存できるか、できるとしたら十分なメリットがあるかを検討すべきです。加入申請のGSIはPKにギルドIDを採用しているので、同じパーティションに所属ユーザの情報を保存しても不自然ではありません。ただし加入申請と所属は区別する必要があるので、SKを使うことにします。例えばIDが7のギルドに1人の加入申請と2人の所属者がいた場合、下のような状態になれば要件を満たします。
PK GuildID |
SK GuildStatus |
attribute 1 UserID |
attribute 2 Nickname |
attribute 3 CharacterID |
---|---|---|---|---|
7 | Apply | 1560789 | Alice | 45 |
Member | 2093510 | Charlie | 62 | |
Member | 1284623 | Daniel | 11 |
GuildStatus
と書いたギルドに関するステータスがitemにあれば済むので、ベースとなるテーブルは下のようになります。
PK UserID |
attribute 1 Nickname |
attribute 2 CharacterID |
attribute 3 GuildID |
attribute 4 GuildStatus |
---|---|---|---|---|
1560789 | Alice | 45 | 7 | Apply |
2093510 | Charlie | 62 | 7 | Member |
1284623 | Daniel | 11 | 7 | Member |
GuildStatus
の分だけ大きくなりますが、GSIを1つ節約できます。
ニックネーム/使用中のキャラクタの変更
ギルド加入申請/メンバー一覧のスキーマではニックネームや使用中のキャラクタ情報を非正規化していないので、通常の更新APIで更新すれば済みます。これらの値をプロジェクションに指定しているGSIには、更新が自動的に反映されます。ただし非同期であり、ベースのitem更新後にGSIが更新されるまでに時間差があることは知っておかなければなりません。その他の発見
スキーマ設計をしてみて気づいた細かな注意点を簡単にまとめます。管理業務の分析
運営側がゲームに必要なデータを操作したりユーザのデータを閲覧する場合のアクセスパターンは、ユーザ起因のアクセスと異なるものが多いため、スキーマ設計の際に忘れず分析しておく必要がありそうです。IDの採番
メジャーなRDBにはレコードを追加すると自動的に通し番号を振ってくれる機能がありますが、DynamoDBにはないので、一意なIDが必要な場合は別途しくみを用意しなければなりません。CU設定の自動化
CUの設定はコストのコントロールそのものですが、時間の経過に伴って値を変えるものなど、予め変更を予定しているものは自動的に設定するしくみを用意すればトラブルを避けられそうです。まとめ
社内の調査ではDynamoDBを使うときの課題としてスキーマ設計が挙げられることを見出し、それを解決するため、運用中のゲームのクエリを例に公式ガイドに従って実際に設計を試み、課題の解決策として設計の観点を示しました。観点をもう一度簡単にまとめます。- テーブルの決定
-
- アクセスの平滑化
- 予測できるアクセスの偏りに合わせたテーブルの割当て
- GSI/LSIの上限(20個/5個)
-
- PKの決定
-
- エンティティの粒度を決める
- パーティション間でのアクセスの分散
- パーティションの容量(10GB)
- item同士の関連付け
-
- SKの決定
-
- itemを分けるか1つにするかを決める
- アクセス回数(分ければ増える)
- itemサイズ(1つにすれば増える。最大400KB)
- GSIによるプロジェクション
- GSI/LSIの消費
- 競合(1つにするならトランザクションが必要)
- 同期(分けるならアプリケーションで実現)
-
おわりに
ドリコムでは一緒に働くメンバーを募集しています!募集一覧はコチラを御覧ください!