• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
サーバー未経験者がソーシャルゲームを通して知ったサーバーの事
 

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

on

  • 47,016 views

2014/2/8に行ったゲームサーバ勉強会でのスライドです。 ...

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

Statistics

Views

Total Views
47,016
Views on SlideShare
46,171
Embed Views
845

Actions

Likes
201
Downloads
191
Comments
0

21 Embeds 845

https://twitter.com 462
http://baba-s.hatenablog.com 144
http://s.deeeki.com 64
https://cybozulive.com 51
https://www.chatwork.com 28
http://wpdustbox.wordpress.com 17
https://kcw.kddi.ne.jp 14
http://nuevospowerpoints.blogspot.com.es 12
http://b.hatena.ne.jp 10
http://tweetedtimes.com 9
http://ishinao.tumblr.com 7
http://ishinao.net 6
http://torokeru.tv 6
https://openlink.to 5
http://feedly.com 3
http://ab-sn5.tumblr.com 2
http://openlink.to 1
http://www.linkedin.com 1
http://nuevospowerpoints.blogspot.mx 1
http://localhost 1
http://www.google.co.jp 1
More...

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel

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

  • サーバー未経験者が ソーシャルゲームを通して知った サーバーの事 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()        #  プレイヤーの経験値アップ        player.add_̲experience() except:        #  エラー起きたらDBをロールバック        transaction.rollback() else:        #  問題なければDB更更新。経験値が増える。        transaction.commit() 体力→KVS 経験値→DB ※実際のコードとは異なります
  • 例えばこんなコード 2011∼ #  この中はトランザクション内という仮定 try:        #  プレイヤーの体⼒力力を消費        player.consume_̲vitality()        #  プレイヤーの経験値アップ        player.add_̲experience() except: ←ここでエラー        #  エラー起きたらDBをロールバック        transaction.rollback() else:        #  問題なければDB更更新。経験値が増える。        transaction.commit() 体力→KVS 経験値→DB
  • どうなるか
  • どうなる? #  この中はトランザクション内という仮定 try:        #  プレイヤーの体⼒力力を消費        player.consume_̲vitality()        #  プレイヤーの経験値アップ        player.add_̲experience() except:        #  エラー起きたらDBをロールバック        transaction.rollback() else:        #  問題なければDB更更新。経験値が増える。        transaction.commit() 2011∼ 体力は消費される 経験値付与でエラーが起きる
  • どうなる? #  この中はトランザクション内という仮定 try:        #  プレイヤーの体⼒力力を消費        player.consume_̲vitality() 2011∼ 体力は消費される 経験値付与でエラーが起きる        #  プレイヤーの経験値アップ        player.add_̲experience() except:        #  エラー起きたらDBをロールバック        transaction.rollback() else:        #  問題なければDB更更新。経験値が増える。        transaction.commit() 体力だけが消費される ユーザーの不利益となる
  • 原因は様々 単純にバグッてる アクセス過多 サーバーが息をしていないetc... 2011∼
  • 原因は様々 単純にバグッてる アクセス過多 サーバーが息をしていないetc... 想定外の事が起きるんです 2011∼
  • 回避策
  • 順番を変える 2011∼ #  この中はトランザクション内という仮定 try:        #  プレイヤーの体⼒力力を消費        #  player.consume_̲vitality()        #  プレイヤーの経験値アップ        player.add_̲experience() except:        #  エラー起きたらDBをロールバック        transaction.rollback() else:        #  問題なければDB更更新。経験値が増える。        transaction.commit() #  プレイヤーの体⼒力力を最後に消費 player.consume_̲vitality() DB更新後に移動
  • ユーザー視点で考える 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 〃15
  • シャードの決定 2012∼ #  プレイヤーIDはユニークである事が前提 player_̲id  =  “hamspamegg” #  適当なハッシュ関数などで数値にし、シャードの分割数で余りを求める #  16  =  playerのDBの総シャード数 #  0〜~15の値が取得出来る player_̲db_̲number  =  _̲hash(player_̲id)  %  16 ※実際のコードとは異なります
  • うまくいった
  • ようにみえたが。。。
  • 障害 2012∼ マスターデータ ギルド プレイヤー0 〃4 〃8 〃12 イベント プレイヤー1 〃5 〃9 プレイヤー2 〃6 〃10 〃13 〃14 プレイヤー3 〃7 〃11 〃15
  • 原因 2012 ある処理だけ分割が効いていなかった 初期化に入れていた空文字が 特定のシャードを指していた #  プレイヤーIDはユニークである事が前提 player_̲id  =  “” if  何かの条件:        player_̲id  =  getHoge() else:        #  偽の処理理....  player_̲idが””のまま        ... player_̲db_̲number  =  _̲hash(player_̲id)  %  16
  • どうなる? 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 アプリのソースに 変更いらず 〃15
  • 学んだこと 負荷が少なくとも、 スケール可能な設計にしよう 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 実際はソースの名残で一部使ってますが... プレイヤー3 〃7 〃11 〃15
  • アジェンダ アジェンダ 前提 最後に トライアル&エラー編 キャッシュ 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!!x16
  • さらに、ギルドバトルだと
  • 同時に起きる可能性も これでもまだ半分以下 バトル
  • 見るからにきつい
  • 問題点 バトル 対象のDBが多いと、管理が難しくなる アクセスが大変。というかしたくない
  • 対応策
  • ギルドバトル専用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
  • 必要なデータの選定 バトル ギルド ギルドメンバーのレベル ギルドメンバーの職業 ギルドメンバーのカード ギルドメンバーのカードのレベルとかとか
  • マッチング バトル ギルド バッチサーバー プレイヤー バトルの数十分前に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未満(のはず) save()
  • 一方。。 バトル mame hiko 体力100 体力100 mameとhikoのデータを取得 mameとhikoのデータを取得 ここで攻撃 したから 体力100のデータ を取得 hikoに攻撃 mameに攻撃 hikoの体力は save() 100未満(のはず) save() 体力100のまま save
  • 実際は。。 バトル mame hiko 体力100 体力100 mameとhikoのデータを取得 た事に っ が無か 攻撃 る! な ここで攻撃 したから mameとhikoのデータを取得 体力100のデータ を取得 hikoに攻撃 mameに攻撃 hikoの体力は save() 100未満(のはず) save() 体力100のまま save
  • 対応策
  • 唯一の共通オブジェクト 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に攻撃 トランザクションは必須 save() save() 攻撃を受けた後のデータが 取得される
  • うまくいった
  • 本当に!
  • これで全てが終わったかに見えた
  • アジェンダ アジェンダ 前提 最後に トライアル&エラー編 キャッシュ 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で効率が悪そうなもので考える キャッシュは使わないという選択肢 色々あるけどまとめきれず
  • ご清聴ありがとうございました