分割と整合性と戦う

102
分割と整合性をがんばる話 ソーシャルゲームの整合性対策

description

第二回ゲームサーバ勉強会での発表資料

Transcript of 分割と整合性と戦う

Page 1: 分割と整合性と戦う

分割と整合性をがんばる話ソーシャルゲームの整合性対策

Page 2: 分割と整合性と戦う

自己紹介清水 佑吾

@yamionp

株式会社 gumi 勤務

Python歴約2年半

サーバーさわりはじめて約10年

前職はISP

Page 3: 分割と整合性と戦う

水平分割がやりたくて転職

Page 4: 分割と整合性と戦う

関わったもの

HTML + FlashLite

Cocos2d-x

Page 5: 分割と整合性と戦う

使用環境

Python 2.7

Django

MySQL 5.5/5.6 (RDS)

Redis

RabbitMQ

Page 6: 分割と整合性と戦う

アジェンダ

2012年前期 負荷対策

2012年中期 トランザクション

2012年後期 デッドロック

Page 7: 分割と整合性と戦う

負荷対策期

Page 8: 分割と整合性と戦う

サービスがヒット

更新処理が限界に

当時最強のインスタンスを用意

もう大丈夫!

・・・が、ダメっっ!

Page 9: 分割と整合性と戦う

というわけで

Page 10: 分割と整合性と戦う

Player

KVSMemcache

RDB

TokyoTryant

FriendGuildTradeMaster

Page 11: 分割と整合性と戦う

垂直分割機能単位で格納先DBを変える

性能問題に突き当たる度に分割対象を選定

外部キーを外して別DBに移すだけの簡単なお仕事

1機能に負荷が集中すると対処不能

KVSにもじゃんじゃん逃す

Page 12: 分割と整合性と戦う

機能をまたがる処理Friend Playerフレンドが増えたので

ポイントUP

Friend ++

Point +10

save

save失敗!

rollbackフレンドが増えたのに ポイントが増えないい…

Page 13: 分割と整合性と戦う

同時に使う機能は分割できない

負荷の多いPlayer/Card/Quest/Itemの分割が難しい

たとえ分割しても負荷は変わらないことも

Page 14: 分割と整合性と戦う

そこで

Page 15: 分割と整合性と戦う

Master Guild

Player

KVSMemcache Redis

RDB

Trade Friend

体力等カード等のCache

Page 16: 分割と整合性と戦う

性能問題には一定の解決をみた

Page 17: 分割と整合性と戦う

が…

Page 18: 分割と整合性と戦う

多発する不整合

消えた更新

なぜか消えるカード

なぜか増えるカード

Page 19: 分割と整合性と戦う

増えるカード

Page 20: 分割と整合性と戦う

プレイヤーをまたがる処理Player A Player B

Trade

Card Delete

Card Add

save

save

失敗!

rollback こちらは残ったまま

Page 21: 分割と整合性と戦う

消えるカード

Page 22: 分割と整合性と戦う

ユーザー「合成したらカードが消えたんですが!

Page 23: 分割と整合性と戦う

プレイヤーをまたがる処理Shard1

ID:1 PlayerA

Shard2

ID:1 PlayerCID:1 -

Page 24: 分割と整合性と戦う

プレイヤーをまたがる処理Shard1

ID:1 PlayerA

Shard2

ID:1 PlayerCID:1 -

上書き!

Page 25: 分割と整合性と戦う

分割キーを消してはいけない

Page 26: 分割と整合性と戦う

機能をまたぐ場合の問題も 残ったまま

Page 27: 分割と整合性と戦う

ただし負荷は下がった高負荷状態にならないのでエラーも少ない

ログだけ丁寧に仕込んで個別ケース対応

KVSに大事なデータを置かない

ゲームに致命的にならない範囲でエラー時はユーザーが得になる方に倒す

バグは直す

Page 28: 分割と整合性と戦う

そして新プロジェクトへ

Page 29: 分割と整合性と戦う

アジェンダ

2012年前期 負荷対策

2012年中期 トランザクション

2012年後期 デッドロック

Page 30: 分割と整合性と戦う

不整合と戦う

Page 31: 分割と整合性と戦う

偉い人「100万人きても大丈夫なようにしといて!」

Page 32: 分割と整合性と戦う

1から抜本的に見直し

負荷は水平分割で対処する

XA Transactionによる一貫性担保

ロックによる排他制御

Page 33: 分割と整合性と戦う

水平分割を前提とした構成

Page 34: 分割と整合性と戦う

全部DBにいれる

Guild

PlayerRDB

Page 35: 分割と整合性と戦う

マスターデータはjson化

変更がないのでデプロイ時にAppサーバーに配布

メモリ上に展開するので非常に高速

ますますキャッシュレスに

Page 36: 分割と整合性と戦う

DBのみで実装する

プレイヤーに紐づくデータはすべてDBに

自動回復系ステータス(体力、BPなど)もDB

トランザクションに収められる!

正規化を徹底

Page 37: 分割と整合性と戦う

自動回復系ステータス

