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