DynamoDBによるソーシャルゲーム実装 How To
- 1. DynamoDBによる
ソーシャルゲーム実装 How
To
2013-03-16 JAWS DAYS 2013 [DEV-02]
株式会社マイネット
伊藤 祐策
- 8. 基本的な使い方
キーを指定してデータへアクセス
CRUD操作は一通り揃っている
操作は全てアトミックに処理される
キーと更新条件を指定してデータを更新する
データの内容が更新条件に適合しなければ失敗する
楽観的ロックの実装に必要
- 9. 他のKVS型DBとの大きな違い
「レンジキー」というものがある。
テーブルのキーは以下の2通りから選択できる
ハッシュキーのみ
ハッシュキー+レンジキーの組み合わせ
- 10. ハッシュキーとは?
データへアクセスするためのプライマリキー
ハッシュキーを上手に分散させることができれ
ば
概ね課金額通りの性能が得られる。
逆を言えば、1つのハッシュキーにアクセスが
集中する設計にしてしまうと、パフォーマンス
が
低下してしまう。【重要】
- 11. レンジキーとは?
ハッシュキー+レンジキーでプライマリキーに
なる
Queryメソッドで範囲検索が可能になる
「ハッシュキー」と「レンジキーの範囲」を指定し
て
複数レコードをまとめて取得することができる
レンジキーに対するアクセスを分散しても
負荷が分散されないことに注意
ハッシュキーでしっかり分散される設計にしよう
- 12. 基本的なレコード操作
CRUD
PutItem ... 作成/置換
UpdateItem ... 全部更新/部分更新
DeleteItem ... 削除
GetItem ... 取得
複数レコードの取得
Query ... 1つのハッシュキーに対する範囲検索
Scan ... テーブル内の全レコード取得
- 13. 格納できる値
値の型は3種類から選べる
String型
UTF-8文字列
Number型
整数または小数
Binary型
用途としては、暗号化されたデータ等
- 14. 格納できない値
NULL値
代わりに属性ごと削除する
長さ0の文字列
NULLと同様、属性ごと削除する
真偽値
Number型の1と0で代用する
- 15. 条件付きアップデート
更新系のメソッドで利用できる
条件に適合しなければ操作は失敗する
以下のような条件が指定可能
「もしレコードが存在しなければ」
「もしレコードが存在したならば」
「もしこの属性の値がこの内容と同一であれば」
「もしこの属性が存在しなければ」
- 17. MySQLの代替手段となり得るか?
完全には無理。
以前DynamoDBだけでソーシャルゲーム作って
みましたが、結論としては色々と無理がある
のでMySQL等とのハイブリット型にするのが良
いです。
- 18. DynamoDBが苦手なこと
縦横無尽な検索
プライマリキー以外のインデックスを張れない
せいぜいレンジキーで範囲検索ができるくらい
小規模な集計処理
集計機能がそもそもないので自前で実装する必要があ
る
大規模データならEMR連携という手段が用意されてい
る
大きなデータの保存
1レコードあたり64kBというデータ長制限がある
素直にS3使いましょう
- 19. MySQLとの比較
DynamoDB MySQL
データ保全 ◎ ○
検索 × ◎
負荷分散 ◎ △
- 20. どう使うべきか?
アプリケーションに要求される機能のうち、
DynamoDBが苦手なものは他の手段に任せる
検索はMySQLやCloudSearchへ
集計はMySQLへ
大きなデータの保存はS3へ
残ったものは全てDynamoDBで実装する
- 21. MySQLハイブリッド型にする場合
MySQLのシステムがある日突然消失しても、
すぐにサービスが再開できるような設計にして
おく
MySQL内のデータは全てDynamoDBのデータを本体とし
たコピーにする。
データが全て消失しても、MySQLインスタンスを作り
なおして全データを再投入すれば完全復旧できるよう
にする。
「多尐ロストしても構わないデータ」の分別をしっか
りつけておく
- 23. 大激闘!キズナバトル
Androidアプリ
2012年12月26日リリース
GvGカードバトル型ゲーム
最大20人のチームを組んで、
1日3回開催されるバトルを
勝ち抜き、最強チームを目指す。
- 24. 大激闘!キズナバトル
使用DynamoDBテーブル数は47。
MySQLとのハイブリット型構成。
バトル開催時間になるとアクセス量は一気に15倍になる。
22時
1日のアクセス数グラ 19時
フ
12時
- 25. DynamoDBの使われ方
原則全てのデータはDynamoDBで管理
ユーザー情報
ユーザーの所有物(カード、アイテム、etc)
チーム情報
バトル結果
MySQLで行なっている処理
対戦相手のマッチング
ランキング集計
チーム検索
入団希望者検索
- 26. 実装の基本方針(1)
ユーザーが1回行動するたびに1レコード作る
アイテムを使った、カードバトルで対戦した、etc
ユーザーの行動が全て「証拠」として残されている。
お問い合わせからクレームが来た時に、何が起こったのかが
明確に分かるので調査が容易になる。
レコードは消さない
保存費用よりWrite性能費用のほうが高い。
【速報】3月1日にデータ保存料金が75%も値下げされまし
た!!
リリース時から全ての歴史が保存されている。
KVSなのでデータ量がいくら増えてもパフォーマンスに影響
がない。
- 27. 実装の基本方針(2)
ほぼ全ての処理をキューで非同期に実行
処理が終わるまでのタイムラグは画面エフェクトを表示して待
たせる
いかに「ごまかす」かが腕の見せ所
キャッシュはTAT改善のために使う
さすがにMemcachedのほうが応答が速い。
Read性能はかなり安いので節約する意味があまりない。
m1.smallインスタンス1台の費用でRead性能を366も買えてしま
う。
- 29. テーブル設計
スキーマレスだけどスキーマは定義する
まずはゲームオブジェクトをクラスとして定義
ユーザー、所有カード、所有アイテム、etc
1クラス=1スキーマ=1テーブル
class App_Record_Card extends DynamoDBRecord
ハッシュキーはユーザーIDで
レンジキーはオブジェクトインスタンスIDで
インスタンスIDは日付+時刻+乱数で生成
- 30. テーブル定義の例
ユーザー カード
ユーザーID 100 ユーザーID 100
名前 †ラインハル インスタンス 1001
ト† ID
レベル 15 レベル 10
所有アイテム
ユーザーID 100
所持金 1500G
薬草 32個
- 32. 残念なお知らせ
全てのレコード操作メソッドは失敗する可能性がある。
TCP/IP ネットワークエラーが発生した場合 (結構頻繁)
Endpoint側に障害が発生した場合 (数回実績あり)
課金額以上の負荷を与えた場合 (何度もやらかした)
RDBMSにおける「トランザクション」は提供されていな
い
複数レコードを一貫性を保ったまま同時に更新することがで
きない。
アプリケーションレイヤで一貫性を保証する実装をしなけれ
ばならない。
- 34. 更新対象が1レコードの場合
所有アイテム クエリ内容
ユーザー 100 UpdateItem ユーザーID 100
ID
所持金 -100G
所持金 1500G
薬草 +1個
薬草 10個
【更新条件】
所持金が1500Gだった
ら
- 36. 更新対象が2レコード以上の場合
所有アイテム 強化対象カード
ユーザー 100 ユーザーID 100
ID
インスタンス 1001
所持金 1500G ID
レベル 10
素材カード
Case 2: ユーザーID 100
カードを強化す インスタンス 1002
る ID
レベル 1
- 37. 更新対象が2レコード以上の場合
所有アイテム 強化対象カード
ユーザー 100 ユーザーID 100
ID
インスタンス 1001
所持金 1500G ID
レベル 10
更新 素材カード
削除 ユーザーID 100
素材カードを消費して強化対 インスタンス 1002
象カードのレベルを1上げる。 ID
費用として500G徴収する。 レベル 1
- 38. 更新対象が2レコード以上の場合
所有アイテム 強化対象カード
ユーザー 100 ユーザーID 100
ID
インスタンス 1001
所持金 1000G ID
レベル 10
Step1: 素材カード
所持金 -500G ユーザーID 100
インスタンス 1002
ID
レベル 1
- 39. 更新対象が2レコード以上の場合
所有アイテム 強化対象カード
ユーザー 100 ユーザーID 100
ID
インスタンス 1001
所持金 1000G ID
レベル 10
Step2: 素材カード
素材カードを削 ユーザーID 100
除 削除
インスタンス 1002
ID
レベル 1
- 40. 更新対象が2レコード以上の場合
所有アイテム 強化対象カード
ユーザー 100 ユーザーID 100
ID
インスタンス 1001
所持金 1000G ID
レベル 10
Step3: 素材カード
レベル ユーザーID 100
アッ・・・ 削除済
インスタンス 1002
ID
レベル 1
- 42. 更新対象が2レコード以上の場合
所有アイテム 強化対象カード
ユーザー 100 ユーザーID 100
ID
インスタンス 1001
所持金 1000G ID
レベル 10
残念!! 素材カード
カードの強化処理 ユーザーID 100
は 削除済
インスタンス 1002
これで終わって ID
レベル 1
しまった!
- 46. システム構成
3.Enqueue 4.Dequeue
SQS
1.HTTP Request
Web Servers Batch Servers
2.Put Record 5.Update Records
DynamoD
B
- 47. 2種類のプロセス
Webリクエスト処理
HTTPリクエストをトリガーとして実行される処理
プロセスはApacheによって実行・管理される
途中でエラーが発生したら503エラーを返して中断さ
れる
キュー処理
SQSへメッセージを送り、メッセージの取り出しを
トリガーとして実行される処理。
プロセスはアプリケーション用のユーザーで実行され
る
正常終了するまで何度も繰り返し実行される
- 48. Amazon SQSを使う
SQSは、処理の「完遂保証」のために使う
失敗した時は何度でも再実行されることを保証さ
せる
キュー処理は最終的に正常終了に収束するよう
実装する
状態遷移図を書いてしっかり机上デバッグ
但し書いたら負けかなと思ってる
図が要らないほどシンプルな実装にしよう
- 49. キュー処理実装の鉄則
再実行耐性を持たせる
同じ処理が2回実行されても
結果に影響がでないようにする。
並列実行耐性を持たせる
同じ処理が2つ以上のプロセスで並行して
実行されても結果に影響がでないようにする。
- 50. 再実行耐性の実装方法
入力内容から処理内容が全て決定されるようにする。
レコードの更新をする際に、確かに更新されたことが判
別できるよう「証拠」を残すようにする。
更新日時を書き込む、ステータス値を変更する、etc。
レコード内容をみればどこまで処理が終わったかが
分かるようにする。
処理済みであればスキップして次の処理へ進むようにす
る。
複雑な分岐をさせず、上から流れ落ちるような処理にす
る。
- 51. 並列行耐性の実装方法
条件付きアップデート機能を用いて楽観的ロックを実装
する。
更新する前にレコードを「一貫性あり」で読み込む。
レコードを更新するときは、「読み込んだ時点から他の
誰にも更新されていなければ」という条件をつける。
「条件付きアップデート」を使う
必要であればレコードにバージョン番号を導入する
更新に失敗した場合は処理を最初からやりなおす。
→ 再実行耐性が実現されていれば問題ないはず!
- 55. カードの強化
所有アイテム 強化対象カード
ユーザー 100 ユーザーID 100
ID
インスタンス 1001
所持金 1500G ID
レベル 10
素材カード
Case 2': ユーザーID 100
今度こそ インスタンス 1002
カードを強化す ID
る レベル 1
- 56. カードの強化
所有アイテム カード強化依頼
ユーザー 100 ユーザーID 100
ID
依頼ID 5001
所持金 1500G
強化対象カード 1001
素材対象カード 1002
Step1: 強化費用 500G
依頼レコードを 開始済 NO
作成する
- 57. カードの強化
所有アイテム カード強化依頼
ユーザー 100 ユーザーID 100
ID
依頼ID 5001
所持金 1500G
強化対象カード 1001
未決済 [ 5001 ]
素材対象カード 1002
Step2:
強化費用 500G
依頼IDを所有アイテム
レコードに登録する 開始済 NO
※STRING_SET型を使う
- 58. カードの強化
強化対象カード カード強化依頼
ユーザーID 100 ユーザーID 100
インスタンス 1001 依頼ID 5001
ID
強化対象カード 1001
レベル 10
素材対象カード 1002
未処理 [ 5001 ]
Step3: 強化費用 500G
依頼IDを強化対象カー 開始済 NO
ド
レコードにも登録する
※STRING_SET型を使う
- 59. カードの強化
キューメッセージ カード強化依頼
処理種別 カード強 ユーザーID 100
化
依頼ID 5001
ユーザー 100
ID 強化対象カード 1001
依頼ID 5001 素材対象カード 1002
Step4: 強化費用 500G
キューメッセージ 開始済 NO
を発行する
- 60. カードの強化(キュー処理)
所有アイテム カード強化依頼
ユーザー 100 ユーザーID 100
ID
依頼ID 5001
所持金 1500G
強化対象カード 1001
未決済 [ 5001 ]
素材対象カード 1002
Step5: 強化費用 500G
レコードを読み込
開始済 NO
む
- 61. カードの強化(キュー処理)
所有アイテム カード強化依頼
ユーザー 100 ユーザーID 100
ID
依頼ID 5001
所持金 1500G
強化対象カード 1001
未決済 [ 5001 ]
素材対象カード 1002
Step6: 強化費用 500G
開始済みにする
※無条件UPDATE 開始済 YES
- 62. カードの強化(キュー処理)
所有アイテム カード強化依頼
ユーザー 100 ユーザーID 100
ID
依頼ID 5001
所持金 1000G
強化対象カード 1001
未決済 (NULL)
素材対象カード 1002
Step7: 強化費用 500G
決済する 開始済 YES
※条件付きUPDATEを使う
- 63. カードの強化(キュー処理)
素材カード カード強化依頼
ユーザーID 100 ユーザーID 100
インスタンス 1002 依頼ID 5001
ID
強化対象カード 1001
レベル 1
素材対象カード 1002
Step8: 強化費用 500G
素材カードを削除 開始済 YES
- 64. カードの強化(キュー処理)
素材カード カード強化依頼
ユーザーID 100 ユーザーID 100
削除
インスタンス 1002 依頼ID 5001
ID
強化対象カード 1001
レベル 1
素材対象カード 1002
Step8: 強化費用 500G
素材カードを削除 開始済 YES
- 65. カードの強化(キュー処理)
強化対象カード カード強化依頼
ユーザーID 100 ユーザーID 100
インスタンス 1001 依頼ID 5001
ID
強化対象カード 1001
レベル 10
素材対象カード 1002
未処理 [ 5001 ]
強化費用 500G
Step9:
強化対象カードのパラメー 開始済 YES
タを加算する
※条件付きUPDATEを使う
- 66. カードの強化(キュー処理)
強化対象カード カード強化依頼
ユーザーID 100 ユーザーID 100
インスタンス 1001 依頼ID 5001
ID
強化対象カード 1001
レベル 11
素材対象カード 1002
未処理 ( NULL )
強化費用 500G
Step9:
強化対象カードのパラメー 開始済 YES
タを加算する
※条件付きUPDATEを使う
- 67. 実装の要点
完遂保証のない処理(Webリクエスト処理)
と、
完遂保証のある処理(キュー処理)で、
実行すべき処理を上手に振り分ける。
Webリクエスト処理の途中でエラーが発生
しても、キュー処理の実行が開始されな
い限り「何も起こらなかった」ことにな
る。
その時は仕方なく503エラーを返す
各レコードにトランザクションIDが残る可能性につい