Page 38: 分割と整合性と戦う

いままではKVSに格納

Master Guild

Player

KVSMemcache Redis

RDB

Trade Friend

Page 39: 分割と整合性と戦う

よくおきる不整合

お金追加体力減算

失敗!

begin

commitrollback

Page 40: 分割と整合性と戦う

自動回復系ステータス

Page 41: 分割と整合性と戦う

今まではKVSに格納していた

DBだけ更新、KVSだけ更新がおきていた

ユーザーに得になる場合は裏技として2chで祭り

ユーザーの損になる場合はCSが爆発する

KVSだけ更新というパターンは0

ほとんどの場合お金かアイテムかカードが一緒に増える

KVSに居るメリットが実は無い

Page 42: 分割と整合性と戦う

実装

現在値、最大値、最終更新時刻を持つ

最終更新時間と現在値から自動回復済の値を計算して使う

減算時のみUPDATE

Page 43: 分割と整合性と戦う

正規化

Page 44: 分割と整合性と戦う

正規化

意味の重複する値を保存しない

レベルの値は無く、合計経験値のみ保存

参照時に経験値からレベルを計算

レベルからパラメータを計算。

Page 45: 分割と整合性と戦う

Beforeid int

card_id int hp int

attack intdefense int

magic_attack int magic_defense int

exp int level int

Page 46: 分割と整合性と戦う

After

id int

card_id int

exp int

驚きのダイエット 効果!

Page 47: 分割と整合性と戦う

XAトランザクション

Page 48: 分割と整合性と戦う

普通のトランザクション

begin;

SELECT…;INSERT INTO…;commit; 反映

Page 49: 分割と整合性と戦う

XAトランザクション

xa begin

SELECT…;INSERT INTO…;xa end

反映

xa prepare

xa commit

xa beginSELECT…;

INSERT INTO…;xa end

xa prepare

xa commit

commit 成功を保証

DB1 DB2

Page 50: 分割と整合性と戦う

prepare

prepare

prepare

prepare

prepare

prepare

App

Page 51: 分割と整合性と戦う

commit

commit

commit

prepare

prepare

prepare

App

commit

commit

commit

Page 52: 分割と整合性と戦う

もし途中でエラーになったら

Page 53: 分割と整合性と戦う

prepare

prepare

prepare

prepare

prepareApp

失敗!rollback

Page 54: 分割と整合性と戦う

rollback

rollback

prepare

prepareApp

rollback

rollback

Page 55: 分割と整合性と戦う

無事に処理前の状態に!

Page 56: 分割と整合性と戦う

複数のDBを跨ったtrxが可能XAに参加するいずれかの段階でエラーが起こればロールバックが可能

複数DBの状態が 処理成功 or 処理なし のいずれかのみを保証できるようになった

中途半端な状態がなくなる

体力のみ減る、カードだけ増えるなどがなくなる

Page 57: 分割と整合性と戦う

が、

DjangoはXA Transactionに非対応

水平分割にも非対応

自社開発!

Page 58: 分割と整合性と戦う

これらを簡単に使うために

エラーハンドリングを毎回書くのは無駄

スキル的にもきびしい

トランザクションに何を含めるかだけ書けるように

Page 59: 分割と整合性と戦う

エンジニアが書くべきこと

トランザクションに何を含めるか

範囲はモデルの機能ではなくリクエストごとに決まる

最適なロック順番は個別の処理ごとに異なる

ロック・トランザクションを要求する

Page 60: 分割と整合性と戦う

# player1とplayer2のDBにトランザクション開始 with commit_on_success([player1_id, player2_id]): # ロック付きで取得 player1 = Player.get_for_update(player1_id) player2 = Player.get_for_update(player2_id) # 減算を実行 player1.decrement_ap(5) player1.increment_money(10) player2.decrement_money(10)

Page 61: 分割と整合性と戦う

def increment_ap(self, quantity): # 自身がロック済みであることを要求 self.require_for_update() # 減算 self.ap -= quantity # UPDATE self.save()

Page 62: 分割と整合性と戦う

入れ子のトランザクションを扱えない

トランザクションに何を含めるかはモデルにはわからない

Page 63: 分割と整合性と戦う

ちなみに

Page 64: 分割と整合性と戦う

commit途中で死んだら?

Page 65: 分割と整合性と戦う

commit

commit

commit

prepare

prepare

prepare

App

commit

commit突然の死!!

Page 66: 分割と整合性と戦う

commit

commit

commitXA Recover

preparecommit

cron

Page 67: 分割と整合性と戦う

処理を完遂!

Page 68: 分割と整合性と戦う

というのが理想

innodbのxaは切断時にpreparedだと勝手にrollbackしてしまう

2005年ぐらいから指摘されていて、patchも送られた

が、patchの取り込みに失敗

どうしようもない

Page 69: 分割と整合性と戦う

ログベースの個別対応orz

Page 70: 分割と整合性と戦う

ある日の夜

イベントリリース!

しばらくは問題なく動作していたが…

ページが開けない!と苦情が

Page 71: 分割と整合性と戦う

