Slide 1

Slide 1 text

1/72 TiDB GAME DAY 2025 Shadowverse: Worlds Beyond にみる TiDB 活用術 株式会社 Cygames サーバーサイド 宮脇 剛史

Slide 2

Slide 2 text

2/72 Cygames について 「最高のコンテンツを作る会社」

Slide 3

Slide 3 text

3/72 Cygames について

Slide 4

Slide 4 text

4/72 Shadowverse: Worlds Beyond について 2025 年 6 月 17 日 (火) リリース 現在事前登録受付中

Slide 5

Slide 5 text

5/72 Shadowverse: Worlds Beyond では TiDB を採用

Slide 6

Slide 6 text

6/ 自己紹介 宮脇 剛史 シニアゲームエンジニア / サーバーサイド サーバーサイドエンジニアとして 複数のタイトルでの運用を経て、 現在はサーバータスクにて横断的な技術支援、 技術検証、負荷試験などを担当。 直近では『Shadowverse: Worlds Beyond』などの TiDB 導入や技術支援を担当し、 大規模データ処理とパフォーマンス最適化を実現。

Slide 7

Slide 7 text

7/72 プロジェクトとは独立した サーバーサイドセクション内の横断チーム – 技術検証 – 負荷試験 – TiDB の導入支援 – 全社的な FinOps 推進 サーバータスクチームについて

Slide 8

Slide 8 text

8/72 Shadowverse: Worlds Beyond への支援 – TiDB テーブル設計ガイドラインの共有 – データベースの問題発生時の調査 – テーブル設計、クエリ、ソースコードのレビュー – 負荷試験の実施 サーバータスクチームについて

Slide 9

Slide 9 text

9/72 1. TiDB 採用に至るまでの検証 2. Shadowverse: Worlds Beyond の TiDB 活用術 – API サーバ実装 – データ運用 – データ分析 3. Shadowverse: Worlds Beyond の負荷試験 4. まとめ アジェンダ

Slide 10

Slide 10 text

10/72 TiDB 採用に至るまでの検証

Slide 11

Slide 11 text

11/72 Cygames のサービスでは データベースとして MySQL を採用 垂直分割、水平分割を併用 あるゲームでは 300 台近くの MySQL を使っていたことも Cygames のデータベース

Slide 12

Slide 12 text

12/72 MySQL ではスケールイン・アウトが難しい 過去の例ではエンジニアが数ヶ月拘束される ゲームは負荷の変動が大きい あるゲームではイベント中 450 万 QPS 近くになるが 平時は 20 万 QPS MySQL では常にピーク時のための リソースを確保することになり無駄が大きい なぜ TiDB なのか

Slide 13

Slide 13 text

13/72 TiDB の持つ MySQL 互換性、スケーラビリティ、弾力性 に期待 なぜ TiDB なのか

Slide 14

Slide 14 text

14/72 Cygames では、 TiDB の導入判断のため 2021 年ごろから検証を開始 MySQL との比較や TiDB の特性の把握を目的として 機能検証 や 性能検証 を実施 Cygames における TiDB の検証

Slide 15

Slide 15 text

15/72 基本的に MySQL との互換性は高い MySQL を使った実装はほぼそのまま動く ただし、細かい部分で挙動が異なるものがある 普段は問題なく動くが、特定の状況で問題になる TiDB 機能検証の結果

Slide 16

Slide 16 text

16/72 TiDB 性能検証 TiDB をそのまま使うと Aurora よりも性能が出ない

Slide 17

Slide 17 text

17/72 TiDB 性能検証 テーブル設計、クエリを改善すると TiDB の性能を引き出すことができる

Slide 18

Slide 18 text

18/72 工夫すれば Cygames の 多くのタイトルで利用可能であると判断 ただし、 TiDB の利用が難しいタイトルもある ゲームロジックがサーバーサイドにあり 通信頻度が非常に高いブラウザゲームなど TiDB 性能検証の結果

Slide 19

Slide 19 text

19/72 Shadowverse: Worlds Beyond TiDB 活用術

Slide 20

Slide 20 text

20/72 1. データベース周りの構成 2. API サーバ実装 3. データ運用 4. データ分析 Shadowverse: Worlds Beyond の TiDB 活用術

Slide 21

Slide 21 text

21/72 Shadowverse: Worlds Beyond の TiDB 活用術 データベース周りの構成

Slide 22

Slide 22 text

22/72 データベース周りの構成

Slide 23

Slide 23 text

23/72 Shadowverse: Worlds Beyond の TiDB 活用術 API サーバ実装

Slide 24

Slide 24 text

24/72 TiDB 活用術: API サーバ実装

Slide 25

Slide 25 text

