Upgrade to Pro — share decks privately, control downloads, hide ads and more …

マスタデータ統一スキーマ言語による効率的なトランクベース開発フローの実現

trapezoid
August 23, 2023

 マスタデータ統一スキーマ言語による効率的なトランクベース開発フローの実現

モバイルゲームのマスタデータ開発/運用では、マスタデータの編集UI、並行開発可能なバージョン管理、バリデーション、クライアント/サーバでのロード/インポートなど、様々な機能が必要になります。

これらは大抵は言語も動作環境もバラバラになるので、それぞれ異なるシステムを使うことが多くなります。

DeNAでもこのそれぞれに対して独立した内製システムを開発/運用してきましたが、

各システムへの個別にスキーマ定義することによる重複作業や、システム間を跨った操作が必要なワークフローの複雑化を招き、開発効率の低下や、システム利用の敷居の高さが問題になっていました。

また、ブランチを使った並行開発による大量のマージ工数も大きな問題になり、並行開発フロー自体の抜本的な改善も必要とされていました。

本セッションでは、これらの問題の解決策として、マスタデータの入力からロードまでの間の、全てのマスタデータ関連システムで必要なスキーマ情報を統一して扱う、

ProtocolBuffersをベースとしたマスタデータ統一スキーマ言語 Muscleを開発した事例を紹介します。

また、これに合わせてマージ工数の抜本的な削減のために導入した並行開発フローである、トランクベース開発の概要と、

そのためにマスタデータ統一スキーマ言語にどのような機能が求められたのかについても紹介します。

trapezoid

August 23, 2023
Tweet

More Decks by trapezoid

Other Decks in Programming

