dynamodbによるソーシャルゲーム実装 how to

Post on 14-Jun-2015

15.361 Views

Category:

Documents

4 Downloads

Preview:

Click to see full reader

DESCRIPTION

JAWS DAYS 2013 [DEV-02] http://jaws-ug.jp/jawsdays2013/

TRANSCRIPT

2013-03-16 JAWS DAYS 2013 [DEV-02]

株式会社マイネット 伊藤 祐策

DynamoDB によるソーシャルゲーム実装 How

To

概要

 『 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倍に

なる。

12時19時

22時1日のアクセス数グラフ

DynamoDB の使われ方

原則全てのデータは DynamoDB で管理 ユーザー情報 ユーザーの所有物(カード、アイテム、 etc) チーム情報 バトル結果

MySQL で行なっている処理 対戦相手のマッチング ランキング集計 チーム検索 入団希望者検索

実装の基本方針 (1)

ユーザーが 1回行動するたびに 1 レコード作る アイテムを使った、カードバトルで対戦した、 etc ユーザーの行動が全て「証拠」として残されている。 お問い合わせからクレームが来た時に、何が起こったのかが明確

に分かるので調査が容易になる。 レコードは消さない

保存費用より Write 性能費用のほうが高い。 【速報】 3月 1日にデータ保存料金が 75% も値下げされまし

た!! リリース時から全ての歴史が保存されている。 KVS なのでデータ量がいくら増えてもパフォーマンスに影響が

ない。

実装の基本方針 (2)

ほぼ全ての処理をキューで非同期に実行 処理が終わるまでのタイムラグは画面エフェクトを表示して待

たせる いかに「ごまかす」かが腕の見せ所

キャッシュは TAT改善のために使う さすがに Memcached のほうが応答が速い。 Read 性能はかなり安いので節約する意味があまりない。 m1.small インスタンス 1台の費用で Read 性能を 366 も買え

てしまう。

テーブル設計

テーブル設計

スキーマレスだけどスキーマは定義する まずはゲームオブジェクトをクラスとして定義

ユーザー、所有カード、所有アイテム、 etc 1クラス=1スキーマ=1テーブル

class App_Record_Card extends DynamoDBRecord

ハッシュキーはユーザー ID で レンジキーはオブジェクトインスタンス ID で

インスタンス ID は日付+時刻+乱数で生成

テーブル定義の例

所有アイテム

ユーザー ID 100

所持金 1500G

薬草 32個

カード

ユーザー ID 100

インスタンスID

1001

レベル 10

ユーザー

ユーザー ID 100

名前 † ラインハルト†

レベル 15

実践

残念なお知らせ

全てのレコード操作メソッドは失敗する可能性がある。 TCP/IP ネットワークエラーが発生した場合 ( 結構頻繁 ) Endpoint側に障害が発生した場合 ( 数回実績あり ) 課金額以上の負荷を与えた場合 (何度もやらかした )

RDBMS における「トランザクション」は提供されていない 複数レコードを一貫性を保ったまま同時に更新することがで

きない。 アプリケーションレイヤで一貫性を保証する実装をしなけれ

ばならない。

更新対象が1レコードの場合

所有アイテム

ユーザーID

100

所持金 1500G

薬草 10個

Case 1:薬草を 1 個購入する

更新対象が1レコードの場合

所有アイテム

ユーザーID

100

所持金 1500G

薬草 10個

クエリ内容

ユーザーID

100

所持金 -100G

薬草 +1個

UpdateItem

【更新条件】所持金が 1500G だったら

更新対象が1レコードの場合

所有アイテム

ユーザーID

100

所持金 1400G

薬草 11個

更新完了!

更新対象が2レコード以上の場合

所有アイテム

ユーザーID

100

所持金 1500G

Case 2:カードを強化する

強化対象カード

ユーザー ID 100

インスタンスID

1001

レベル 10素材カード

ユーザー ID 100

インスタンスID

1002

レベル 1

更新対象が2レコード以上の場合

所有アイテム

ユーザーID

100

所持金 1500G

強化対象カード

ユーザー ID 100

インスタンスID

1001

レベル 10素材カード

ユーザー ID 100

インスタンスID

1002

レベル 1

更新削除

素材カードを消費して強化対象カードのレベルを1上げる。費用として 500G 徴収する。