25/72 • テーブル・クエリ設計 • トランザクション分離レベル TiDB 活用術: API サーバ実装

Slide 26

Slide 26 text

26/72 TiDB のプライマリキーやインデックスに注意 auto_increment, auto_random の使用を非推奨 ランダムなユーザー ID など システム全体で分散するものをキーの先頭に置く 詳しくは TiDB GAME DAY 2024 の資料参照 https://tech.cygames.co.jp/archives/3631/ テーブル・クエリ設計の工夫

Slide 27

Slide 27 text

27/72 MySQL では REPEATABLE READ を使っていた TiDB で 同じように REPEATABLE READ を使うと Lost Update が発生する場合がある TiDB では READ COMMITTED を採用 トランザクション分離レベルの工夫

Slide 28

Slide 28 text

28/72 説明に使用するテーブル id 1 user_id item_id value 1 1 1 1 2 1 users user_items

Slide 29

Slide 29 text

29/72 Lost Update TX1 TX2 BEGIN; SELECT value FROM user_items WHERE user_id=1 AND item_id=1; value: 1 を取得 UPDATE user_items SET value=10 WHERE user_id=1; value: 10 に更新 UPDATE user_items SET value=1+1 WHERE user_id=1 AND item_id=1; value: 2 に上書き Lost Update COMMIT;

Slide 30

Slide 30 text

30/72 Lost Update の回避 TX1 TX2 BEGIN; SELECT * FROM users WHERE id=1 FOR UPDATE; ロック用テーブルで ロック SELECT value FROM user_items WHERE user_id=1 AND item_id=1; value: 1 を取得 BEGIN; SELECT * FROM users WHERE id=1 FOR UPDATE; ロック待ちとなり 更新できない UPDATE user_items SET value=1+1 WHERE user_id=1 AND item_id=1; value: 2 に更新 COMMIT;

Slide 31

Slide 31 text

31/72 MySQL では REPEATABLE READ のスナップショットは 最初の SELECT を実行したタイミングで取得 SELECT したテーブル以外のデータも同時にスナップショット 排他処理の前に意図せず SELECT をして事故が起きるケースも トランザクション開始直後に排他処理という ルールを守っていれば問題なかった MySQL のスナップショット

Slide 32

Slide 32 text

32/72 MySQL のスナップショット TX1 TX2 BEGIN; UPDATE user_items SET value=10 WHERE user_id=1; value: 10 に更新 SELECT * FROM users WHERE id=1; ユーザー情報取得 スナップショット UPDATE user_items SET value=20 WHERE user_id=1; value: 20 に更新 SELECT value FROM user_items WHERE user_id=1 AND item_id=1; スナップショットか ら value: 10 を取得 COMMIT;

Slide 33

Slide 33 text

33/72 TiDB では トランザクション開始時にスナップショットを取得 排他処理の前に誤って SELECT を 実行してしまった場合と同様の問題が発生 TiDB のスナップショット

Slide 34

Slide 34 text

34/72 TiDB での Lost Update TX1 TX2 BEGIN; スナップショット BEGIN; SELECT * FROM users WHERE id=1 FOR UPDATE; UPDATE user_items SET value=10 WHERE user_id=1; value: 10 に更新 COMMIT; SELECT * FROM users WHERE id=1 FOR UPDATE; ユーザー情報取得 SELECT value FROM user_items WHERE user_id=1 AND item_id=1; スナップショットか ら value: 1 を取得 UPDATE user_items SET value=1+1 WHERE user_id=1 AND item_id=1; value: 2 に上書き Lost Update COMMIT;

Slide 35

Slide 35 text

35/72 TiDB で Lost Update が発生する問題の対策を検討 1. GET_LOCK / RELEASE_LOCK を使う 2. 更新に関係する SELECT 全てに FOR UPDATE を付ける 3. READ COMMITTED を使う TiDB での Lost Update

Slide 36

Slide 36 text

36/72 TiDB ではトランザクション開始時に スナップショットが取られる トランザクション開始前に GET_LOCK を使い スナップショット取得前に排他処理をすることで回避 Lost Update 対策 1: GET_LOCK

Slide 37

Slide 37 text

37/72 TX1 TX2 SELECT GET_LOCK('users:1', 60); ロック BEGIN; スナップショット SELECT GET_LOCK('users:1', 60); ロック待ち SELECT * FROM users WHERE id=1 FOR UPDATE; ユーザー情報取得 SELECT value FROM user_items WHERE user_id=1 AND item_id=1; スナップショットか ら value: 1 を取得 UPDATE user_items SET value=1+1 WHERE user_id=1 AND item_id=1; value: 2 に更新 COMMIT; SELECT RELEASE_LOCK('users:1'); ロック解除 Lost Update 対策 1: GET_LOCK

