Your SlideShare is downloading. ×
0
サーバー未経験者が
ソーシャルゲームを通して知った
サーバーの事
2014/2/8 ゲームサーバ勉強会

株式会社gumi
古閑学/@_mamehiko_
自己紹介
古閑 学/@_mamehiko_
株式会社gumi 東京オフィス エンジニア
2013/12で3年目。
肩書きは(名ばかり)スペシャリスト
最近はcocos2d-xでクライアントエンジニア
以前はコンシューマーでプログラマを8年程
2...
gumiって?

自己紹介
自己紹介(gumiでは)

2011

2012

自己紹介

2013

上記サーバーサイドの開発をしてました。
騎士道とドラゴンジェネシスでは元リードエンジニア
今日のお話

今日のお話

上級者
サーバー未経験からソーシャルゲームを
通して得た経験をさらけ出します。
インフラ

開発

コード失敗事例とか
当時の思い込みとか

初心者
アジェンダ

アジェンダ

前提

最後に

トライアル&エラー編

キャッシュ
Redis

2011∼
2012∼

まとめ

ギルドバトル編
マスターデータ
バトル
マッチング
アジェンダ

アジェンダ

前提

最後に

トライアル&エラー編

キャッシュ
Redis

2011∼
2012∼

まとめ

ギルドバトル編
マスターデータ
バトル
マッチング
前提
言語
python2.7

Webフレームワーク
Django1.4以上

DataBase
MySQL5.5

前提
アジェンダ

アジェンダ

前提

最後に

トライアル&エラー編

キャッシュ
Redis

2011∼
2012∼

まとめ

ギルドバトル編
マスターデータ
バトル
マッチング
初めてのソーシャルゲーム
構成

2011∼

TokyoTyrant

ロードバランサ

Appサーバー

MySQL

memcached
RDS(MySQL)
マスターデータ
プレイヤーデータ
ギルドデータ
などが一つのDBに

2011∼
RDS(MySQL)
マスターデータ
プレイヤーデータ
ギルドデータ
などが一つのDBに
つまり全部入り

2011∼
なんか、クエリってのを
減らした方がいいらしい
後、KVSってのがあるらしい
なんか、クエリってのを
減らした方がいいらしい
後、KVSってのがあるらしい

雰囲気でやってた時代!

※あくまで個人の発言です
KVS(TokyoTyrant)

2011∼

TokyoTyrant

ロードバランサ

APサーバー

MySQL

memcached
データの選定
例えばこんなゲーム
プレイヤーには体力がある
体力を消費してクエストを進める
クエストを進めると経験値が入る
経験値が入るとレベルアップする

2011∼
例えばこんなゲーム
プレイヤーには体力がある
体力を消費してクエストを進める
クエストを進めると経験値が入る
経験値が入るとレベルアップする
あるあるソーシャルゲーム

2011∼
更新の高いものをKVSへ
プレイヤーには体力がある
体力を消費してクエストを進める
クエストを進めると経験値が入る
経験値が入るとレベルアップする
SQL減らしたいしね!

2011∼
うまくいった
ようにみえたが。。。
例えばこんなコード

2011∼

#  この中はトランザクション内という仮定

try:

        #  プレイヤーの体⼒力力を消費

        player.consume_̲vitality()
        #  プレイ...
例えばこんなコード

2011∼

#  この中はトランザクション内という仮定

try:

        #  プレイヤーの体⼒力力を消費

        player.consume_̲vitality()
        #  プレイ...
どうなるか
どうなる?
#  この中はトランザクション内という仮定

try:

        #  プレイヤーの体⼒力力を消費

        player.consume_̲vitality()
        #  プレイヤーの経験値アップ

 ...
どうなる?
#  この中はトランザクション内という仮定

try:

        #  プレイヤーの体⼒力力を消費

        player.consume_̲vitality()

2011∼

体力は消費される
経験値付与でエラー...
原因は様々
単純にバグッてる
アクセス過多
サーバーが息をしていないetc...

2011∼
原因は様々
単純にバグッてる
アクセス過多
サーバーが息をしていないetc...
想定外の事が起きるんです

2011∼
回避策
順番を変える

2011∼

#  この中はトランザクション内という仮定

try:

        #  プレイヤーの体⼒力力を消費

        #  player.consume_̲vitality()
        #  プレイ...
ユーザー視点で考える

2011∼

エラーケース
変更前

1.体力も減らないが、経験値も増えない
2.体力だけが減り、経験値は増えない
ユーザー視点で考える

2011∼

エラーケース
変更前

1.体力も減らないが、経験値も増えない
2.体力だけが減り、経験値は増えない

エラーケース

変更後

1.体力も減らないが、経験値も増えない
2.体力は減らないが、経験値は増える...
根本解決ではないが、
回避のテクニック
学んだこと

DBとKVSの整合性は難しい

2011∼
さらにクエリを減らす
構成

2011∼

TokyoTyrant

ロードバランサ

APサーバー

RDS

memcached
参照の多いデータ
マスターデータ
プレイヤー
プレイヤーのカードとか

2011∼
参照の多いデータ
マスターデータ
プレイヤー
プレイヤーのカードとか

軽くする=キャッシュしかないと思ってた

2011∼
あるあるキャッシュバグ

2011∼

更新したはずが昔のデータを参照している
キャッシュ削除忘れ
回避策
更新箇所ではDBから取得
#  プレイヤーデータをDBから取得

2011∼

player  =  player.objects.get(player_̲id=”111”)
更新箇所ではDBから取得

2011∼

#  プレイヤーデータをDBから取得

player  =  player.objects.get(player_̲id=”111”)

DBで不整合を起こす率は減った
ただ、キャッシュから取得している...
学んだこと

キャッシュを多用すると
バグりやすいし、
バグも見つけにくい

2011∼
2011年まとめ

