第二回ゲームサーバー勉強会に参加してきた
7/19(土)に開催された、ゲームサーバーに特化した勉強会についてのメモです。
広くてきれいな場を提供していただいたGREEさん、ありがとうございました。 そういえばGREEさんに入るのは、今回が初めてですね( ・ω・)
今回の勉強会では、以下のようなお話を頂きました。
- データベースの再入門
(@nsega様 ) - 分割と整合性をがんばる話 ソーシャルゲームの整合性対策
(株式会社gumi 小林様) - MMOのサーバーについて「剣と魔法のログレス 〜いにしえの女神〜」の実装例 (株式会社Aiming 山藤様)
スライド資料はまだ公開されてない模様でしたが、一部共有をして頂けるとの事だったので、公開され次第リンク貼っておきます。
今回、書いているメモは、あくまで私個人のメモから抜粋しているので、正確でない部分があるかもしれません。その部分はご指摘いただければと思います。( ・`ω・´)
結構、分量が多くなってしまったことを反省…その分濃い勉強会でした。
データベースの再入門
初心者しかいないと仮定した説明をします、との前置きがありました。内容もDBを使うにあたっての基本的なお話が中心でした。
nsegaさんは、MongoDBの人で有名な方です サーバーサイドの開発、直近だとネイティブもやってるそうです。 DBはPosgtre、Mysqlをよく使うそうです。
ここからメモ
データベースについて
データベースを扱うためには、データの定義を明確にする必要がある。
論理的、物理的問わず、最初に定義を固めておく。
DBMSと、問い合わせするためのSQLには、以下のような種類があり、目的が異なる。
複数人で同時に操作する、壊れたら大変なデータ、を扱うときはDB。
DBを使うケース = 検索性重視、障害時のリカバリを迅速に行う、整合性を保証したい場合
必要なデータを探すときは、インデックスがパフォーマンスにかなり影響する。DBで使われるインデックスの特性を理解すること
- B+Treeインデックス
- ハッシュインデックス
トランザクションを重視する理由
- データ更新の並列性を保つ
- データの整合性を保障する
レプリケーション
- DBのデータを複製できる。障害が発生しても、ある程度、データが元に戻せる。
- マスタ-スレーブ構成。Update等の更新は、マスタに対して行う。
マスタの変更は随時、スレーブに配信する、アプリからのReadはスレーブから。
RDBMSとNoSQLについて
NoSQL
- 何種類かある(KV型、ドキュメントベース型、カラムファミリ型)
- データ分散しやすい。大量データもOK。
- スケールアウトさせやすい(分割が楽)
- 安いサーバーでも十分
- 苦手
- 一貫性の保障。性質上、トランザクションを考えると、どうしても遅くなる。
- 何種類かある(KV型、ドキュメントベース型、カラムファミリ型)
お互いが不得意なことを、無理に置き換える必要はない 。適材適所が大事。DBかKVSの選択は、要件を解決するための手札でしかない。
KVSの使いどころは、タイムライン表示とか、リアルタイムランキング。 セッションステートとしても良い感じ 。
データベースを使った使用事例
データモデリング大事。あとで矛盾があっても、手戻りは基本できない。
これから勉強する人は、IPAのデータベーススペシャリストの勉強が、おすすめできる。
データの種類と管理方法
- マスタ系データ
アイテムとかモンスターそのもののデータ。頻繁にかわることはあまりない- M_xxxのようなプレフィックスつけて管理してる
- トランザクション系データ
ユーザーデータ。頻繁に更新追加が行われるもの- T_xxxのようなプレフィックスをつけて管理してる
- マスタ系データ
Tips
ER図レビュー、ソースレビュー、クエリレビューやる
- 実行計画を見る
- リクエスト回数が多すぎないか(ORM使っているとある)
- 事前に事故防止ができる
- わかってるエンジニアのアサインが必須になる
レプリケーション設計
- 最初からDB負荷を分散できる
- レプリケーション遅延を見越して設計する必要ある。マスタ反映後スレーブ反映しているので、若干遅延が出てしまう。タイミングによって、変更が反映されていない情報が取れる問題が。
- そのため、時にはせっかくスレーブに向けてたSelectクエリを、Master側に向ける必要も出てくる
シャーディング
- 水平分割: レコード単位で別DBに向ける
- 垂直分割: 1テーブル内の、更新が多い項目だけを別テーブルに切り出す。
シャーディングすることによって、DB性能、管理しやすさ、可用性に優しくなる。ただ、テーブルJoinが難しく…設計で考慮することが、増える。
開発
- フロントとサーバーサイド分けずに、一人でやったほうが効率いい
- API定義書とかつらい。全部自分で作ることで、コミュニケーションレスでできる。楽に。
- ただし、両方できる人は貴重…
分割と整合性をがんばる話 ソーシャルゲームの整合性対策
gumiの清水さんのお話でした。ソーシャルゲームの運用を通して、いろいろ工夫されたことを中心に、話されてました。
gumiさんは、python使ってることで有名な、SAPですね。 最近、大口の資金調達で話題になっていたりもしました。
ここからメモ
負荷対策
新規サービスが大ヒットして、負荷が限界に…
当時の、AWS最高インスタンスでもダメだったので、単純なスケールアップでは対応できなかった。サービス側で色々改修した。
- Player系データを、単独のDBサーバーに。
- 機能単位で、DB先を変更していたものを、Master、Trade、Guild、Friendをそれぞれ別サーバーに分割する方法に変更(垂直分割)
- 性能限界にぶち当たるたびに試行錯誤してた。
- 外部キーを外して、別DBに移す作業をひらすら実施。
- 一つの機能に負荷が集中して、対処不可能に
- KVSにも、じゃんじゃん流す
複数ユーザーが、同時に使う機能は、分割することが難しい
- Player情報系は負荷が多く、分割難しい
- ロックの粒度で負荷変わらない...
なので、Player毎に、接続先DBを変更する、水平分割を行って解決。
ユーザー増えたらサーバー増やす→ 性能限界の問題は解消できた。
しかし、複数DBをまたがせると、
- 多発する不具合
- 消えた更新、消えたカード
トレード機能で、カードを他のユーザー所有に変更するとき、カードから、ユーザーのヒモ付を消すようにしていた。
→ このユーザーが、分割キーだったので、分割キーを消すことで、このカードはどのDBに置くべきか、判断つかなくなった。
結果、他のユーザーの資産が影響受ける問題に。
→ 水平分割した時は、分割キーは消しちゃいけない
トランザクション
不整合と戦う
一から抜本的に対応- 負荷は水平分割で対処
- XA Transactionによる一貫性担保
- ロックによる排他制御
水平分割を前提とした構成にする
- プレイヤーデータはPlayer用のShardにまとめる。
- ただし、ギルドテーブルだけは、全ユーザに対する網羅的なアクセスが発生するので、これは1DBサーバーにまとめるようにした。
- マスタデータはJsonにして、デプロイ時にメモリ上に展開するような方法に。
DBのみで実装する
XA Transaction
分散トランザクション。
- XAに参加するいずれかのDBで問題起これば、ロールバック可能
- 複数DBをまたいだトランザクションいける
- フレームワーク側が対応していなかったので自社開発
- Preapared後のデータコミット時に、コミット途中で死んだら?
- 裏でCron回して、コミットされていないやつを、解除するタスクを回そうとしていた。
- DBのバージョンアップで、Prepared状態で固まっていると、自動でロールバックする対応が入ったので、結局使わなかった。
- が、DBが死んだ際に、復旧のため使った
デッドロック
- トランザクション掛けた、Updateが2回、クロスしたりする起きる。 解決できないので、サービス側でレコードロックをするようにした。
- innoDBはインデックス使ってレコードロックかけれる
- Whereで抽出する際に、インデックスが張っていれば、レコードが特定されてそこだけ行ロックできる
- インデックスがないと、行が特定できないのでテーブルロックが走る
発生した事例では、テーブルにインデックス貼ってなかったせいで、テーブルロックがかかる。そのため、デッドロックで、DBサーバーのCPUが張り付く問題が発生した。
また、無駄インデックスが残っていると、意図しないインデックスが使われて、思いもよらないロックが取られることがあった。
回避するためには、処理時に
- ロックの順番を統一すること
- DBまたがってる場合、DB順もソート
- 別のテーブルにまたがってる場合、テーブルもソート
- 参照に、更新処理まぜない(あればSelect、なければInsertSelect)のケース
MYSQLは親切。同じDB内のデッドロックを検知して解除してくれる
まとめ
- KVSに移すのは問題の先延ばしにしかならない
- デッドロック対策の前に適切なインデックスを
- インデックスショットガン、ダメ絶対
- プロファイラ大事
MMOのサーバーについて「剣と魔法のログレス 〜いにしえの女神〜」の実装例
Aimingの、山藤さんのお話です。
近頃、ランキングで目立ってきている、あのログレスの、実装についての内容でした。
今、目立つタイトルだけに、会場の期待度もかなり大きい印象でした。
MMOのタイトルらしく、通信周りの工夫の話が、大きくウェイト割かれていたように思います。
ここからメモ
ログレスのサーバー
フロントエンド側のサーバー、バックエンド側のサーバーを、それぞれ、階層分けて冗長化している。 WANを縦につなぐ、同階層での接続はしていない。
フロントエンドサーバー
バックエンドサーバー
- ユーザークライアントは接続しないサーバー。
- フロントエンドサーバー間の、データやりとりを、Socketで行っている。
サーバー中では、DBとのやりとりも行っていた。 - サーバーはC++で実装している
Socket
- サーバープッシュが必要な箇所で使用。
- レスポンス速度が要求される、常時接続、差分更新
- 利点
- オペレーション都度の、接続コストが発生しない
- 一回の、送受信量を少なくできる
- 欠点
- バッテリー消費(セッション維持するとどうしてもつらい)
- 回線切れに弱い
- ソフトウェアで対応しないといけないことが増える
- セッション維持している関係上、ロードバランサー使ったスケールアウト難しい
HTTP
- サーバープッシュがいらないところ
- レスポンス速度を要求しないところ
- 必要都度、通信、切断、
- 一括更新
実装例
キャラクタの移動
- 歩けない場所の制御、等あるため、サーバーでも管理する
- キャラクタが動くと
- 描画範囲外で、見えなかったものが見えるように
- 見えなかったユーザーも見えるように
見えなかったものが、見えると、どうなる
- 自分の行動を他のユーザーにも反映しないといけない
- 見えてる人がとった行動を、自分に反映しないといけない 人が増えることによってやりとりするデータが増えていく
移動すると、
- 描画通信範囲の更新を繰り返す
- 移動フロー
- クライアントで移動開始
- サーバー側に目的値と到達点を送信
- サーバー側で移動の妥当性を検証
- 周辺のプレイヤーに送る
移動の都度、サーバー介すると反応が悪くなるので、いろいろと工夫が必要。
- 他の人は多少遅れても気にならない
- クライアントだけで移動させてしまうと、通信範囲の計算できない
- クライアントだけで移動させると、マップの当たり判定無視できてしまう。
- チート問題が付きまとうので、サーバー側の妥当性は検証必須
チャット
文字ベースコミニュケーション
- キャラクタの、チャット送信でのポップアップはSocketで表示させている。
- チャットの吹き出しは見える人だけ見えればいよね
- 吹き出しは発言後なるべく早く画面にだしたい
- チャット履歴はHTTPで
- チャットログは、オフライン時のやつも見れる必要がある
- コミニュケーションの途切れを防ぐ
実際に起こった問題と対応
- 一台のサーバーにログインできるプレイヤー数が限られる
- エリア毎に別サーバーで割り当て。で分散。
- クライアントは、裏で、フロントエンドサーバー間の接続を、切り替えながら動いている
- この切替時のサーバー負荷が大きい
- ユーザー移動が大量に起こると、バックエンドが時間かかってローディングが終わらない
- サーバーをラウンドロビンして、解決
- 影響範囲はバックエンドとフロントエンドの通信部分だけ。
クライアントには影響なしで修正できた。構成が分離されていることによるメリット。