Transcript

  1. © DeNA Co., Ltd. 2 登壇者プロフィール • 大竹 悠人(Haruto Otake)

    ◦ Unity向けの様々な内製ライブラリの実装・保守 ◦ Unity製タイトルへの様々な技術サポート • 株式会社ディー・エヌ・エー ◦ ゲームサービス事業本部 開発事業部 第一技術部 テクノロジー推進第二G ▪ ゲーム開発系横断部門
  2. © DeNA Co., Ltd. 3 • マスタデータの高い頻度での更新を継続的に行う、モバイルゲーム開発において • それに必須となるマスタデータの並行開発を効率化するため •

    トランクベース開発をマスタデータ開発に導入した • また、それを支えるマスタデータ用スキーマ言語を開発した 何の話?
  3. © DeNA Co., Ltd. 6 マスタデータ開発/運用に必要な要素 • 高い編集性のあるUIによるマスタデータの入力 • 並行開発が可能なリビジョン管理

    • 意図しないデータを弾くバリデーション • ゲームプログラム上からキーに基づく高速な検索と(型付きでの)参照 • ゲームプログラム上から検索/参照可能なフォーマットへの変換
  4. © DeNA Co., Ltd. 7 高い編集性のあるUIによるマスタデータの入力 • マスタデータは他の値と比較しながら入力することが非常に多い ◦ このような入力を行うには、表形式UIによる入力が望ましい

    ◦ JSON等を手で書くような形で回るのはプロジェクト初期のみ • ExcelやGoogle Spreadsheetのような高機能な既存の表計算ソフトの活用が望ましい ◦ 他の手法でこれらが備える編集性に追いつくことは困難
  5. © DeNA Co., Ltd. 8 並行開発が可能なリビジョン管理 • 多人数開発でデータの一貫性を保つには入力データのリビジョン管理が必要 • 複数のリリースバージョンに向けた、複数ラインでの並行開発を行うには

    入力データのリリースバージョン管理が必要 ◦ モバイルゲーム開発では開発ラインが更新のリリース1年前に立ち上がることも ◦ 一方で、リリースは少なくとも毎月1回の高頻度で行われる • 開発ラインの1つのスパンが長く、並列度も高い開発に耐える管理手法が必要 ID Name Power Version 1 AAAAA 100 1 2 BBBBB 200 1 3 CCCCC 300 2 4 DDDDD 400 3
  6. © DeNA Co., Ltd. 9 並行開発が可能なリビジョン管理 • マスタデータのリリースバージョンの管理の例 ◦ リリースバージョン毎に異なる開発ブランチを作り、ブランチ単位で管理

    ◦ マスタデータ自体にリリースバージョンを持ち、データ単位で管理 ID Name Power Version 1 AAAAA 100 1 2 BBBBB 200 1 3 CCCCC 300 2 4 DDDDD 400 3
  7. © DeNA Co., Ltd. 10 意図しないデータを弾くバリデーション • 意図しないデータは、ゲームの意図しない振る舞いに直接つながる ◦ モバイルゲームの運営で発生する不具合の、非常に大きな原因のひとつ

    ◦ 開発ビルドの動作を阻害して、チームの開発を止める原因にもなる • データの受け入れ条件をバリデーションとして記述し、データを検証することで、 意図しないデータの混入をできるだけ早期に検知し、ビルドへの混入を避ける
  8. © DeNA Co., Ltd. 11 ゲームプログラム上からキーに基づく高速な検索と(型付きでの)参照 • ゲーム上からは、大量のマスタデータを頻繁に検索することになる ◦ 型がついた状態で安全に参照できることが望ましい

    ▪ ORM(Object Relation Mapper)が必要 • 任意のキーに基づく検索が、ゲームを阻害せず高速で動作する必要がある ◦ ゲームプレイ時に書き込みは伴わないので、読み込み速度が特に重要
  9. © DeNA Co., Ltd. 12 ゲームプログラム上から検索/参照可能なフォーマットへの変換 • マスタデータの検索/参照を高速に行うには、保存フォーマットにも工夫が必要 ◦ バイナリフォーマットを用いることが多い

    ◦ 必然的に、編集性やマージの容易さの面では劣ってしまう • 一方でマスタデータの編集/管理を行うには、編集性やマージの容易さが重要 ◦ 何らかのWebサービス上で管理したり、テキストフォーマットで行うことが多い • 複数のフォーマットを扱う管理が必須になり、それらを変換できる必要が生じる
  10. © DeNA Co., Ltd. 14 必要な要素のDeNAの共通基盤システムによる従来の対応関係 • 編集/バージョン管理/バリデーションはOyakataによってまとめて提供 • サーバは内製ゲームサーバ基盤Takashoから扱う

    ◦ Google Cloud Spanner上のテーブル用のデータとしてINSERTする • クライアントは内製マスタデータORM基盤D4Lから扱う ◦ FlatBuffersで表されたデータに変換する。コンバータは自動生成される
  11. © DeNA Co., Ltd. 15 Oyakata : 内製マスタデータ管理/入力システム • マスタデータを管理するための

    DeNAの共通基盤システム • GithubのようなUIを備えたバージョン管理 ◦ Pull Requestを含むブランチ運用が可能 • Excel及びWeb上でのマスタデータ編集 • マスタデータをJSONに変換するコンバータ • マスタデータの内容を検証するバリデータ 引用元: マスターデータ管理に対する 共通基盤シス テム開発というアプローチ
  12. © DeNA Co., Ltd. 22 問題点1 : 各システムに対して重複してスキーマ定義が必要 • 本質的に同じ構造を重複してスキーマ定義として記述する必要がある

    ◦ 実装コスト増大の他、手作業による記述の不一致によって想定外の挙動を誘発 ◦ 工数増大やリスク回避の為、開発チームでは新規テーブルの作成を避けがちに ▪ 1テーブルのカラム数の増大による編集UXの低下を招く
  13. © DeNA Co., Ltd. 23 問題点2 : 各システムが独立している事によるデプロイフローの複雑化 • ツールも独立している事で実行環境の整備が難しくなる

    ◦ プロビジョニング/利用バージョン管理が必要 • デプロイフローがJenkinsに強く依存する ◦ 整備したJenkins上の実行環境を前提にした結果、 ローカルからのデプロイが実質不可能になる • マスタを編集/確認のイテレーションが鈍化 ◦ デプロイ先環境と確認用クライアントの接続先環境を 一致させる工夫が必要(開発環境の分離) ◦ 実行が自動化できず、編集の度にジョブを手動で起動
  14. © DeNA Co., Ltd. 24 問題点3 : 一部システムのレガシー化 • 内製のクライアントマスタデータ用ORMであるD4Lは8年前から使われてきたレガシー

    ◦ サーバ基盤の置き換えなど前提が大きく変化し、最適な設計とは言えない状態 ◦ 代替となる選択肢がOSSで現れた(MasterMemory & MessagePack for C#) • オープンな選択肢(MasterMemory)のほうが可能な限り望ましい分野 ◦ 内製だけでは利用実績も少なくなるので、なかなかシステムとして枯れない ◦ 内製に拘るより、必要に応じてコントリビューションしていくのが最善 • 移行できる機会を伺っていた
  15. © DeNA Co., Ltd. 25 問題点4: マスタデータ並列開発に伴う”マージリレー”の発生 • リリースバージョン毎にリリースブランチを作成する並行開発を採用 ◦

    全ての変更はターゲットより後にリリースされるバージョンに取り込まれるべき ◦ 逆に、ターゲットより前にリリースされるバージョンに取り込まれてはならない • リリース順で順番にブランチをマージする、”マージリレー”運用が毎日発生する ◦ コンフリクト/不整合の解消も含め、毎日1人がこの作業で一日中かかりきりに
  16. © DeNA Co., Ltd. 26 既存システムの問題への解決策 • 本質的な作業の重複がない、DRYなマスタデータのスキーマ定義手法への移行 • デプロイのイテレーション高速化

    • MasterMemoryへの移行 • マージリレーが発生しない管理手法への移行 トランクベース開発によるデータ管理を前提に 各システムを一貫して制御可能な 統一スキーマ言語を開発する
  17. © DeNA Co., Ltd. 28 トランクベース開発を採用する目的 • トランクベース開発を採用することでマージリレーのない並行開発フローを実現する ◦ マージリレーの痛みは大きく、日に一回、マージリレー担当者が一括で受けてしまう

    • マージそのものは無くせないので、チームが抱える負荷の総量を下げる ◦ マージの痛みを小さく、頻繁に、各々の変更の当事者が受け、処理しやすくする
  18. © DeNA Co., Ltd. 29 非トランクベース開発 • Git Flow等に代表される並行開発手法 •

    developブランチが開発の本流(常に機能するビルド) • 存在期間の長いfeatureブランチで機能開発 ◦ 開発が完了したらdevelopにマージ • リリースを行う単位でreleaseブランチを作成 ◦ (QAなどのフェーズで)不具合修正をコミット ◦ リリースしたらmainにマージ • あるリリース向けの開発ライン自体は1つである前提のフロー ◦ 開発▶QA▶リリースを行う開発ラインを複数持てない ◦ あくまで、機能開発を並行に行う為の手法
  19. © DeNA Co., Ltd. 30 複数ラインでの 非トランクベース開発 • 複数ラインでの開発を実現する為の派生運用の例 •

    不要な変更の混入を避ける為、ライン毎に異なるdevelopを用意 ◦ 前のバージョンのdevelopから派生して、 次のバージョンのdevelopを作成することを繰り返す ◦ 将来のバージョンのデータが入らないように、 過去のバージョンの変更を取り入れ続ける必要がある • 過去のバージョンの変更を取り入れる = マージリレーの実施 ◦ マージリレー担当者が変更を全て把握するのは非常に困難 ▪ 把握していない変更をマージするのも非常に困難 ◦ 変更が大きいほど、コンフリクトのリスクも高い
  20. © DeNA Co., Ltd. 31 トランクベース開発 • 1本のトランクブランチを、唯一の本流ブランチとして扱う並行開発手法 ◦ 原則として、どのバージョンに入れる変更も全てトランクにマージされる

    • 小さな単位/短い生存期間の機能ブランチを、トランクに頻繁にマージする ◦ これによってマージの難易度を下げる • QA前などにリリースブランチを作成。直接コミットせず、トランクにマージしない
  21. © DeNA Co., Ltd. 33 フィーチャーフラグによるリリース制御 • フィーチャーフラグはデータやコード側でフラグ値を元にリリース範囲の制御を行うもの • マスタデータにおいての単純なフィーチャーフラグ

    ◦ 表/行/列のそれぞれの単位でバージョンに基づくフィーチャーフラグを持たせる ◦ 各バージョンで必要な表/行/列だけに絞ったマスタデータを得られるようになる ID Name Power AbilityID (Ver >= 3) RowVersion 1 AAAAA 100 0 1 2 BBBBB 200 0 1 3 CCCCC 300 0 2 4 DDDDD 400 1 3 ID Name Type Cost RowVersion 1 Ability1 Attack 1 3 Abilityマスタ(Ver >= 3) Charaマスタ
  22. © DeNA Co., Ltd. 36 マスタデータ統一スキーマ言語 Muscle • マスタデータの編集からランタイムまでのスキーマ情報を1箇所で記述できる言語 •

    Protocol BuffersによるIDLに基づき各システム用のDTO/設定/コンバータを生成 ◦ Oyakata / Takasho(ゲームサーバ) / MasterMemory(Unityクライアント等)に対応
  23. © DeNA Co., Ltd. 37 Muscleのゴール • 各システムのスキーマ定義を統一スキーマ言語として集約して定義可能にする ◦ 指定された内容に応じて、各システムに対して設定/コードを自動生成する

    ◦ 各システム間のデータ変換コードも自動生成する • トランクベース開発に基づくワークフローを高い再現性/効率性で実現可能にする ◦ フューチャースイッチをスキーマ言語としてサポートし、 統一スキーマ自体もトランクベース開発で管理できるようにする • マスタデータ開発のイテレーションを高速化して、開発効率の改善に寄与する
  24. © DeNA Co., Ltd. 38 Muscleのノンゴール • 全てのシステムの全ての機能を扱えることは目指さない ◦ 機能の網羅性より、全てのシステムを統合して使えることを優先し、

    各サブシステムはMuscleの定めるワークフローの構成要素とみなす ◦ 自由度をある程度絞ることはワークフローの再現性を高める事にもつながる ▪ Muscleの実装が現場のワークフローの実態に直結するようにする
  25. © DeNA Co., Ltd. 39 Muscleを用いたワークフロー : スキーマ定義フロー • スキーマ定義はGithubで管理し、

    トランクベースで運用を行う • スキーマ定義のトランクに変更が入った時、 CIでOyakata上のトランクに自動で反映 • Pull Request作成時にOyakata上で差分を確認 ◦ 差分確認用の仮ブランチを自動生成 • Oyakata側でのマージは行わず、 差分確認用の仮ブランチは廃棄する ◦ トランク更新時に内容は同期される為、 マージは不要になる
  26. © DeNA Co., Ltd. 40 Muscleを用いたワークフロー : スキーマ定義フロー • コードのトランクベース運用は費用対効果が悪い

    ◦ コミットするのはプログラマに限定される為、 マスタデータより相対的にマージコストが低い ◦ ゲームコードでトランクベース開発向けの フィーチャースイッチを運用する障壁は高い • 自動生成されるコードは非トランクベースで運用 ◦ 各バージョンのブランチに、 そのバージョンで必要なコードのみを生成する
  27. © DeNA Co., Ltd. 41 Muscleを用いたワークフロー : データ開発フロー • トランク反映時に開発中の全バージョンの

    マスタデータのデプロイを実行 ◦ フィルタ処理により、1つのトランクから バージョン毎のマスタデータを取得 ◦ それぞれのバージョンのサーバ環境に コンバートしたバイナリのハッシュを登録 • マージ前の確認はUnity Editor内で機能ブランチ を対象にJSONダウンロード & コンバート ◦ 詳しくは後述
  28. © DeNA Co., Ltd. 42 Protocol Buffersによるスキーマ言語の構築 • Protocol Buffers(=Protobuf)とは?

    ◦ Google製の様々な言語に対応した著名なシリアライザー ▪ protocにIDLで記述されたスキーマを渡して各言語用シリアライザを生成 ◦ 言語に依存しないインターフェース定義言語(IDL)基盤としての性質もある • MuscleはProtobufをスキーマ言語構築の基盤として利用 ◦ ProtobufのCustom Optionとprotoc pluginで独自のコード生成を行う ◦ Muscle本体はProtobufをシリアライザとしてはほぼ利用しない ◦ 非常に効率的にスキーマ言語を構築できるほか、利用者のスキーマ記述時に Protobufのエコシステムによって入力補完やLintなどを利用できる
  29. © DeNA Co., Ltd. 43 Protobuf IDLの基本 • messageによって直積型(構造体のような型)を宣言 ◦

    フィールドの型,名前,フィールド番号を宣言 • フィールドに使用できる主な型 ◦ string/bool/int32/int64/uint32/uint64/float/double /bytes/etc../任意のmessage/任意のenum など ◦ repeatedを型名の前に付けることでリスト型に • enumで列挙型を宣言 ◦ デフォルト値(0)の定義が必須 syntax = 'proto3'; message CharaMaster { string Id = 1; string Name = 2; uint64 Power = 3; repeated string Labels = 4; CharaTypes CharaType = 5; } enum CharaTypes { CharaTypes_Unknown = 0, CharaTypes_Playable = 1, CharaTypes_NonPlayable = 2 }
  30. © DeNA Co., Ltd. 44 カスタムオプションによる拡張 • message, field, enum等の各構文ごとに

    オプション(アノテーション)指定による 付加情報の付与が可能 ◦ 標準のオプションの構造は固定 • 既定のオプションをextend構文で拡張すると 独自のカスタムオプションを定義できる • カスタムオプションを利用して、 統一スキーマ言語として必要なアノテーションを スキーマ中に記述できるようにする syntax = 'proto3'; package sample; message CustomMessageOptions { optional string string_option = 1; optional bool bool_option = 2; } extend google.protobuf.MessageOptions { CustomMessageOptions custom_message = 7739036; } message MessageWithCustomOption { option (sample.custom_message) = { string_option: "hoge", bool_option: true }; }
  31. © DeNA Co., Ltd. 45 Protocol Buffersによるコード生成の構造 • 自動生成のロジックとIDLのパースがプロセス単位で分離されている ◦

    protocプラグインを実装することで、独自の自動生成を容易に実装できる • Protobuf IDLをprotocがDescriptorという構造にパースし、protocプラグインに渡す ◦ DescriptorはProtobuf IDLの構文自体を、Protobufデータとして表したもの ◦ プラグインは標準入出力でDescriptorを受け取り、自動生成内容を返す
  32. © DeNA Co., Ltd. 46 主要なDescriptorとその主な関連 • Message, Field, Enum,

    EnumValueといったProtobuf IDL の主要なプリミティブにそれぞれのDescriptorが存在 ◦ 言語構造と一致するように他を内包する構造 • ファイル単位のDescriptorに1ファイル中に定義されてい るMessage, EnumのDescriptorが含まれる • 独自のカスタムオプションとして指定したアノテーショ ンの内容も、 このそれぞれのDescriptorから取得することができる • Protobuf IDLに記述されている情報は、 基本的に全てDescriptorから読み取ることが可能
  33. © DeNA Co., Ltd. 47 Muscle IDLの基本 • 簡単なテーブルを1つ定義する例 •

    messageを一つ作成 • カラムをmessage中のフィールドとして宣言 ◦ map以外の標準的な型には殆ど対応 ◦ プリミティブ型のリストにも対応 • DTO/コンバータでもこのテーブルが扱える ようにコード生成される syntax = 'proto3'; package muscle.sample; import "muscle/custom_option.proto"; message CharaMaster { option (muscle.message) = { //テーブルとして扱うことを宣言 table: true, oyakata:{use:true} //サーバとクライアントの双方で利用することを宣言 master_memory:{use:true}, takasho:{use:true} }; string Id = 1 [(muscle.field) = { primary_key: true //主キーであることを宣言 }]; string Name = 2; uint64 Power = 3; }
  34. © DeNA Co., Ltd. 48 Muscle処理系のアーキテクチャ • ツールチェインは全てC#で実装 ◦ 出力対象毎にprotocプラグインを実装

    • 叩きやすいフロントエンド(Muscle.Compiler)を用意 ◦ 与えられたProtobuf IDLを元に、protoc及び protocプラグインを使って、それぞれを出力 ▪ プラグインのバイナリを内部で抱えて隠蔽 ◦ 出力に対するポストプロセス処理も実施 ▪ MessagePack/MasterMemory側のコード生成 ▪ コンバータのCI実行用のバイナリ(nupkg)生成
  35. © DeNA Co., Ltd. 49 Muscleによる生成物 : Oyakata向け • 宣言的なIaC(Infrastructure

    as Code)のような構造を採用 • IDLからは、Oyakataへの設定として要求する内容をJSON化した 中間フォーマット(Oyakata Requirement)を生成 ◦ テーブル/スキーマ/バリデーション/リレーションなどの情報 • Oyakata Requirementを実際にOyakataへ適用する別ツールを用意 ◦ 既存リソースへの差分を正しく差分として反映する必要 ◦ カラム追加で既存テーブルを破壊する等の振る舞いはNG
  36. © DeNA Co., Ltd. 50 Muscleによる生成物 : Oyakata向け • 設定生成と設定適用を分離したメリットは大きい

    • 設定生成と設定適用をそれぞれ独立して開発/検証できる ◦ 不具合発生時の原因究明が容易 ▪ 設定生成と設定適用のどちらが原因かを切り分けられる ◦ 設定生成を外部への副作用なくテスト可能 ◦ 設定適用時の実装/テストも容易に ▪ 最小テストケースをJSONとして固定化して用意できる • 内部設計としても、責務分離の効いた非常に健全な状態を保てる
  37. © DeNA Co., Ltd. 51 Muscleによる生成物 : MasterMemory向け • MasterMemoryでのマスタの読み込みに必須な

    C#のclassとしてのスキーマ定義(DTO)を生成 • OyakataのデータからMasterMemoryのファイルへの コンバータ(Unity/CLIアプリ)を生成 • Oyakata上で編集してからUnity上で読み込むために 必要な全ての実装が揃うように自動生成される [MessagePackObject(true)] [MemoryTable(nameof(CharaMaster))] public class CharaMaster { [PrimaryKey] public string Id { get; set; } public string Name { get; set; } public ulong Power { get; set; } }
  38. © DeNA Co., Ltd. 52 Muscleによる生成物 : Takasho(サーバ)向け • サーバ側のデータベースへのインポートAPI用の

    Protobuf IDL(gRPC用)を生成する ◦ Muscle IDL(統一スキーマ)とは別のProtobuf IDL • Oyakataのデータから上記Protobufバイナリへの コンバータ(Unity/CLIアプリ)を生成 ◦ 変換したバイナリをインポートAPIへの入力にする • インポートAPIの実装はサーバ側での実装作業が必要 ◦ 現状はMuscle IDL上の記述からわかる自動化に必要な 情報を一部アノテーションとして残すようにしている ◦ サーバの実装に依らず柔軟に接続できるとも言える message CharaMaster { string Id = 251844781 [ json_name = "Id", (muscle.importer_field) = { primary_key: true } ]; string Name = 421700962 [ json_name = "Name" ]; uint64 Power = 299945930 [ json_name = "Power" ]; }
  39. © DeNA Co., Ltd. 53 プロビジョニングを容易にするための仕組み • Muscle.Compilerやその生成結果も含め、 CLIツールを全てdotnet toolで扱う

    ◦ nugetパッケージとしてパッケージ化 ◦ Github Packagesのnugetレジストリに登録 ◦ Unity用にはUPMパッケージ化して出力 • dotnet tool restoreを実行するだけで、 CLIツールのプロビジョニングが全て完了する
  40. © DeNA Co., Ltd. 54 Muscle IDLの機能 • Protobufをシリアライザとして用いない為、IDLの定義の解釈を独自に自由に行える ◦

    カスタムオプションで構文上記述できる内容は増やせる ◦ 手続きを記述しない宣言的な機能は概ねIDLとして表現できる • 自由度を活かし、マスタデータ運用に必要な様々な言語機能をIDL上に実現している ◦ トランクベース開発を可能にする言語機能 ◦ マスタデータの表現力を上げる言語機能
  41. © DeNA Co., Ltd. 55 トランクベース開発を可能にする言語機能(一部) • 最低要求バージョンによるフューチャーフラグ (表/列/行) •

    機能トグルによるフューチャーフラグ(表/列/行) • 宣言的バリデーション • 行単位のデバッグ用データ宣言
  42. © DeNA Co., Ltd. 56 最低要求バージョンによるフューチャーフラグ • 表/列/行が有効になる為に必要なVersionの最低 値をそれぞれ宣言し、満たない場合は無効化 •

    Oyakataで全てのバージョン向けのデータを トランクブランチ一本で共存可能に ◦ OyakataでのフィルタはDL時に行われる • 要求を満たせば後続でも自動的に有効化される ◦ リリースバージョンを基準とした、 トランクベース開発の為の フューチャーフラグとして機能する message CharaMaster { option (muscle.message) = { //...省略 //行単位の要求バージョンを利用する oyakata: { row_versioning: true } //表単位の要求バージョンを利用する //Versionが >= 2 の時にテーブルが有効になる required_version: 2 }; //...省略.. AbilityTypes CharaAbilityType = 4[(muscle.field) = { //列単位の要求バージョンを利用する //Versionが >= 3 の時にカラムが有効になる required_version: 3 }]; string CharaAbilityId = 5 [(muscle.field) = { relation_switch_by: "CharaAbilityType" required_version: 3 }]; }
  43. © DeNA Co., Ltd. 57 最低要求バージョンによるフューチャーフラグ • RequiredVersion列に記述した最低要求バージョンを 満たす場合のみ出力される •

    過去のバージョンのレコードも出力対象になる ID Name Power RequiredVersion 1 AAAAA 100 0 2 BBBBB 100 1 3 CCCCC 300 2 ID Name Power 1 AAAAA 100 2 BBBBB 100 ID Name Power 1 AAAAA 100 2 BBBBB 100 3 CCCCC 300 Version 1 Version 2
  44. © DeNA Co., Ltd. 58 機能トグルによるフューチャーフラグ • 任意の文字列を識別子にtrue/falseを設定する、 機能トグルという概念を定義する •

    表/列/行が 有効になる為に必要な機能トグルを宣言 • バージョン毎に有効化する機能トグルを宣言 ◦ 有効化した機能トグルは後続のバージョン でも有効化できる(しないことも可能) ◦ コード生成時のCLI引数で一時的有効化も可 • 未完成機能のスキーマをトランクへマージ可能に ◦ 通常のビルドには含めない状態にできる option (muscle.file) = { //Version >=2 で ”Chara”を //Version >=3 で ”CharaAbility”を有効にする global_use_feature_per_version: [ { version: 2, inheritable: ["Chara"] }, { version: 3, inheritable: ["CharaAbility"] }, ] }; message CharaMaster { option (muscle.message) = { //表単位の機能トグルを利用する //”Chara”が有効な時にテーブルが有効化 required_feature: ["Chara"] }; //...省略.. AbilityTypes CharaAbilityType = 4[(muscle.field) = { //列単位の機能トグルを利用する //”CharaAbility”が有効な時にカラムが有効化 required_feature: ["CharaAbility"] }]; //...省略.. }
  45. © DeNA Co., Ltd. 59 機能トグルによるフューチャーフラグ ID Name Power RequiredFeature

    1 AAAAA 100 2 BBBBB 100 Chara 3 CCCCC 200 Chara 4 DDDDD 300 CharaAbility 5 EEEEE 400 CharaAbility • RequiredFeature列に記述した機能トグルが 有効化されている場合のみ出力される ◦ 記述がない場合、常時出力対象となる • 複数の機能トグルの有効化も可能 ID Name Power 1 AAAAA 100 2 BBBBB 100 3 CCCCC 200 ID Name Power 1 AAAAA 100 4 DDDDD 300 5 EEEEE 400 Charaのみ 有効時 CharaAbilityのみ 有効時
  46. © DeNA Co., Ltd. 60 宣言的バリデーション • OyakataはmrubyによるDSLを記述することで マスタデータに対するバリデーションを行える •

    Muscleでカラムの正常な値の条件を宣言的に記述 ◦ 記述を元にバリデーションDSLを生成/設定 ◦ 他の文法上自明な内容もDSLを生成(外部参照等) • 複雑なバリデーションは手動での記述も可能 message CharaMaster { //...省略 uint64 Power = 3 [(muscle.field) = { validations: [ { message: "最大値は1000", condition: { max: {long: 1000} } } ] }]; //...省略 } #Muscleから自動生成された定義です。直接編集しないで下さい value { # 最大値は1000 assert (@Power <= 1000), "最大値は1000 [Power = #{@Power}]" }
  47. © DeNA Co., Ltd. 61 行単位のデバッグ用データ宣言 • 製品では利用しないが開発時の動作確認には必要 なマスタデータも存在する •

    製品には入らないように行単位で制御する ◦ フューチャーフラグの一種 • リリースモードでは特定の条件の行を削除 ◦ IsDebugDataカラムがtrueであるもの • contains_debug_rowを宣言して有効化 message CharaMaster { //省略.. option (muscle.message) = { //省略.. oyakata: { //行単位のデバッグデータ制御 を //利用することを宣言 contains_debug_row: true } }; //省略.. }
  48. © DeNA Co., Ltd. 63 テーブル間リレーション • 他のテーブルの主キーを外部キーとして保持して参照する機能 ◦ IDL上で参照先テーブルのmessage型をフィールドの型に指定

    • MasterMemory/Takashoでは参照先の主キーの型として解釈 ◦ 参照先テーブルの行を取得するメソッドも生成される • Oyakata上では参照先のテーブル内容で入力補完が有効になる ◦ 編集時は主キー以外のカラムをもとに選択することも可能 ◦ 被参照側の行から参照している行を探すことも可能 message CharaMaster { option (muscle.message) = { table: true, master_memory:{use:true}, takasho:{use:true} }; string Id = 1 [(muscle.field) = { primary_key: true }]; string Name = 2; uint64 Power = 3; AbilityMaster AbilityId = 4; } message AbilityMaster { option (muscle.message) = { table: true, master_memory:{use:true}, takasho:{use:true} }; string Id = 1 [(muscle.field) = { primary_key:true }]; string Name = 2; }
  49. © DeNA Co., Ltd. 64 1:Nリレーション • 1つのテーブルのカラムから異なる複数のテーブル を参照するようなリレーション •

    参照先テーブル種別を表すenum値のカラムと 参照先への外部キーのカラムのペアで表現する ◦ enumの値ごとに紐づくテーブルを指定する message CharaMaster { /* 省略*/ /* 1:Nリレーションの対象テーブルを示すフィールド (enum型) */ AbilityTypes AbilityType = 3; string AbilityId = 4 [(muscle.field) = { /* 1:Nリレーションで複数のマスタを参照する際の、 対象テーブルの区別に使う フィールド名を指定。 */ relation_switch_by: "AbilityType" }]; } enum AbilityTypes { option (muscle.enum) = { master_memory: {use: true}, oyakata: {use: true} /* 1:Nリレーションの対象の区別に使う enumであることを宣言 */ relation_switch: true }; AbilityTypes_Unknown = 0; AbilityTypes_Heal = 1 [(muscle.enum_value) = { /* 紐づくマスタのmessage型をAny型の空メッセージとして指定 */ relation_type:{ [type.googleapis.com/muscle.sample.HealAbilityMaster]:{} } } ]; AbilityTypes_Damage = 2[(muscle.enum_value) = { relation_type:{ [type.googleapis.com/muscle.sample.DamageAbilityMaster]:{} } } ]; } message DamageAbilityMaster { /* 省略*/ string Id = 1 [(muscle.field) = {primary_key:true}]; uint64 DamageAmount = 2; } message HealAbilityMaster { /* 省略*/ string Id = 1 [(muscle.field) = {primary_key:true}]; uint64 HealAmount = 2; }
  50. © DeNA Co., Ltd. 65 直積型(構造体) • テーブルではないmessageは直積型(構造体)として扱われる ◦ フィールドの型として指定することで利用可能

    • Oyakataは1階層までの直積型を複数のカラムで表現可能 ◦ 直積型の各フィールドを展開した.付きの名前で表現 • MasterMemoryでは直積型をそのままclassとして表現 message Position { double X = 1; double Y = 2; } message CharaMaster { //省略.. Position Position = 2; //..省略 } { "Id": 1, "Position": { "X": 12.345, "Y": 67.89 } } public class Position { public double X { get; set; } public double Y { get; set; } } public class CharaMaster { public string Id { get; set; } public Position Position { get; set; } }
  51. © DeNA Co., Ltd. 66 enum型 • Muscle IDL中のenumを各システムでもenumとして生成 ◦

    MasterMemoryではC#のenum ◦ Takashoではprotobufのenum ◦ Oyakataではenumの値から固定行のテーブルを作成 ▪ リレーションが設定され、入力補完が有効化 enum AbilityTargets { option (muscle.enum) = { master_memory: {use: true} oyakata: {use: true} }; AbilityTargets_Unknown = 0; AbilityTargets_Single = 1; AbilityTargets_All = 2; } message HealAbilityMaster { /* 省略*/ string Id = 1 [(muscle.field) = { primary_key:true }]; int64 HealAmount = 2; AbilityTargets Target = 3; }
  52. © DeNA Co., Ltd. 67 Mix-in • テーブル間でのフィールド定義の共通化を可能にする • 共通化するフィールドを定義したmessageを記述

    • テーブルから上記をmixin宣言をつけて型として参照 ◦ 対象の型のフィールド定義が全て取り込まれる ◦ Mixin後のフィールド名のPrefixも指定できる • 複数のテーブルからこれを行うことで共通化が可能 message CharaMaster { //省略 string Id = 1 [(muscle.field) = { primary_key: true }]; string Name = 2; uint64 Power = 3; AbilityReference CharaAbility [(muscle.field) = { //Prefixを付与した上で対象の //全てのフィールドを取り込む mixin: {use: true, prefix: "Chara"} }]; } message AbilityReference { AbilityTypes AbilityType = 1; string AbilityId = 2 [(muscle.field) = { relation_switch_by: "AbilityType" }]; } // enum AbilityTypes { 省略 }
  53. © DeNA Co., Ltd. 69 Unity Editorでのマスタデータ確認のイテレーションの高速化 • マスタデータ変更のコミット後、デプロイ完了を待つのはイテレーションを阻害する •

    Muscleは全領域のスキーマ定義とコンバート手段を掌握している ◦ コンバートをUnity Editorからも実行可能なC#コードとして提供している Unity Editorで再生するだけで、編集した最新のマスタデータに 基づいて、サーバを含めてゲームが動作するようにする
  54. © DeNA Co., Ltd. 70 マスタデータの透過的なデプロイをUnity Editor上で行う • 自動生成されるコンバータはUnity上でC#コードとして直接実行可能な状態で出力 ◦

    EditorでもランタイムでもいつでもJSONから変換が可能 ◦ その場で透過的にマスタデータを変換して読み込むことができる • OyakataのJSONのダウンロードもUnity Editorから実行可能 ◦ プロビジョニングを不要にするため、Oyakataの専用Downloaderの実行バイナリを、 Unity Editorから透過的にダウンロードして実行するヘルパを用意 • サーバへのマスタデータのインポート処理もgRPCによってUnityから呼び出し可能にする ◦ サーバ側で利用するマスタをリクエストヘッダを元に切り替えることも可能にする
  55. © DeNA Co., Ltd. 72 マスタデータの透過的なデプロイをUnity Editor上で行う • Unity Editorで再生するだけで、編集した最新のマスタデータに基づいて、

    サーバを含めてゲームが動作するようになった • マスタデータはキャッシュを行い、差分のみがダウンロードされる ◦ デプロイにかかる実行時の待ち時間は10秒未満
  56. © DeNA Co., Ltd. 73 Unity Searchによるマスタデータの検索 • Oyakata上のマスタデータをUnity上から直接検索/確認可能にして、 Unity内からのアクセス性を改善する

    ◦ Unityの検索機能であるUnity Search上でマスタデータを扱うSearchProviderを実装 ◦ Unity Searchの高度なクエリエンジンを通してマスタデータを検索できる
  57. © DeNA Co., Ltd. 74 ワークフロー改善自体を加速する • マスタデータワークフローを改善する試み自体のイテレーションを改善する • Muscleを複数のシステムに跨るワークフロー改善の受け口にできるようにする

    ◦ 各システムに直接的な機能がない物でも、多くの改善を迅速に提供できる ◦ 各システムに対する手運用で実現可能なものは、Muscleによる定型化も容易 ◦ 手運用では現実には運用不可能な程に複雑なものも、Muscleからなら扱える • これらに後から各システムが特化した機能追加を行った際、 Muscleからの制御を変えることで同じIDLのまま透過的に移行することが可能になる ◦ 特にOyakataで編集時の体験を高める機能を後から追加する際などに有効
  58. © DeNA Co., Ltd. 76 • Muscleの採用タイトルはまだ数本程度 ◦ 課題が出きっていない状態 •

    タイトル側のワークフローの改善とセットにしてMuscleも改善する動きを継続中 ◦ Muscleを通すことで、改善の再現性が高い状態は現在も担保できている • 今後も実績と改善を共に積み上げていく 運用実績の積み重ね
  59. © DeNA Co., Ltd. 77 サーバ側に対するコード生成内容の強化 • 現状ではRDBMSにインポートするコードは自動生成できていない ◦ インポートAPIの入力になるスキーマとコンバータだけを自動生成している

    ◦ サーバの内部実装に合わせて柔軟に結合できる一方で、自動化できていない運用 が残っている • いずれ、サーバ側でマスタデータが参照するために必要な実装も自動生成したい ◦ RDBMSに対するインポートコードやDDLの生成 ◦ サーバ(golang)向けのインメモリデータベースの実装も検討
  60. © DeNA Co., Ltd. 79 まとめ • MuscleをProtobufを活用して実装する事で、統一したDRYなスキーマ記述で トランクベース開発によるマスタデータ開発が可能になった •

    これにより、マージリレーのないマスタデータ開発が可能になった • 全てのスキーマを知ることを活かして、イテレーション高速化などの マスタデータ開発の効率を高める機能を高い再現性/汎用性で実現できた
  61. © DeNA Co., Ltd. 80 • Trunk Based Development •

    Compilation and Descriptors | The Protobuf Language 参考文献