KVSの基本的な使い方を学ぶ

2011∼
アジェンダ

アジェンダ

前提

最後に

トライアル&エラー編

キャッシュ
Redis

2011∼
2012∼

まとめ

ギルドバトル編
マスターデータ
バトル
マッチング
ユーザーが順調に増えてきた
さらに

2012∼
○日後に広告打つんで
さらにユーザー増えますよ!

おぉ、いいっすね!
さらに

2012∼
○日後に広告打つんで
さらにユーザー増えますよ!

おぉ、いいっすね!

負荷大丈夫ですよね?
フカ!

大丈夫です(震え声)
色々きつくなるかも

2012∼

ロードバランサ

Appサーバー

memcached

Redis
RDS
負荷対策を考える

2012∼

ロードバランサ

容易
加は
追
Appサーバー

memcached

Redis
RDS
負荷対策を考える

ロードバランサ

2012∼

使用

方法
の
見直
し

Appサーバー

memcached

Redis
RDS
問題はRDS
負荷対策を考える

2012∼

スケールアップ
サーバーそのものを増強。CPUとかメモリとか。
増強する性能に限界がある

スケールアウト
サーバーの台数を増やす事で処理性能をあげる
色々きつくなるかも

2012∼

ロードバランサ

Appサーバー

memcached

Redis
RDS
どれくらいかがわからない。。
負荷対策を考える

2012∼

スケールアップ
サーバーそのものを増強。CPUとかメモリとか。
増強する性能に限界がある

スケールアウト

採用

サーバーの台数を増やす事で処理性能をあげる
規模不明だし
初期構成

2012∼

マスターデータ
ギルド
プレイヤー
イベント
スケールアウト(垂直)
マスターデータ

ギルド

プレイヤー

イベント

2012∼
スケールアウト(水平)

2012∼

マスターデータ

ギルド

プレイヤー1 プレイヤー2 プレイヤー3 プレイヤー4

イベント
スケールアウト(水平)

2012∼

マスターデータ

シャード
を分割

ギルド

プレイヤー0
〃4
〃8
〃12

イベント

プレイヤー1
〃5
〃9

プレイヤー2
〃6
〃10

〃13

〃14

プレイヤー3
〃7
〃11
〃...
シャードの決定

2012∼

#  プレイヤーIDはユニークである事が前提

player_̲id  =  “hamspamegg”

#  適当なハッシュ関数などで数値にし、シャードの分割数で余りを求める
#  16  =  playerの...
うまくいった
ようにみえたが。。。
障害

2012∼

マスターデータ

ギルド

プレイヤー0
〃4
〃8
〃12

イベント

プレイヤー1
〃5
〃9

プレイヤー2
〃6
〃10

〃13

〃14

プレイヤー3
〃7
〃11
〃15
原因

2012

ある処理だけ分割が効いていなかった
初期化に入れていた空文字が
特定のシャードを指していた
#  プレイヤーIDはユニークである事が前提

player_̲id  =  “”

if  何かの条件:
        play...
どうなる?

2012∼

プライマリキーがAUTO INCREMENTの
IDの場合、同構成のテーブルでも、
各シャードで同じIDが存在する
player_idが空文字列で上書きされ、
元々持っていたユーザーからは
特定できなくなる
どうなる?

2012∼

プライマリキーがAUTO INCREMENTの
IDの場合、同構成のテーブルでも、
各シャードで同じIDが存在する
player_idが空文字列で上書きされ、
元々持っていたユーザーからは
特定できなくなる

つまり...
復活
プレイヤーの行動ログから、
想定されるデータの洗い出し
ただ、残っていないログもあり、
完全な復活は難しかった

2012∼
学んだこと

2012∼

スケールアウトは原因の特定が困難な事も
入念なデバッグと、ログを仕込もう
引き続き分割(おまけ)
ユーザー数の減少。。

2012∼
ユーザー数の減少。。

2012∼

負荷は下
がる
ユーザー数の減少。。

2012∼

負荷は下
がる

が

コストが
かかる!

!
RDSはコストかかる。。

2012∼

マスターデータ

ギルド

プレイヤー0
〃4
〃8
〃12

イベント

プレイヤー1
〃5
〃9

プレイヤー2
〃6
〃10

〃13

〃14

プレイヤー3
〃7
〃11
〃15
統合

2012∼

マスターデータ

シャード

ギルド

プレイヤー0
〃4
〃8
〃12

イベント

プレイヤー1
〃5
〃9
〃13

プレイヤー2
〃6
〃10
〃14

そのまま

プレイヤー3
〃7
〃11
〃15

コスト削減
逆を言えば
分割も楽

2012∼

マスターデータ

シャード

そのまま

ギルド

プレイヤー0
〃4
〃8
〃12

イベント

プレイヤー1
〃5
〃9

プレイヤー2
〃6
〃10

〃13

〃14

プレイヤー3
〃7
〃11

アプリのソ...
学んだこと

負荷が少なくとも、
スケール可能な設計にしよう

2012∼
2012年まとめ

DBの分割について学ぶ

2012∼
ここまでが主なトライアル&エラー
アジェンダ

アジェンダ

前提

最後に

トライアル&エラー編

キャッシュ
Redis

2011∼
2012∼

まとめ

ギルドバトル編
マスターデータ
バトル
マッチング
集大成
いつもの会話
25人vs25人のギルドバトル
をしたいんだけど

おぉ、いいっすね!

ギルドバトル
いつもの会話
25人vs25人のギルドバトル
をしたいんだけど

おぉ、いいっすね!

負荷大丈夫ですよね?
フカ!

大丈夫です(震え声)

ギルドバトル
例えばこんなバトル

ギルドバトル