Slide 38

Slide 38 text

38/72 SELECT FOR UPDATE はスナップショットを 参照せず最新のデータが取得できる トランザクション中、更新に関わる SELECT 全てに FOR UPDATE を付けることで回避 Lost Update 対策 2: SELECT FOR UPDATE

Slide 39

Slide 39 text

39/72 Lost Update 対策 2: SELECT FOR UPDATE TX1 TX2 BEGIN; スナップショット BEGIN; SELECT * FROM users WHERE id=1 FOR UPDATE; UPDATE user_items SET value=10 WHERE user_id=1; value: 10 に更新 COMMIT; SELECT * FROM users WHERE id=1 FOR UPDATE; ユーザー情報取得 SELECT value FROM user_items WHERE user_id=1 AND item_id=1 FOR UPDATE; 最新の value: 10 を 取得 UPDATE user_items SET value=10+1 WHERE user_id=1 AND item_id=1; value: 11 に更新 COMMIT;

Slide 40

Slide 40 text

40/72 REPEATABLE READ の必要性を再検討 – トランザクションの先頭で排他処理を行うため 他トランザクションから同時に更新されることはない – ファジーリードを気にする必要がない – REPEATABLE READ である必要性がない READ COMMITTED を使うことで SELECT は最新データを取得することができる Lost Update 対策 3: READ COMMITTED

Slide 41

Slide 41 text

41/72 TX1 TX2 BEGIN; BEGIN; SELECT * FROM users WHERE id=1 FOR UPDATE; UPDATE user_items SET value=10 WHERE user_id=1; value: 10 に更新 COMMIT; SELECT * FROM users WHERE id=1 FOR UPDATE; ユーザー情報取得 SELECT value FROM user_items WHERE user_id=1 AND item_id=1; 最新の value: 10 を 取得 UPDATE user_items SET value=10+1 WHERE user_id=1 AND item_id=1; value: 11 に更新 COMMIT; Lost Update 対策 3: READ COMMITTED

Slide 42

Slide 42 text

42/72 Lost Update 回避方法ごとの性能 (TPS) を比較 シナリオ 1. ユーザー単位でロック 2. そのユーザーのアイテム全 100 件取得 3. その内 1 件を更新 TiDB における Lost Update 対策の性能比較

Slide 43

Slide 43 text

43/72 TiDB における Lost Update 対策の性能比較 Aurora db.r8g.8xlarge TiDB Cloud TiDB: 8 vCPU, 16 GiB * 2 TiKV: 8 vCPU, 32 GiB * 3 パフォーマンスが良く、対応も容易な READ COMMITED を採用

Slide 44

Slide 44 text

44/72 • テーブル・クエリ設計 効率の良い参照ができるように プライマリキーやインデックスを設計する • トランザクション分離レベル MySQL と TiDB でスナップショットを取得する タイミングが異なるため Lost Update の可能性がある READ COMMITTED を採用することで回避 API サーバ実装の工夫まとめ

Slide 45

Slide 45 text

45/72 Shadowverse: Worlds Beyond の TiDB 活用術 データ運用

Slide 46

Slide 46 text

46/72 TiDB 活用術: データ運用

Slide 47

Slide 47 text

47/72 • トランザクションサイズによるエラー • 大量のデータの消し込み方法 TiDB 活用術: データ運用

Slide 48

Slide 48 text

48/72 トランザクションのサイズとは? TiDB はトランザクション中の更新内容をメモリに溜め込む 溜め込むためのバッファのサイズがトランザクションサイズ トランザクションのバッファのサイズには上限がある 更新量が多いとバッファが溢れトランザクションが失敗する トランザクションサイズによるエラー

Slide 49

Slide 49 text

49/72 TiDB のトランザクションの仕組み TiKV から更新対象のデータを取得し、更新後のデータをバッファリング

Slide 50

Slide 50 text

50/72 TiDB のトランザクションの仕組み COMMIT 時にトランザクションのバッファを FLUSH

Slide 51

Slide 51 text

51/72 TiDB のトランザクションの仕組み 更新データ量が多いとバッファが溢れエラーが発生

Slide 52

Slide 52 text

52/72 これらのクエリは TiDB でエラーになる可能性がある – 大量のデータをまとめて更新 UPDATE {table} SET value = 100; – データの一括削除 DELETE FROM {table}; – テーブルのメンテナンスのためのデータコピー INSERT INTO {table_new} SELECT * FROM {table_old}; トランザクションサイズにより失敗する例

Slide 53

Slide 53 text

53/72 TiDBがクエリを自動で分割して実行する BATCH ON id LIMIT 1000 DELETE FROM users; このクエリは以下のように分割、順次実行される DELETE FROM users WHERE id BETWEEN 1 AND 1000; DELETE FROM users WHERE id BETWEEN 1001 AND 2000; 対策 1: Non-Transactional DML ︙