CloudWatch

AppサーバーCPU使用率もリクエスト数も問題ないが...

DBのCPU使用率が張り付いていた

Page 72: 分割と整合性と戦う

即JetProfilerを起動

ç

Page 73: 分割と整合性と戦う

テキスト

クリック一つて即Eplainグラフィカル&レーティングしてくれる。 DBにくわしくなくてもいかにもダメそうな感じ

Page 74: 分割と整合性と戦う

インデックスがなかった

特定クエリが処理時間の9割以上を占めていた

緊急メンテに入りインデックスを追加

インデックスをはったら5%以下に

Page 75: 分割と整合性と戦う

ほとんど同じ状況で 別パターン

Page 76: 分割と整合性と戦う

無駄インデックス問題

特定クエリが処理時間の3割以上を占めていた

スローではないが一クエリ当たりの時間が多い

Explainしたら index merge

インデックスを削除したら100倍高速化

Page 77: 分割と整合性と戦う

アジェンダ

2012年前期 負荷対策

2012年中期 トランザクション

2012年後期 デッドロック

Page 78: 分割と整合性と戦う

排他制御

ロック

CAS

Page 79: 分割と整合性と戦う

CASの話はしません

Page 80: 分割と整合性と戦う

ロック

innodbはレコードロックが可能

ロックの実現にはインデックスが使われる

存在するインデックスより狭い範囲のロックはできない

Page 81: 分割と整合性と戦う

ロック範囲ID player_id value

1 401 A

2 401 B

3 402 B

4 403 C

PrimaryKey Index

Page 82: 分割と整合性と戦う

SELECT * FROM player WHERE player_id = 401 FOR UPDATE

Page 83: 分割と整合性と戦う

ロック範囲ID player_id value

1 401 A

2 401 B

3 402 B

4 403 C

PrimaryKey Index

ロック範囲

Page 84: 分割と整合性と戦う

SELECT * FROM player WHERE value = “B” FOR UPDATE

Page 85: 分割と整合性と戦う

ロック範囲ID player_id value

1 401 A

2 401 B

3 402 B

4 403 C

PrimaryKey Index

期待するロック範囲実際のロック範囲

Page 86: 分割と整合性と戦う

実際のロック範囲はオプティマイザーの気分次第

必要なインデックスが無いと不必要に大きな範囲のロックをとってしまう

インデックスが無駄にあると意図しないインデックスを使われてロックをとられてしまう

Page 87: 分割と整合性と戦う

何が起きるか

Page 88: 分割と整合性と戦う

ある日

ゲームが重い

画面が開けない

レイドボスを攻撃したのに重くて叩けなかった

イベントが動かない!

Page 89: 分割と整合性と戦う

生涯発生中に自分がプレイしても得に問題なかった

だがエラー報告が大量発生

サーバー負荷は大したことなかった

CPU/RAM/Disk/Networkすべて低レベル

ロードバランサーのレスポンスタイムがどんどん劣化

Page 90: 分割と整合性と戦う

JetProfiler

Page 91: 分割と整合性と戦う

ロック状態

Page 92: 分割と整合性と戦う

何が起きていたか

デッドロックによってロック待ちとタイムアウトが発生

Page 93: 分割と整合性と戦う

ロック

ID player_id value1 401 A2 401 B3 402 B4 403 C

App

1

2

Page 94: 分割と整合性と戦う

デッドロック

ID player_id value1 401 A2 401 B3 402 B4 403 C

App

1

App 2

デッドロック

Page 95: 分割と整合性と戦う

MySQLさんは親切

同じDB内のデッドロックは検知して解除してくれる

分割しているとMySQLは検知できない

XAでトランザクションをまとめているので複数DBにまたがって止まる

Page 96: 分割と整合性と戦う

回避するにはロック順番を統一する

ロックする前にソート(id, Player_id,)

DBをソート

テーブルをソート

レコードをソート

大きくロックを取る player単位、レイドボス単位

Page 97: 分割と整合性と戦う

参照処理に更新を混ぜない

Page 98: 分割と整合性と戦う

負荷も跳ね上がる。更新にはほとんどの場合ロックが必要

参照がロックをとる

ロック機会の圧倒的増大

デッドロック祭り

止まってしまうサービス

まってくれない終電

Page 99: 分割と整合性と戦う

MySQL「XAはSERIALIZABLE」

どのみち更新に必要なデータはFOR UPDATEで取得する必要がある

じつはいらなくね・・・?

REPEATABLE READにしたら速度もあがって問題なくなりました

Page 100: 分割と整合性と戦う

まとめ単にKVSに移すのは問題の先延ばしにしかならない

きちんと使えばRDBだけで十分さばける

マスターオンリー障害対策用のSlaveはいるがクエリは裁かない

デッドロック対策の前に適切なインデックスを

インデックスショットガン。だめ、絶対。

NewRelicとJetProfilerは神 超オススメです

Page 101: 分割と整合性と戦う

ご清聴ありがとうございました

Page 102: 分割と整合性と戦う

質疑応答