ギルドvsギルド
プレイヤーにはHP、行動力、攻撃力等がある
行動力を消費して別のプレイヤーを攻撃する
対象プレイヤーは一人の時もあれば複数もある
与えたダメージはギルドにポイントとして入る
基本構成

マスタデータ

マスターデータ

ギルド

プレイヤー0
〃4
〃8
〃12

プレイヤー1
〃5
〃9

プレイヤー2
〃6
〃10

〃13

〃14

プレイヤー3
〃7
〃11
〃15
改善
基本構成

マスタデータ

昔からあるこれ

マスターデータ

ギルド

プレイヤー0
〃4
〃8
〃12

プレイヤー1
〃5
〃9

プレイヤー2
〃6
〃10

〃13

〃14

プレイヤー3
〃7
〃11
〃15
マスターデータ

マスタデータ

今まではjsonをマスターDBにいれて参照
キャッシュがあれば、キャッシュから取得
参照度は一番高い
Appサーバーでのメモ化とかも
マスターデータ

マスタデータ

Appサーバー

マスターデータ
③

ロードバランサ

①
②
memcached

① Appサーバーのメモリにアクセス
② キャッシュにアクセス
③ DBにアクセス
というのが2012まで
マスターデータ
Appサーバー
(マスターデータ)

ロードバランサ

マスタデータ

マスターデータ

①
memcached

① Appサーバーにマスターデータがある!!
どういうこと?
以前

マスタデータ

1. jsonの内容をDBに保存
2. DBにアクセスしてデータを取得
どういうこと?
以前

今

マスタデータ

1. jsonの内容をDBに保存
2. DBにアクセスしてデータを取得

jsonをAppサーバーに展開
マスターデータ

マスタデータ

Appサーバー

全てがマスターデータを持つ
ロードバランサ

Appサーバーで完結するので高速
デメリット?

マスタデータ

Appサーバーでのプロセスが大きくなる
が、約1年運用した結果でも今の所問題なし

デプロイ時にメモリは解放されます
というわけで
基本構成
マスターデータ

マスタデータ

マスターデー

タのDBを使わ

なくなった

ギルド

プレイヤー0
〃4
〃8
〃12

プレイヤー1
〃5
〃9

プレイヤー2
〃6
〃10

〃13

〃14

実際はソースの名残で一部使っ...
アジェンダ

アジェンダ

前提

最後に

トライアル&エラー編

キャッシュ
Redis

2011∼
2012∼

まとめ

ギルドバトル編
マスターデータ
バトル
マッチング
今まで通りにやると。。。
単体攻撃

バトル

ギルド

プレイヤー0
〃4
〃8
〃12

プレイヤー1
〃5
〃9

プレイヤー2
〃6
〃10

〃13

〃14

Attack!!

プレイヤー3
〃7
〃11
〃15
複数攻撃

バトル

最大17

箇所への

アクセス
!

ギルド

プレイヤー0
〃4
〃8
〃12

プレイヤー1
〃5
〃9

プレイヤー2
〃6
〃10

プレイヤー3
〃7
〃11

〃13

〃14

〃15

Attack!!x...
さらに、ギルドバトルだと
同時に起きる可能性も

これでもまだ半分以下

バトル
見るからにきつい
問題点

バトル

対象のDBが多いと、管理が難しくなる
アクセスが大変。というかしたくない
対応策
ギルドバトル専用DB

バトル

ギルド

プレイヤー0
〃4
〃8
〃12

プレイヤー1
〃5
〃9

プレイヤー2
〃6
〃10

〃13

〃14

ギルドバトル1

ギルドバトル2

〃3
〃5

〃4
〃6

〃7

〃8

New...
必要なデータの選定

バトル

ギルド
ギルドメンバーのレベル
ギルドメンバーの職業
ギルドメンバーのカード
ギルドメンバーのカードのレベルとかとか
マッチング

バトル

ギルド

バッチサーバー
プレイヤー

バトルの数十分前にcronでバッチが流れる
対戦ギルドの組み合わせを決める
マッチング

バトル

ギルド
#  マッチングIDの発⾏行行

matching_̲id  =  uuid4()

バッチサーバー
プレイヤー

対戦の組み合わせごとに
マッチングID(UUID)を発行する
マッチング

バトル

ギルド

スナップショット
ギルドバトル

バッチサーバー
プレイヤー

マッチングIDを元にギルドバトルDBを選択し、
スナップショットを取る
分割特定はプレイヤーDBの
特定と同じロジック
閉じた戦い

バトル

ギルドA

ギルドB

ギルドC

ギルドD

ギルドE

ギルドF

ギルドバトルDB
うまくいった
が
まだ問題が

一つのDBに集まったとはいえ、
同時に攻撃した場合に問題が起きる
レース・コンディション

バトル
レースコンディション
mame

バトル

hiko

体力100

体力100

mameとhikoのデータを取得

mameとhikoのデータを取得
hikoに攻撃
mameに攻撃
save()
save()
mame視点

バトル

mame

hiko

体力100

体力100

mameとhikoのデータを取得

mameとhikoのデータを取得
ここで攻撃
したから

hikoに攻撃
mameに攻撃

hikoの体力は
save()
100...
一方。。

バトル

mame

hiko

体力100

体力100

mameとhikoのデータを取得

mameとhikoのデータを取得
ここで攻撃
したから

体力100のデータ
を取得

hikoに攻撃
mameに攻撃

hikoの体...
実際は。。

バトル

mame

hiko

体力100

体力100

mameとhikoのデータを取得
た事に
っ

が無か
攻撃
る!
な
ここで攻撃
したから

mameとhikoのデータを取得
体力100のデータ
を取得

hiko...
対応策
唯一の共通オブジェクト
GuildBattleManager
matching_id
ギルドA

ギルドB

バトル
唯一の共通オブジェクト

バトル

更新処理は必ずManagerを通す
Managerで行ロックをかける
共通オブジェクトなのでデッドロック無し
順番