Slide 54

Slide 54 text

54/72 対策 1: Non-Transactional DML 分割されたクエリは Atomic ではない 対象テーブルに他のクエリが実行されていると 意図しない結果になる可能性がある SHOW PROCESSLIST で進行状況が把握できる

Slide 55

Slide 55 text

55/72 対策 2: Pipelined DML クエリの実行中、更新データを TiKV に 逐次書き込むことでトランザクションの制限を回避 Non-Transactional DML と異なり Atomic 性が保証される

Slide 56

Slide 56 text

56/72 対策 2: Pipelined DML トランザクション中の更新データは随時 TiKV に FLUSH する

Slide 57

Slide 57 text

57/72 トランザクションサイズによるエラーまとめ 大量のデータの更新方法は以下のように整理 基本的なバッチ処理 – スクリプトで 1 行ずつ更新 メンテナンス時の作業 – Non-Transactional DML – Pipelined DML

Slide 58

Slide 58 text

58/72 大量のデータの消し込み Cygames ではデータ削除に論理削除を採用している データベースの容量確保のため 論理削除後のデータは一定期間経過後に削除する 削除が必要なデータの例 – ログテーブル – プレゼントテーブル(受け取り済みデータ)

Slide 59

Slide 59 text

59/72 大量のデータの消し込み方法の検討 MySQL ではパーティションドロップを使用 TiDB ではどのように削除するのが良いかを検討 – DELETE による消し込み – TTL による消し込み – パーティションドロップによる消し込み

Slide 60

Slide 60 text

60/72 DELETE による消し込み • create_time や delete_time が一定期間より 前のものを削除する DELETE クエリを毎日実行 削除対象を検索したり、ストレージに書き込むため データ量の多いテーブルでは TiDB や TiKV の負荷が大きい

Slide 61

Slide 61 text

61/72 TTL による消し込み • create_time や delete_time に TTL を設定 • 一定時間ごとに自動的に TTL ジョブが実行 • TTLジョブは内部的に SELECT や DELETE を実行し、 データを削除 DELETE による消し込みと同様 TiDB, TiKV の負荷が大きい

Slide 62

Slide 62 text

62/72 パーティションドロップによる消し込み • create_time や delete_time で 日ごとのパーティションを作成 • パーティションドロップのクエリを毎日実行 他の方法とは異なり TiKV のレイヤーでまとめて削除できるので軽量かつ高速 MySQL と同様にパーティションドロップを採用

Slide 63

Slide 63 text

63/72 データ運用の工夫まとめ • トランザクションサイズによるエラー TiDB で大量の更新を行うとエラーになる可能性があるため スクリプトによる 1 行ずつの更新か、専用のクエリを使用 • 大量のデータの消し込み方法 パーティションドロップでの消し込みを採用

Slide 64

Slide 64 text

64/72 Shadowverse: Worlds Beyond の TiDB 活用術 データ分析

Slide 65

Slide 65 text

65/72 TiDB 活用術: データ分析

Slide 66

Slide 66 text

66/72 分析基盤: Snowflake 社内共通の分析基盤として Snowflake が既にある 容量確保のため、多くのログデータは S3 上に保存 TiFlash を導入しても分析に必要なデータがない Changefeed は使用せず、別途 ETL サーバを用意 Snowflake上のデータがリアルタイムで更新されると 今までと同じような分析がしづらくなる

Slide 67

Slide 67 text

67/72 分析基盤の今後 ログデータの保存場所の関係から 今後も分析基盤としての TiFlash の使用は難しい 現在の ETLサーバはコスト、負荷的にも改善の余地がある Changefeed の使用など、より良い構成を今後も検討する

Slide 68

Slide 68 text

68/72 Shadowverse: Worlds Beyond 負荷試験

Slide 69

Slide 69 text

69/72 開発中、リリース前に複数回の負荷試験を実施 いずれもデータベース以外の他の要素が先に詰まる TiDB に関しては問題がなかった 本番リリースに向けて TiDB の最適化ができていると判断 負荷試験の結果

Slide 70

Slide 70 text

70/72 まとめ

Slide 71

Slide 71 text

71/72 • 検証から得た知見を元に TiDB の活用術を構築 – 最高のコンテンツを作るため、自分たちが使うツールは 時間を掛けてでも検証し、仕組みを理解して、活用する – Cygames は今後も TiDB をデータベースの第一の選択肢として 積極的に取り組んでいく • TiDB を採用した Shadowverse: Worlds Beyond は 6 月 17 日リリース まとめ

Slide 72

Slide 72 text

72/72