MasterMemory導入事例 ~大規模プロジェクトで爆速のアウトゲームを作るには!?~

こんにちわ。luckinです。

KLabでは今までデータベース基盤としてSqliteを採用していましたが、パフォーマンス面で問題を抱えることが多くありました。

今回はSqliteでのデータベース基盤での課題やそれを解決するために大規模開発プロジェクトにMasterMemoryを導入した際のノウハウや実際に入れてみての使い勝手やMasterMemoryがパフォーマンス、特にメモリサイズにどれくらい影響を及ぼすのかについてご紹介します。

Sqliteデータベース基盤の課題

KLabでは長らくSqliteでのデータベース基盤を採用し、多くのプロジェクトを支えてきました。

しかし、近年ではプロダクトのパフォーマンス向上やゲーム内コンテンツの増加に伴いSqliteがボトルネックの要因になることが増えてきました。

Sqlite単体では十分なパフォーマンスを発揮しています。ではどこがボトルネックになるのでしょうか?

KLabではレコードを取り出す際のアロケーションが大きな問題となりました。

C#は静的型付け言語であり、Sqliteの柔軟なデータ構造に適していません。

レコードを取り出すにはBoxingを覚悟した柔軟なオブジェクトで取り出すか、テーブルスキーマに準じた型定義を行ったオブジェクトで取り出す必要があります。

KLabでは後者を選択しており、このオブジェクトのアロケーションがレコード毎に発生します。

またレコード内の情報が全て利用される訳ではなく、一部分の情報のみが利用されることが多くあります。

このC#とSqliteの相性の悪さによるアロケーションを回避するため以下の条件を満たすデータベース基盤を模索しました。