バトル
mame

hiko

体力100

体力100
一旦処理が止められ

Manager
mameとhikoのデータを取得

hikoに攻撃

save()
順番

バトル
mame

hiko

体力100

体力100
mameの処理終了後に流れ出す

Manager
mameとhikoのデータを取得
mameとhikoのデータを取得
hikoに攻撃

mameに攻撃

save()
save(...
順番

バトル
mame

hiko

体力100

体力100
mameの処理終了後に流れ出す

Manager
mameとhikoのデータを取得
mameとhikoのデータを取得
hikoに攻撃

mameに攻撃

トランザクションは必須
...
うまくいった
本当に!
これで全てが終わったかに見えた
アジェンダ

アジェンダ

前提

最後に

トライアル&エラー編

キャッシュ
Redis

2011∼
2012∼

まとめ

ギルドバトル編
マスターデータ
バトル
マッチング
終わらないマッチング
日ごとに増えるユーザー
日ごとに増えるデータ
日ごとに延びるマッチング時間
日ごとに短くなる睡眠時間

マッチング
改善

マッチング

ギルド

スナップショット
ギルドバトル

バッチサーバー
プレイヤー

バッチサーバーから直接ギルドバトルDBにコピーしていた
改善

マッチング

ギルド

#  ギルドIDの対戦リスト

guild_̲ids  =  ([1,  2],[3,4],[5,6])

バッチサーバー

Redis

プレイヤー

Redisに
対戦ギルドの組み合わせのIDのみのリスト
を...
改善

マッチング

ギルド

バッチサーバー
プレイヤー

Redis

ジョブサーバー

Redisに対戦リストが入っていないかを常に問い合わせる
改善

マッチング

ギルド

,2] [5,6]
[1
[3,4
]
ギルドバトル

バッチサーバー

Redis

ジョブサーバー

プレイヤー

Redisに入っている対戦リストから
組み合わせのIDをポップし、
並列でスナップショットを...
終わるマッチング

マッチング

Redisのデータ操作は
アトミック性が保証されている
対戦リストが増えて処理が終わらなくなったら
ジョブサーバーを増やす
マッチング時間が5分の1に

俺が泣いた
アジェンダ

アジェンダ

前提

最後に

トライアル&エラー編

キャッシュ
Redis

2011∼
2012∼

まとめ

ギルドバトル編
マスターデータ
バトル
マッチング
キャッシュ

最後に

余り使っていない
キャッシュが残るバグは今でもある
今までは重いクエリをごまかしていた
それよりもDBのindexを適切に張る
Jetprofiler

重いクエリを検知してくれる
indexミスなのでアクセス障害が
起きた時などに重宝した

最近は使ってないかもしれない
ですが、、

最後に
Redis

最後に

ランキングや1日1回フラグなどに使用
expireを設定するとメモリの節約にもなる
消えても痛くないデータだが、
なるべく永続的に残したいもの
アジェンダ

アジェンダ

前提

最後に

トライアル&エラー編

キャッシュ
Redis

2011∼
2012∼

まとめ

ギルドバトル編
マスターデータ
バトル
マッチング
まとめ

まとめ

DBは規模によらずスケールアウト前提で
最初からKVSに手を出さない
DBで効率が悪そうなもので考える
キャッシュは使わないという選択肢
色々あるけどまとめきれず
ご清聴ありがとうございました
Upcoming SlideShare
Loading in...5
×

サーバー未経験者がソーシャルゲームを通して知ったサーバーの事

52,612

Published on

2014/2/8に行ったゲームサーバ勉強会でのスライドです。
サーバー未経験者がソーシャルゲームを通して知ったサーバーの事。
失敗経験を元に何故今がこうなっているかというのを詰め込みました。
初心者〜中級者向け勉強会だったので、なるべく非エンジニアでもイメージで伝わるようにちょっとだけ心がけてます。

Published in: Technology
2 Comments
234 Likes
Statistics
Notes
  • すごい!!!!!!!!!!
    翻訳は全然予想外だったのでうれしいです!
    ありがとうございます!!!!
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • 韓国語に翻訳してみました。
    http://www.slideshare.net/flashscope/ss-39227943
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
No Downloads
Views
Total Views
52,612
On Slideshare
0
From Embeds
0
Number of Embeds
24
Actions
Shares
0
Downloads
262
Comments
2
Likes
234
Embeds 0
No embeds

No notes for slide

Transcript of "サーバー未経験者がソーシャルゲームを通して知ったサーバーの事"

  1. 1. サーバー未経験者が ソーシャルゲームを通して知った サーバーの事 2014/2/8 ゲームサーバ勉強会 株式会社gumi 古閑学/@_mamehiko_
  2. 2. 自己紹介 古閑 学/@_mamehiko_ 株式会社gumi 東京オフィス エンジニア 2013/12で3年目。 肩書きは(名ばかり)スペシャリスト 最近はcocos2d-xでクライアントエンジニア 以前はコンシューマーでプログラマを8年程 2児の娘のパパ 会社外で話すのは初めて 自己紹介
  3. 3. gumiって? 自己紹介
  4. 4. 自己紹介(gumiでは) 2011 2012 自己紹介 2013 上記サーバーサイドの開発をしてました。 騎士道とドラゴンジェネシスでは元リードエンジニア
  5. 5. 今日のお話 今日のお話 上級者 サーバー未経験からソーシャルゲームを 通して得た経験をさらけ出します。 インフラ 開発 コード失敗事例とか 当時の思い込みとか 初心者
  6. 6. アジェンダ アジェンダ 前提 最後に トライアル&エラー編 キャッシュ Redis 2011∼ 2012∼ まとめ ギルドバトル編 マスターデータ バトル マッチング
  7. 7. アジェンダ アジェンダ 前提 最後に トライアル&エラー編 キャッシュ Redis 2011∼ 2012∼ まとめ ギルドバトル編 マスターデータ バトル マッチング
  8. 8. 前提 言語 python2.7 Webフレームワーク Django1.4以上 DataBase MySQL5.5 前提
  9. 9. アジェンダ アジェンダ 前提 最後に トライアル&エラー編 キャッシュ Redis 2011∼ 2012∼ まとめ ギルドバトル編 マスターデータ バトル マッチング
  10. 10. 初めてのソーシャルゲーム
  11. 11. 構成 2011∼ TokyoTyrant ロードバランサ Appサーバー MySQL memcached
  12. 12. RDS(MySQL) マスターデータ プレイヤーデータ ギルドデータ などが一つのDBに 2011∼
  13. 13. RDS(MySQL) マスターデータ プレイヤーデータ ギルドデータ などが一つのDBに つまり全部入り 2011∼
  14. 14. なんか、クエリってのを 減らした方がいいらしい 後、KVSってのがあるらしい
  15. 15. なんか、クエリってのを 減らした方がいいらしい 後、KVSってのがあるらしい 雰囲気でやってた時代! ※あくまで個人の発言です
  16. 16. KVS(TokyoTyrant) 2011∼ TokyoTyrant ロードバランサ APサーバー MySQL memcached
  17. 17. データの選定
  18. 18. 例えばこんなゲーム プレイヤーには体力がある 体力を消費してクエストを進める クエストを進めると経験値が入る 経験値が入るとレベルアップする 2011∼
  19. 19. 例えばこんなゲーム プレイヤーには体力がある 体力を消費してクエストを進める クエストを進めると経験値が入る 経験値が入るとレベルアップする あるあるソーシャルゲーム 2011∼
  20. 20. 更新の高いものをKVSへ プレイヤーには体力がある 体力を消費してクエストを進める クエストを進めると経験値が入る 経験値が入るとレベルアップする SQL減らしたいしね! 2011∼
  21. 21. うまくいった
  22. 22. ようにみえたが。。。
  23. 23. 例えばこんなコード 2011∼ #  この中はトランザクション内という仮定 try:        #  プレイヤーの体⼒力力を消費        player.consume_̲vitality()        #  プレイヤーの経験値アップ        player.add_̲experience() except:        #  エラー起きたらDBをロールバック        transaction.rollback() else:        #  問題なければDB更更新。経験値が増える。        transaction.commit() 体力→KVS 経験値→DB ※実際のコードとは異なります
  24. 24. 例えばこんなコード 2011∼ #  この中はトランザクション内という仮定 try:        #  プレイヤーの体⼒力力を消費        player.consume_̲vitality()        #  プレイヤーの経験値アップ        player.add_̲experience() except: ←ここでエラー        #  エラー起きたらDBをロールバック        transaction.rollback() else:        #  問題なければDB更更新。経験値が増える。        transaction.commit() 体力→KVS 経験値→DB
  25. 25. どうなるか
  26. 26. どうなる? #  この中はトランザクション内という仮定 try:        #  プレイヤーの体⼒力力を消費        player.consume_̲vitality()        #  プレイヤーの経験値アップ        player.add_̲experience() except:        #  エラー起きたらDBをロールバック        transaction.rollback() else:        #  問題なければDB更更新。経験値が増える。        transaction.commit() 2011∼ 体力は消費される 経験値付与でエラーが起きる
  27. 27. どうなる? #  この中はトランザクション内という仮定 try:        #  プレイヤーの体⼒力力を消費        player.consume_̲vitality() 2011∼ 体力は消費される 経験値付与でエラーが起きる        #  プレイヤーの経験値アップ        player.add_̲experience() except:        #  エラー起きたらDBをロールバック        transaction.rollback() else:        #  問題なければDB更更新。経験値が増える。        transaction.commit() 体力だけが消費される ユーザーの不利益となる
  28. 28. 原因は様々 単純にバグッてる アクセス過多 サーバーが息をしていないetc... 2011∼
  29. 29. 原因は様々 単純にバグッてる アクセス過多 サーバーが息をしていないetc... 想定外の事が起きるんです 2011∼
  30. 30. 回避策
  31. 31. 順番を変える 2011∼ #  この中はトランザクション内という仮定 try:        #  プレイヤーの体⼒力力を消費        #  player.consume_̲vitality()        #  プレイヤーの経験値アップ        player.add_̲experience() except:        #  エラー起きたらDBをロールバック        transaction.rollback() else:        #  問題なければDB更更新。経験値が増える。        transaction.commit() #  プレイヤーの体⼒力力を最後に消費 player.consume_̲vitality() DB更新後に移動
  32. 32. ユーザー視点で考える 2011∼ エラーケース 変更前 1.体力も減らないが、経験値も増えない 2.体力だけが減り、経験値は増えない
  33. 33. ユーザー視点で考える 2011∼ エラーケース 変更前 1.体力も減らないが、経験値も増えない 2.体力だけが減り、経験値は増えない エラーケース 変更後 1.体力も減らないが、経験値も増えない 2.体力は減らないが、経験値は増える ユーザーにはお得!!
  34. 34. 根本解決ではないが、 回避のテクニック
  35. 35. 学んだこと DBとKVSの整合性は難しい 2011∼
  36. 36. さらにクエリを減らす
  37. 37. 構成 2011∼ TokyoTyrant ロードバランサ APサーバー RDS memcached
  38. 38. 参照の多いデータ マスターデータ プレイヤー プレイヤーのカードとか 2011∼
  39. 39. 参照の多いデータ マスターデータ プレイヤー プレイヤーのカードとか 軽くする=キャッシュしかないと思ってた 2011∼
  40. 40. あるあるキャッシュバグ 2011∼ 更新したはずが昔のデータを参照している キャッシュ削除忘れ
  41. 41. 回避策
  42. 42. 更新箇所ではDBから取得 #  プレイヤーデータをDBから取得 2011∼ player  =  player.objects.get(player_̲id=”111”)
  43. 43. 更新箇所ではDBから取得 2011∼ #  プレイヤーデータをDBから取得 player  =  player.objects.get(player_̲id=”111”) DBで不整合を起こす率は減った ただ、キャッシュから取得している所では タイミング次第で表示ずれが起きる
  44. 44. 学んだこと キャッシュを多用すると バグりやすいし、 バグも見つけにくい 2011∼
  45. 45. 2011年まとめ KVSの基本的な使い方を学ぶ 2011∼
  46. 46. アジェンダ アジェンダ 前提 最後に トライアル&エラー編 キャッシュ Redis 2011∼ 2012∼ まとめ ギルドバトル編 マスターデータ バトル マッチング
  47. 47. ユーザーが順調に増えてきた
  48. 48. さらに 2012∼ ○日後に広告打つんで さらにユーザー増えますよ! おぉ、いいっすね!
  49. 49. さらに 2012∼ ○日後に広告打つんで さらにユーザー増えますよ! おぉ、いいっすね! 負荷大丈夫ですよね? フカ! 大丈夫です(震え声)
  50. 50. 色々きつくなるかも 2012∼ ロードバランサ Appサーバー memcached Redis RDS
  51. 51. 負荷対策を考える 2012∼ ロードバランサ 容易 加は 追 Appサーバー memcached Redis RDS
  52. 52. 負荷対策を考える ロードバランサ 2012∼ 使用 方法 の 見直 し Appサーバー memcached Redis RDS
  53. 53. 問題はRDS
  54. 54. 負荷対策を考える 2012∼ スケールアップ サーバーそのものを増強。CPUとかメモリとか。 増強する性能に限界がある スケールアウト サーバーの台数を増やす事で処理性能をあげる
  55. 55. 色々きつくなるかも 2012∼ ロードバランサ Appサーバー memcached Redis RDS
  56. 56. どれくらいかがわからない。。
  57. 57. 負荷対策を考える 2012∼ スケールアップ サーバーそのものを増強。CPUとかメモリとか。 増強する性能に限界がある スケールアウト 採用 サーバーの台数を増やす事で処理性能をあげる 規模不明だし
  58. 58. 初期構成 2012∼ マスターデータ ギルド プレイヤー イベント
  59. 59. スケールアウト(垂直) マスターデータ ギルド プレイヤー イベント 2012∼
  60. 60. スケールアウト(水平) 2012∼ マスターデータ ギルド プレイヤー1 プレイヤー2 プレイヤー3 プレイヤー4 イベント
  61. 61. スケールアウト(水平) 2012∼ マスターデータ シャード を分割 ギルド プレイヤー0 〃4 〃8 〃12 イベント プレイヤー1 〃5 〃9 プレイヤー2 〃6 〃10 〃13 〃14 プレイヤー3 〃7 〃11 〃15
  62. 62. シャードの決定 2012∼ #  プレイヤーIDはユニークである事が前提 player_̲id  =  “hamspamegg” #  適当なハッシュ関数などで数値にし、シャードの分割数で余りを求める #  16  =  playerのDBの総シャード数 #  0〜~15の値が取得出来る player_̲db_̲number  =  _̲hash(player_̲id)  %  16 ※実際のコードとは異なります
  63. 63. うまくいった
  64. 64. ようにみえたが。。。
  65. 65. 障害 2012∼ マスターデータ ギルド プレイヤー0 〃4 〃8 〃12 イベント プレイヤー1 〃5 〃9 プレイヤー2 〃6 〃10 〃13 〃14 プレイヤー3 〃7 〃11 〃15
  66. 66. 原因 2012 ある処理だけ分割が効いていなかった 初期化に入れていた空文字が 特定のシャードを指していた #  プレイヤーIDはユニークである事が前提 player_̲id  =  “” if  何かの条件:        player_̲id  =  getHoge() else:        #  偽の処理理....  player_̲idが””のまま        ... player_̲db_̲number  =  _̲hash(player_̲id)  %  16
  67. 67. どうなる? 2012∼ プライマリキーがAUTO INCREMENTの IDの場合、同構成のテーブルでも、 各シャードで同じIDが存在する player_idが空文字列で上書きされ、 元々持っていたユーザーからは 特定できなくなる
  68. 68. どうなる? 2012∼ プライマリキーがAUTO INCREMENTの IDの場合、同構成のテーブルでも、 各シャードで同じIDが存在する player_idが空文字列で上書きされ、 元々持っていたユーザーからは 特定できなくなる つまり、データが消える!!
  69. 69. 復活 プレイヤーの行動ログから、 想定されるデータの洗い出し ただ、残っていないログもあり、 完全な復活は難しかった 2012∼
  70. 70. 学んだこと 2012∼ スケールアウトは原因の特定が困難な事も 入念なデバッグと、ログを仕込もう
  71. 71. 引き続き分割(おまけ)
  72. 72. ユーザー数の減少。。 2012∼
  73. 73. ユーザー数の減少。。 2012∼ 負荷は下 がる
  74. 74. ユーザー数の減少。。 2012∼ 負荷は下 がる が コストが かかる! !
  75. 75. RDSはコストかかる。。 2012∼ マスターデータ ギルド プレイヤー0 〃4 〃8 〃12 イベント プレイヤー1 〃5 〃9 プレイヤー2 〃6 〃10 〃13 〃14 プレイヤー3 〃7 〃11 〃15
  76. 76. 統合 2012∼ マスターデータ シャード ギルド プレイヤー0 〃4 〃8 〃12 イベント プレイヤー1 〃5 〃9 〃13 プレイヤー2 〃6 〃10 〃14 そのまま プレイヤー3 〃7 〃11 〃15 コスト削減
  77. 77. 逆を言えば
  78. 78. 分割も楽 2012∼ マスターデータ シャード そのまま ギルド プレイヤー0 〃4 〃8 〃12 イベント プレイヤー1 〃5 〃9 プレイヤー2 〃6 〃10 〃13 〃14 プレイヤー3 〃7 〃11 アプリのソースに 変更いらず 〃15
  79. 79. 学んだこと 負荷が少なくとも、 スケール可能な設計にしよう 2012∼
  80. 80. 2012年まとめ DBの分割について学ぶ 2012∼
  81. 81. ここまでが主なトライアル&エラー
  82. 82. アジェンダ アジェンダ 前提 最後に トライアル&エラー編 キャッシュ Redis 2011∼ 2012∼ まとめ ギルドバトル編 マスターデータ バトル マッチング
  83. 83. 集大成
  84. 84. いつもの会話 25人vs25人のギルドバトル をしたいんだけど おぉ、いいっすね! ギルドバトル
  85. 85. いつもの会話 25人vs25人のギルドバトル をしたいんだけど おぉ、いいっすね! 負荷大丈夫ですよね? フカ! 大丈夫です(震え声) ギルドバトル
  86. 86. 例えばこんなバトル ギルドバトル ギルドvsギルド プレイヤーにはHP、行動力、攻撃力等がある 行動力を消費して別のプレイヤーを攻撃する 対象プレイヤーは一人の時もあれば複数もある 与えたダメージはギルドにポイントとして入る
  87. 87. 基本構成 マスタデータ マスターデータ ギルド プレイヤー0 〃4 〃8 〃12 プレイヤー1 〃5 〃9 プレイヤー2 〃6 〃10 〃13 〃14 プレイヤー3 〃7 〃11 〃15
  88. 88. 改善
  89. 89. 基本構成 マスタデータ 昔からあるこれ マスターデータ ギルド プレイヤー0 〃4 〃8 〃12 プレイヤー1 〃5 〃9 プレイヤー2 〃6 〃10 〃13 〃14 プレイヤー3 〃7 〃11 〃15
  90. 90. マスターデータ マスタデータ 今まではjsonをマスターDBにいれて参照 キャッシュがあれば、キャッシュから取得 参照度は一番高い Appサーバーでのメモ化とかも
  91. 91. マスターデータ マスタデータ Appサーバー マスターデータ ③ ロードバランサ ① ② memcached ① Appサーバーのメモリにアクセス ② キャッシュにアクセス ③ DBにアクセス
  92. 92. というのが2012まで
  93. 93. マスターデータ Appサーバー (マスターデータ) ロードバランサ マスタデータ マスターデータ ① memcached ① Appサーバーにマスターデータがある!!
  94. 94. どういうこと? 以前 マスタデータ 1. jsonの内容をDBに保存 2. DBにアクセスしてデータを取得
  95. 95. どういうこと? 以前 今 マスタデータ 1. jsonの内容をDBに保存 2. DBにアクセスしてデータを取得 jsonをAppサーバーに展開
  96. 96. マスターデータ マスタデータ Appサーバー 全てがマスターデータを持つ ロードバランサ Appサーバーで完結するので高速
  97. 97. デメリット? マスタデータ Appサーバーでのプロセスが大きくなる が、約1年運用した結果でも今の所問題なし デプロイ時にメモリは解放されます
  98. 98. というわけで
  99. 99. 基本構成 マスターデータ マスタデータ マスターデー タのDBを使わ なくなった ギルド プレイヤー0 〃4 〃8 〃12 プレイヤー1 〃5 〃9 プレイヤー2 〃6 〃10 〃13 〃14 実際はソースの名残で一部使ってますが... プレイヤー3 〃7 〃11 〃15
  100. 100. アジェンダ アジェンダ 前提 最後に トライアル&エラー編 キャッシュ Redis 2011∼ 2012∼ まとめ ギルドバトル編 マスターデータ バトル マッチング
  101. 101. 今まで通りにやると。。。
  102. 102. 単体攻撃 バトル ギルド プレイヤー0 〃4 〃8 〃12 プレイヤー1 〃5 〃9 プレイヤー2 〃6 〃10 〃13 〃14 Attack!! プレイヤー3 〃7 〃11 〃15
  103. 103. 複数攻撃 バトル 最大17 箇所への アクセス ! ギルド プレイヤー0 〃4 〃8 〃12 プレイヤー1 〃5 〃9 プレイヤー2 〃6 〃10 プレイヤー3 〃7 〃11 〃13 〃14 〃15 Attack!!x16
  104. 104. さらに、ギルドバトルだと
  105. 105. 同時に起きる可能性も これでもまだ半分以下 バトル
  106. 106. 見るからにきつい
  107. 107. 問題点 バトル 対象のDBが多いと、管理が難しくなる アクセスが大変。というかしたくない
  108. 108. 対応策
  109. 109. ギルドバトル専用DB バトル ギルド プレイヤー0 〃4 〃8 〃12 プレイヤー1 〃5 〃9 プレイヤー2 〃6 〃10 〃13 〃14 ギルドバトル1 ギルドバトル2 〃3 〃5 〃4 〃6 〃7 〃8 New!! プレイヤー3 〃7 〃11 〃15
  110. 110. 必要なデータの選定 バトル ギルド ギルドメンバーのレベル ギルドメンバーの職業 ギルドメンバーのカード ギルドメンバーのカードのレベルとかとか
  111. 111. マッチング バトル ギルド バッチサーバー プレイヤー バトルの数十分前にcronでバッチが流れる 対戦ギルドの組み合わせを決める
  112. 112. マッチング バトル ギルド #  マッチングIDの発⾏行行 matching_̲id  =  uuid4() バッチサーバー プレイヤー 対戦の組み合わせごとに マッチングID(UUID)を発行する
  113. 113. マッチング バトル ギルド スナップショット ギルドバトル バッチサーバー プレイヤー マッチングIDを元にギルドバトルDBを選択し、 スナップショットを取る 分割特定はプレイヤーDBの 特定と同じロジック
  114. 114. 閉じた戦い バトル ギルドA ギルドB ギルドC ギルドD ギルドE ギルドF ギルドバトルDB
  115. 115. うまくいった
  116. 116.
  117. 117. まだ問題が 一つのDBに集まったとはいえ、 同時に攻撃した場合に問題が起きる レース・コンディション バトル
  118. 118. レースコンディション mame バトル hiko 体力100 体力100 mameとhikoのデータを取得 mameとhikoのデータを取得 hikoに攻撃 mameに攻撃 save() save()
  119. 119. mame視点 バトル mame hiko 体力100 体力100 mameとhikoのデータを取得 mameとhikoのデータを取得 ここで攻撃 したから hikoに攻撃 mameに攻撃 hikoの体力は save() 100未満(のはず) save()
  120. 120. 一方。。 バトル mame hiko 体力100 体力100 mameとhikoのデータを取得 mameとhikoのデータを取得 ここで攻撃 したから 体力100のデータ を取得 hikoに攻撃 mameに攻撃 hikoの体力は save() 100未満(のはず) save() 体力100のまま save
  121. 121. 実際は。。 バトル mame hiko 体力100 体力100 mameとhikoのデータを取得 た事に っ が無か 攻撃 る! な ここで攻撃 したから mameとhikoのデータを取得 体力100のデータ を取得 hikoに攻撃 mameに攻撃 hikoの体力は save() 100未満(のはず) save() 体力100のまま save
  122. 122. 対応策
  123. 123. 唯一の共通オブジェクト GuildBattleManager matching_id ギルドA ギルドB バトル
  124. 124. 唯一の共通オブジェクト バトル 更新処理は必ずManagerを通す Managerで行ロックをかける 共通オブジェクトなのでデッドロック無し
  125. 125. 順番 バトル mame hiko 体力100 体力100 一旦処理が止められ Manager mameとhikoのデータを取得 hikoに攻撃 save()
  126. 126. 順番 バトル mame hiko 体力100 体力100 mameの処理終了後に流れ出す Manager mameとhikoのデータを取得 mameとhikoのデータを取得 hikoに攻撃 mameに攻撃 save() save() 攻撃を受けた後のデータが 取得される
  127. 127. 順番 バトル mame hiko 体力100 体力100 mameの処理終了後に流れ出す Manager mameとhikoのデータを取得 mameとhikoのデータを取得 hikoに攻撃 mameに攻撃 トランザクションは必須 save() save() 攻撃を受けた後のデータが 取得される
  128. 128. うまくいった
  129. 129. 本当に!
  130. 130. これで全てが終わったかに見えた
  131. 131. アジェンダ アジェンダ 前提 最後に トライアル&エラー編 キャッシュ Redis 2011∼ 2012∼ まとめ ギルドバトル編 マスターデータ バトル マッチング
  132. 132. 終わらないマッチング 日ごとに増えるユーザー 日ごとに増えるデータ 日ごとに延びるマッチング時間 日ごとに短くなる睡眠時間 マッチング
  133. 133. 改善 マッチング ギルド スナップショット ギルドバトル バッチサーバー プレイヤー バッチサーバーから直接ギルドバトルDBにコピーしていた
  134. 134. 改善 マッチング ギルド #  ギルドIDの対戦リスト guild_̲ids  =  ([1,  2],[3,4],[5,6]) バッチサーバー Redis プレイヤー Redisに 対戦ギルドの組み合わせのIDのみのリスト を入れる
  135. 135. 改善 マッチング ギルド バッチサーバー プレイヤー Redis ジョブサーバー Redisに対戦リストが入っていないかを常に問い合わせる
  136. 136. 改善 マッチング ギルド ,2] [5,6] [1 [3,4 ] ギルドバトル バッチサーバー Redis ジョブサーバー プレイヤー Redisに入っている対戦リストから 組み合わせのIDをポップし、 並列でスナップショットを取る スナップショット
  137. 137. 終わるマッチング マッチング Redisのデータ操作は アトミック性が保証されている 対戦リストが増えて処理が終わらなくなったら ジョブサーバーを増やす
  138. 138. マッチング時間が5分の1に 俺が泣いた
  139. 139. アジェンダ アジェンダ 前提 最後に トライアル&エラー編 キャッシュ Redis 2011∼ 2012∼ まとめ ギルドバトル編 マスターデータ バトル マッチング
  140. 140. キャッシュ 最後に 余り使っていない キャッシュが残るバグは今でもある 今までは重いクエリをごまかしていた それよりもDBのindexを適切に張る
  141. 141. Jetprofiler 重いクエリを検知してくれる indexミスなのでアクセス障害が 起きた時などに重宝した 最近は使ってないかもしれない ですが、、 最後に
  142. 142. Redis 最後に ランキングや1日1回フラグなどに使用 expireを設定するとメモリの節約にもなる 消えても痛くないデータだが、 なるべく永続的に残したいもの
  143. 143. アジェンダ アジェンダ 前提 最後に トライアル&エラー編 キャッシュ Redis 2011∼ 2012∼ まとめ ギルドバトル編 マスターデータ バトル マッチング
  144. 144. まとめ まとめ DBは規模によらずスケールアウト前提で 最初からKVSに手を出さない DBで効率が悪そうなもので考える キャッシュは使わないという選択肢 色々あるけどまとめきれず
  145. 145. ご清聴ありがとうございました
  1. A particular slide catching your eye?

    Clipping is a handy way to collect important slides you want to go back to later.

×