Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

大規模ゲーム開発で問題になる点① 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

Slide 4

Slide 4 text

大規模ゲーム開発で問題になる点② APIの処理が複雑で、データの伝搬が困難 更新データをクライアントに返す場合、伝搬させ る階層が多く煩雑になりやすい Handler Interactor Facade Service Component Infra 4

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Query Cache DBから取得したトランザクションデータをContext内でキャッシュする仕組み 処理の流れ ● リクエスト単位で初めて叩かれるクエリはDBから取得してContextに保持 ● リクエスト単位で同一のクエリが叩かれる場合はContextから取得 ● データが更新された場合はContext内のデータも更新する ○ (トランザクション処理はリクエストの最後に一括で行う) 利点 ● 各所で同じクエリを叩いてもDBアクセスが走らない ● リクエスト単位で状態を保持したレコードを扱える 8

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

Data Change リクエスト内で変更されたトランザクションデータをクライアントに返す仕組み 処理の流れ ● Insert / Update / Delete されたデータをContext内に保持 ● API処理の最後にMiddleware内の処理で更新データを一括でフォーマットする ● 更新されたデータをクライアント側で受け取る 利点 ● Infra層でのDB更新結果をHandler層まで伝搬できる 11

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

まとめ 大規模ゲームを開発する上でContextを以下のように利用している ● Game Context … 認証情報やユーザー情報 ● Query Cache … DBアクセスした内容をキャッシュしておく仕組み ● Data Change … トランザクションデータを伝搬する仕組み (その他、ステータス管理や課金情報の管理にもContextを利用している) 15