データベース基盤要件

  • 現状のデータベース基盤を超えるパフォーマンスを発揮できること
  • メンバーへの学習コストが低いこと
  • 暗号化(難読化)が検討できること
  • コード上で十分な可読性があること
    • 型が明確である(objectを使わない
    • 特定カラムにアクセスする際にカラム名でアクセスできること(SQLクエリを使ってDictionaryで返すような構造にはしない

Sqliteの課題を解決するMasterMemoryとは

C#でおなじみneueccさんこと河合 宜文さんが実装に携わっているオープンソースのオンメモリマスタデータ基盤となります。

ファイルのフォーマットとしてはmsgpackになっており仕組みは非常にシンプルでソートしたリストを二分探索するだけとなっています。

ではどこが他のDB基盤と異なるのかというとそれは圧倒的アロケーションの少なさとなります。

特にUnityではIncrementalGCが導入されたものの、引き続きアロケーション削減とGCによるスパイク軽減は重要な課題であり、MasterMemoryはこの面で圧倒的パフォーマンスを発揮します。

img ※ Cysharp様のMasterMemoryリポジトリから引用

メリット

  • 静的に解析によるコード自動生成で可読性が高いコードが書ける
  • 索引するI/Fも自動生成により提供されるため、メンバーへの学習コストを抑えることができる
  • オンメモリDBであるためFindする際のコストが二分探索のみ
  • アロケーションが0のためアロケーションコストとガベージコレクトコストを激減できる
  • string.Internを使用しているため文字列のメモリを削減できる
  • msgpackのためLZ4圧縮も使えばファイルサイズをとても小さくできる
  • msgpackフォーマットであることが明確なので難読化が検討できる
  • オンメモリDBのため各モデルクラス内で値を持つのではなくマスタを参照することで全体のマネージド空間を小さくできる

デメリット

  • オンメモリDBのためマネージドに展開されるメモリサイズに配慮しないと結果的にメモリを圧迫してしまう可能性がある
  • Immutable前提のDBのためランタイムでのレコードのInsertには適さない
    • 今回導入したプロジェクトではマスタデータのみでの使用だったため、大きなデメリットにはならなかった

事前検証

まずプロジェクト導入するにあたって事前に既存のプロジェクトからフォークしたプロジェクトにてMasterMemoryの検証を行いました。

その結果を下記にまとめます。

検証内容

下記のスキーマを持ったテーブルにレコードを265件InsertしSelectを100回行う。

その後取り出したデータをMonoのマネージドメモリ上に展開し、C#上でアクセス可能になるまでの速度と総アロケーション量を検証する。

テーブルスキーマ

SQL
CREATE TABLE card(
  rank INTEGER NOT NULL,
  id INTEGER NOT NULL,
  character_m_id INTEGER NOT NULL,
  order_no INTEGER NOT NULL,
  card_rarity_type INTEGER NOT NULL,
  card_attribute INTEGER NOT NULL,
  role INTEGER NOT NULL,
  thumbnail_asset_path TEXT NOT NULL,
  autograph_image_asset_path TEXT NOT NULL,
  at_gacha INTEGER NOT NULL,
  at_event INTEGER NOT NULL,
  training_m_id INTEGER NOT NULL,
  voice_path TEXT NOT NULL,
  point INTEGER NOT NULL,
  exchange_item_id INTEGER NOT NULL,
  role_effect_master_id INTEGER NOT NULL,
  skill_slot INTEGER NOT NULL,
  max_skill_slot INTEGER NOT NULL,
  analysis_id INTEGER NOT NULL,
  PRIMARY KEY (id)
);

検証対象

検証結果

speed allocation
sqlite3 + klbvfs 351ms 8.2MB
sqlite3 + klbvfs + 独自C#キャッシュ機構 44ms 188.3KB
MasterMemory 8.17ms 0B

シンプルな検証ではありますが、既存のデータベース基盤と比較して十分な速度が出ていることがわかります。

特に今回はキャッシュヒットしやすい検証環境でしたが、それでもキャッシュ機構を用いた結果より5倍ほどの向上がみられました。


プロジェクトへの導入

構成

導入プロジェクトではDBとMasterMemoryとSqliteを使い分ける形で導入しています。

導入プロジェクトでは膨大なアセットを扱う関係でパス情報が増えるため全てをMasterMemoryに寄せるのはメモリサイズ的に現実的ではないためです。

  • MasterMemory
    • マスタデータ全般
  • Sqlite
    • リソースのパス情報(リソースのロードの方が遥かに負荷が高いためSqliteのコスト無視できる)
    • 使用頻度の低い長文文字情報(利用規約/クレジット/キャラクターのプロフィール等)

実装したビルドパイプライン

公式から提供されているもの

  • mmgen/mpc(MasterMemoryのMsgPackC#のコード自動生成ツール)

独自に用意したもの

  • MasterMemoryテーブルクラスの生成ツール
    • サーバ側のテーブル定義を合致させるために独自DSLからクライアント/サーバ両スクリプトのテーブル定義を出力できるように
  • MasterMemoryのマスタデータファイルの生成ツール
    • 公式からファイルをビルドするツールは提供されていないため独自実装が必須
  • 独自Enumクラスの自動生成ツール
    • MasterMemoryではEnumをKeyにして検索した場合、C#の仕様上の問題でBoxingしてしまう(後述)
    • また独自Enumクラスは手で書くと非常に煩雑な作業になるため自動生成ツールを用意

ビルドの流れ

img


導入した上での感想

良かったこと

  • 非常に高速なためプロダクト全体のクオリティを担保しやすい

    • 過去にSqliteを採用していたPJではSELECT件数を非常に意識して実装を行い、PJ後期ではSqlite関連のパフォーマンスチューニングが必須だったが、導入PJではこれら一切を行わず品質が高いプロダクトを実装できた。
  • APIがわかりやすいのでメンバーへの学習コストがかなり少なく済んだ

    • Sqliteと違いクエリの知識も不要かつAPIも非常にわかりやすいため、マニュアル等は一切用意せずとも導入することができる。
  • 懸念だったマネージドメモリのサイズを適切な範囲内に収められたこと

    • 導入プロジェクトではキャラクターの総数が一万を超えることが想定されており、メモリサイズが懸念されたが、適切な範囲に収められたこと
    • リリース相当のデータ入力が完了した段階(tsvファイルベースで9.0MB)の内訳
      • マネージドメモリ上のサイズ3.3MB(難読化を行っているため実際にはもう少し小さくなる)
      • ストレージ上のサイズ600KB

良くなかったこと

  • 枯れた技術ではないのでバグが見つかる 以下バグの内容

    • Enumをキーにして探索するとBoxingでAllocateする
      • 独自enumクラスを実装して回避(まぁ誤差の範囲内なので無視しても問題にならないレベル) img
    • キーの最大値を超えた値をSelectしたのにレコードがヒットするバグあった
      • 本家リポジトリの方で修正済み
  • 定期的にオンメモリ上のサイズをMemoryProfileでチェックする必要がある(旗振り役必須)

    • レコードやカラムが大きくなるテーブルスキーマは型を意識する必要がある
      • プロジェクトではEnumは基本byte型
      • レコードが増えそうなテーブルについては適切な型(byteやshort)を使う
      • コードの自動生成ツールを使ってマスタクラスではnullableを避けて型+boolのフィールドを持つように

さいごに

KLabでは品質の高いプロダクトを実装する上でModelは不変であるという考え方を取り入れています。これは実装を単純明快にすると引き換えにパフォーマンスを犠牲にするものでした。

今回MasterMemoryを導入することで実装を単純明快にしつつ、C#のビジネスロジックレイヤーでパフォーマンスを意識する必要をほとんど無くすことができたと思っています。

最後に、この素晴らしいライブラリを作ってくださったneueccさんとリポジトリのコントリビューターの皆様に感謝を

このブログについて

KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。

おすすめ

合わせて読みたい

このブログについて

KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。