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

大規模ゲーム開発におけるContext活用パターン

 大規模ゲーム開発におけるContext活用パターン

GoのContextではリクエストスコープの値を伝播する事ができます。 主な利用例として認証トークンを伝搬させる手法がありますが、ゲーム開発においては他にも様々なContextの利用方法があります。

この資料では大規模ゲーム開発で利用しているContextの活用パターンについてお話しします。 Contextを活用することで、DBアクセス頻度を少なくしたり、レスポンスサイズを小さくする工夫ができたため、その実装手法を共有します。

8192a3d22a844b926f2e1bd7aef5fb25?s=128

QualiArts

April 23, 2022
Tweet

More Decks by QualiArts

Other Decks in Programming

Transcript

  1. 大規模ゲーム開発における Context活用パターン Go Conference 2022 Spring @8kka

  2. 自己紹介 8kka(はっか) • 株式会社QualiArts所属 • Goでのゲーム開発歴は2年ぐらい • GCPやk8sなどのインフラ構築も担当 • 最近技術書店でGoの本出しました

    2
  3. 大規模ゲーム開発で問題になる点① type interactor struct { mCache mcache.Cache userTxManager database.UserTxManager actionLogger

    action.Logger conditionComponent condition.Component missionComponent missioncomponent.Component idConverterComponent idconverter.Component logComponent log.Component liveFacade live.Facade questFacade questfacade.Facade gvgFacade gvg.Facade hierarchyFacade hierarchy.Facade questLiveService questservice.LiveService questRewardService questservice.RewardService questListService questservice.ListService deckService deckservice.Service photoService photoservice.Service rewardService rewardservice.Service consumptionService consumption.Service userService userservice.Service giftService giftservice.Service pvpService pvpservice.Service messageService message.Service activityLessonService activitylesson.Service hierarchyService hierarchyservice.Service tutorialService tutorial.Service divisionService division.Service } 同じデータへのアクセスが様々な箇所で発生する ある処理を実装するInteractorでアイテム情報を 扱うServiceやComponentはこれだけある • conditionComponent • questRewardService • rewardService • consumptionService • giftService 3
  4. 大規模ゲーム開発で問題になる点② APIの処理が複雑で、データの伝搬が困難 更新データをクライアントに返す場合、伝搬させ る階層が多く煩雑になりやすい Handler Interactor Facade Service Component Infra

    4
  5. コンテキスト利用例一覧 • Game Context • Query Cache • Data Change

    5
  6. Game Context 認証情報、リクエスト情報、ユーザー情報など各所に伝搬させたい情報を保持する • 一般的にGoのContextで利用されるような情報を定義する • ここで扱う情報はイミュータブルな値で、変更はされない 6

  7. Game Context type contextKey struct{} // Contextに保持する際のkey type GameContext struct

    { Auth *Auth Request *Request Log *Log Now time.Time } // Contextへの設定と取得 func Set(ctx context.Context, gctx *GameContext) context.Context { return context.WithValue(ctx, contextKey{}, gctx) } func Extract(ctx context.Context) *GameContext { value := ctx.Value(contextKey{}) if value == nil { return nil } return value.(*GameContext) } Auth:userID, authToken etc... • ユーザーの認証に必要な情報 Request:ip, os, api, illegalData etc... • リクエストに含まれる情報 • クライアントで検知した不正判定なども含まれる Log:logID etc... • ログ出力に利用する情報 • 同じAPIから出力されたログはlogIDで判定する Now: • API共通で利用する現在時刻の情報 > gamecontext.go 7
  8. Query Cache DBから取得したトランザクションデータをContext内でキャッシュする仕組み 処理の流れ • リクエスト単位で初めて叩かれるクエリはDBから取得してContextに保持 • リクエスト単位で同一のクエリが叩かれる場合はContextから取得 • データが更新された場合はContext内のデータも更新する

    ◦ (トランザクション処理はリクエストの最後に一括で行う) 利点 • 各所で同じクエリを叩いてもDBアクセスが走らない • リクエスト単位で状態を保持したレコードを扱える 8
  9. Query Cache type QueryCache interface { Set(key string, value interface{})

    Get(key string) interface{} Exists(key string) bool GetAndExists(key string) (interface{}, bool) Clear() } type queryCache struct { sync.RWMutex // テーブル名をkeyとするクエリ結果情報群 // keyの例) user, usercard, useritem etc... cacheMap map[string]interface{} } // cacheMapのvalueに当たる構造(テーブル毎に自動生成) type cacher struct { sync.RWMutex queries []*query caches map[string]*cache // PKをkeyとするクエリ結果 } // クエリ結果の情報(userテーブルの場合) type cache struct { value *user.User } // キャッシュに利用したクエリ情報(conditionはカラムや条件の情報) type query struct { conditions []*condition } > querycache.go > usercache.go 9
  10. Query Cache (condition) type query struct { conditions []*condition }

    type condition struct { column string operator ConditionOperator value interface{} } // conditionの例(SELECT * FROM user WHERE UserID = "foo") // { // column: "UserID", // operator: ConditionOperatorEq, // value: "foo", // }, > usercache.go user_itemの取得 1. WHERE UserID IN (“userA”, “userB”) -> userAとuserBのuser_itemをDBから取得 -> 以下のconditionを設定する  {column:”UserID”, operator:”IN”, value”userA”}  {column:”UserID”, operator:”IN”, value”userB”} 2. WHERE UserID = “userA” -> userAのuser_itemをContextから取得 10
  11. Data Change リクエスト内で変更されたトランザクションデータをクライアントに返す仕組み 処理の流れ • Insert / Update / Delete

    されたデータをContext内に保持 • API処理の最後にMiddleware内の処理で更新データを一括でフォーマットする • 更新されたデータをクライアント側で受け取る 利点 • Infra層でのDB更新結果をHandler層まで伝搬できる 11
  12. Data Change type DataChange interface { AddUpdatedData(userID string, data interface{})

    error AddDeletedData(userID string, data interface{}) error GetUpdated() *UpdatedData GetDeleted() *DeletedData Clear() } type dataChange struct { updated *UpdatedData deleted *DeletedData } // UserItems など更新されるトランザクションデータ毎にmapを保持 type UpdatedData struct { sync.RWMutex User *user.User UserItems map[string]*useritem.UserItem UserCards map[string]*usercard.UserCard UserCharacters map[string]*usercharacter.UserCharacter } // UserItemPKs など、削除されるトランザクションデータ毎にPKを保持 type DeletedData struct { sync.RWMutex UserItemPKs map[string]*useritem.PK UserDeckPKs map[string]*userdeck.PK UserPointPKs map[string]*userpoint.PK } > datachange.go > datachange.go 12
  13. Data Change func (d *dataChange) AddUpdatedData(userID string, data interface{}) error

    { if reflect.TypeOf(data).Kind() != reflect.Ptr { return fmt.Errorf("attempt to add a non-pointer") } updated := d.updated switch castdata := data.(type) { case *useritem.UserItem: // UserItemが更新されている場合、castdataを詰める if updated.UserItems == nil { updated.UserItems = make(map[string]*useritem.UserItem) } updatedData.UserItems[generateKey(castdata.ItemID)] = castdata } // case *usercard.UserCard: のように、他のトランザクションデータも記述する return nil } > datachange.go 13
  14. Data Change // データ更新情報を乗せるレスポンスかチェック dcRes, ok := res.(dataChangeResponse) if !ok

    { return res, nil } // 更新,削除データを入れる変数を用意する updated := &pb.UpdatedData{} deleted := &pb.DeletedData{} dataChange := datachange.Extract(ctx) if dataChange == nil { return res, nil } updatedData := dataChange.GetUpdated() deletedData := dataChange.GetDeleted() for _, item := range updatedData.UserItems { pbitem := converter.ToProtoUserItem(item) updated.Items = append(updated.Items, pbitem) } for _, deck := range updatedData.UserDecks { pbdeck := converter.ToProtoUserDeck(deck) updated.Decks = append(updated.Decks, pbdeck) } for _, point := range updatedData.UserPoints { pbpoint := converter.ToProtoUserPoint(point) updated.Points = append(updated.Points, pbpoint) } dcRes.SetCommonResponse(&pb.CommonResponse{ UpdatedData: updated, DeletedData: deleted, }) > middleware.go > middleware.go 14
  15. まとめ 大規模ゲームを開発する上でContextを以下のように利用している • Game Context … 認証情報やユーザー情報 • Query Cache

    … DBアクセスした内容をキャッシュしておく仕組み • Data Change … トランザクションデータを伝搬する仕組み (その他、ステータス管理や課金情報の管理にもContextを利用している) 15