更新対象が2レコード以上の場合

所有アイテム

ユーザーID

100

所持金 1000G

強化対象カード

ユーザー ID 100

インスタンスID

1001

レベル 10素材カード

ユーザー ID 100

インスタンスID

1002

レベル 1

Step1:所持金 -500G

更新対象が2レコード以上の場合

所有アイテム

ユーザーID

100

所持金 1000G

強化対象カード

ユーザー ID 100

インスタンスID

1001

レベル 10素材カード

ユーザー ID 100

インスタンスID

1002

レベル 1

Step2:素材カードを削除 削除

更新対象が2レコード以上の場合

所有アイテム

ユーザーID

100

所持金 1000G

強化対象カード

ユーザー ID 100

インスタンスID

1001

レベル 10素材カード

ユーザー ID 100

インスタンスID

1002

レベル 1

Step3:レベルアッ・・・

削除済

突然の死

更新対象が2レコード以上の場合

所有アイテム

ユーザーID

100

所持金 1000G

強化対象カード

ユーザー ID 100

インスタンスID

1001

レベル 10素材カード

ユーザー ID 100

インスタンスID

1002

レベル 1

残念!!カードの強化処理はこれで終わってしまった!

削除済

お問い合わせ内容

【ユーザー ID 】 100【ユーザー名】 †ラインハルト†【日時】 2013 年 3 月 16 日 16:25:58【お問い合わせ内容】お金とカードだけ取られた!!!ふざけんな補償しろ!!!

正しい実装パターン

用意するもの

Webサーバー Batchサーバー DynamoDBAmazon SQS

システム構成

DynamoDB

Web Servers

SQS

Batch Servers

1.HTTP Request

2.Put Record

3.Enqueue

4.Dequeue

5.Update Records

2種類のプロセス

Web リクエスト処理 HTTP リクエストをトリガーとして実行される処理 プロセスは Apache によって実行・管理される 途中でエラーが発生したら 503エラーを返して中断さ

れる キュー処理

SQS へメッセージを送り、メッセージの取り出しをトリガーとして実行される処理。

プロセスはアプリケーション用のユーザーで実行される

正常終了するまで何度も繰り返し実行される

Amazon SQS を使う

SQS は、処理の「完遂保証」のために使う 失敗した時は何度でも再実行されることを保証さ

せる キュー処理は最終的に正常終了に収束するよう

実装する 状態遷移図を書いてしっかり机上デバッグ

但し書いたら負けかなと思ってる 図が要らないほどシンプルな実装にしよう

キュー処理実装の鉄則

再実行耐性を持たせる 同じ処理が2回実行されても

結果に影響がでないようにする。並列実行耐性を持たせる

同じ処理が2つ以上のプロセスで並行して実行されても結果に影響がでないようにする。

再実行耐性の実装方法

入力内容から処理内容が全て決定されるようにする。 レコードの更新をする際に、確かに更新されたことが判別できるよう「証拠」を残すようにする。 更新日時を書き込む、ステータス値を変更す

る、 etc 。 レコード内容をみればどこまで処理が終わったかが

分かるようにする。 処理済みであればスキップして次の処理へ進むように

する。 複雑な分岐をさせず、上から流れ落ちるような処理に

する。

並列行耐性の実装方法

条件付きアップデート機能を用いて楽観的ロックを実装する。

更新する前にレコードを「一貫性あり」で読み込む。 レコードを更新するときは、「読み込んだ時点から他

の誰にも更新されていなければ」という条件をつける。 「条件付きアップデート」を使う 必要であればレコードにバージョン番号を導入する

更新に失敗した場合は処理を最初からやりなおす。 → 再実行耐性が実現されていれば問題ないはず!

処理単位のフローチャート

GetItem()

処理済み?UpdateItem() with Condition

throw RetryException次の処理へ

更新成功?

NO

NO

YESYES

キュー処理全体の流れ

処理単位1

開始

終了

処理単位2

処理単位3

流れ落ちるように

実践・改

カードの強化

所有アイテム

ユーザーID

100

所持金 1500G

Case 2':今度こそカードを強化する

強化対象カード

ユーザー ID 100

インスタンスID

1001

レベル 10素材カード

ユーザー ID 100

インスタンスID

1002

レベル 1

カードの強化

所有アイテム

ユーザーID

100

所持金 1500G

カード強化依頼

ユーザー ID 100

依頼 ID 5001

強化対象カード

1001

素材対象カード

1002

強化費用 500G

開始済 NO

Step1 :依頼レコードを作成する

カードの強化

Step2 :依頼 ID を所有アイテムレコードに登録する※STRING_SET 型を使う

所有アイテム

ユーザーID

100

所持金 1500G

未決済 [ 5001 ]

カード強化依頼

ユーザー ID 100

依頼 ID 5001

強化対象カード

1001

素材対象カード

1002

強化費用 500G

開始済 NO

カードの強化

Step3 :依頼 ID を強化対象カードレコードにも登録する※STRING_SET 型を使う

カード強化依頼

ユーザー ID 100

依頼 ID 5001

強化対象カード

1001

素材対象カード

1002

強化費用 500G

開始済 NO

強化対象カード

ユーザー ID 100

インスタンスID

1001

レベル 10

未処理 [ 5001 ]

カードの強化

キューメッセージ

処理種別 カード強化

ユーザーID

100

依頼 ID 5001Step4 :キューメッセージを発行する

カード強化依頼

ユーザー ID 100

依頼 ID 5001

強化対象カード

1001

素材対象カード

1002

強化費用 500G

開始済 NO

カードの強化 ( キュー処理 )

Step5 :レコードを読み込む

所有アイテム

ユーザーID

100

所持金 1500G

未決済 [ 5001 ]

カード強化依頼

ユーザー ID 100

依頼 ID 5001

強化対象カード

1001

素材対象カード

1002

強化費用 500G

開始済 NO

カードの強化 ( キュー処理 )

Step6 :開始済みにする※ 無条件 UPDATE

所有アイテム

ユーザーID

100

所持金 1500G

未決済 [ 5001 ]

カード強化依頼

ユーザー ID 100

依頼 ID 5001

強化対象カード

1001

素材対象カード

1002

強化費用 500G

開始済 YES

カードの強化 ( キュー処理 )

Step7 :決済する※ 条件付き UPDATE を使う

所有アイテム

ユーザーID

100

所持金 1000G

未決済 (NULL)

カード強化依頼

ユーザー ID 100

依頼 ID 5001

強化対象カード

1001

素材対象カード

1002

強化費用 500G

開始済 YES

カードの強化 ( キュー処理 )

Step8 :素材カードを削除

カード強化依頼

ユーザー ID 100

依頼 ID 5001

強化対象カード

1001

素材対象カード

1002

強化費用 500G

開始済 YES

素材カード

ユーザー ID 100

インスタンスID

1002

レベル 1

カードの強化 ( キュー処理 )

Step8 :素材カードを削除

カード強化依頼

ユーザー ID 100

依頼 ID 5001

強化対象カード

1001

素材対象カード

1002

強化費用 500G

開始済 YES

素材カード

ユーザー ID 100

インスタンスID

1002

レベル 1

削除

カードの強化 ( キュー処理 )

Step9 :強化対象カードのパラメータを加算する※ 条件付き UPDATE を使う

カード強化依頼

ユーザー ID 100

依頼 ID 5001

強化対象カード

1001

素材対象カード

1002

強化費用 500G

開始済 YES

強化対象カード

ユーザー ID 100

インスタンスID

1001

レベル 10

未処理 [ 5001 ]

カードの強化 ( キュー処理 )

Step9 :強化対象カードのパラメータを加算する※ 条件付き UPDATE を使う

カード強化依頼

ユーザー ID 100

依頼 ID 5001

強化対象カード

1001

素材対象カード

1002

強化費用 500G

開始済 YES

強化対象カード

ユーザー ID 100

インスタンスID

1001

レベル 11

未処理 ( NULL )

実装の要点

完遂保証のない処理 (Web リクエスト処理 )と、完遂保証のある処理 ( キュー処理 ) で、実行すべき処理を上手に振り分ける。

Web リクエスト処理の途中でエラーが発生しても、キュー処理の実行が開始されない限り「何も起こらなかった」ことになる。 その時は仕方なく 503エラーを返す 各レコードにトランザクション ID が残る可能性について

は、タイムアウト処理を別途実装することで対処する。

本資料について(再掲)

以下の URL からいつでもダウンロード可能です

http://iy-h.com/01/

top related