GoのContextではリクエストスコープの値を伝播する事ができます。 主な利用例として認証トークンを伝搬させる手法がありますが、ゲーム開発においては他にも様々なContextの利用方法があります。
この資料では大規模ゲーム開発で利用しているContextの活用パターンについてお話しします。 Contextを活用することで、DBアクセス頻度を少なくしたり、レスポンスサイズを小さくする工夫ができたため、その実装手法を共有します。
大規模ゲーム開発におけるContext活用パターンGo Conference 2022 Spring@8kka
View Slide
自己紹介8kka(はっか)● 株式会社QualiArts所属● Goでのゲーム開発歴は2年ぐらい● GCPやk8sなどのインフラ構築も担当● 最近技術書店でGoの本出しました2
大規模ゲーム開発で問題になる点①type interactor struct {mCache mcache.CacheuserTxManager database.UserTxManageractionLogger action.LoggerconditionComponent condition.ComponentmissionComponent missioncomponent.ComponentidConverterComponent idconverter.ComponentlogComponent log.ComponentliveFacade live.FacadequestFacade questfacade.FacadegvgFacade gvg.FacadehierarchyFacade hierarchy.FacadequestLiveService questservice.LiveServicequestRewardService questservice.RewardServicequestListService questservice.ListServicedeckService deckservice.ServicephotoService photoservice.ServicerewardService rewardservice.ServiceconsumptionService consumption.ServiceuserService userservice.ServicegiftService giftservice.ServicepvpService pvpservice.ServicemessageService message.ServiceactivityLessonService activitylesson.ServicehierarchyService hierarchyservice.ServicetutorialService tutorial.ServicedivisionService division.Service}同じデータへのアクセスが様々な箇所で発生するある処理を実装するInteractorでアイテム情報を扱うServiceやComponentはこれだけある● conditionComponent● questRewardService● rewardService● consumptionService● giftService3
大規模ゲーム開発で問題になる点②APIの処理が複雑で、データの伝搬が困難更新データをクライアントに返す場合、伝搬させる階層が多く煩雑になりやすいHandlerInteractorFacadeServiceComponentInfra4
コンテキスト利用例一覧● Game Context● Query Cache● Data Change5
Game Context認証情報、リクエスト情報、ユーザー情報など各所に伝搬させたい情報を保持する● 一般的にGoのContextで利用されるような情報を定義する● ここで扱う情報はイミュータブルな値で、変更はされない6
Game Contexttype contextKey struct{} // Contextに保持する際のkeytype GameContext struct {Auth *AuthRequest *RequestLog *LogNow 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.go7
Query CacheDBから取得したトランザクションデータをContext内でキャッシュする仕組み処理の流れ● リクエスト単位で初めて叩かれるクエリはDBから取得してContextに保持● リクエスト単位で同一のクエリが叩かれる場合はContextから取得● データが更新された場合はContext内のデータも更新する○ (トランザクション処理はリクエストの最後に一括で行う)利点● 各所で同じクエリを叩いてもDBアクセスが走らない● リクエスト単位で状態を保持したレコードを扱える8
Query Cachetype QueryCache interface {Set(key string, value interface{})Get(key string) interface{}Exists(key string) boolGetAndExists(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.RWMutexqueries []*querycaches map[string]*cache // PKをkeyとするクエリ結果}// クエリ結果の情報(userテーブルの場合)type cache struct {value *user.User}// キャッシュに利用したクエリ情報(conditionはカラムや条件の情報)type query struct {conditions []*condition}> querycache.go > usercache.go9
Query Cache (condition)type query struct {conditions []*condition}type condition struct {column stringoperator ConditionOperatorvalue interface{}}// conditionの例(SELECT * FROM user WHERE UserID = "foo")// {// column: "UserID",// operator: ConditionOperatorEq,// value: "foo",// },> usercache.gouser_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
Data Changeリクエスト内で変更されたトランザクションデータをクライアントに返す仕組み処理の流れ● Insert / Update / Delete されたデータをContext内に保持● API処理の最後にMiddleware内の処理で更新データを一括でフォーマットする● 更新されたデータをクライアント側で受け取る利点● Infra層でのDB更新結果をHandler層まで伝搬できる11
Data Changetype DataChange interface {AddUpdatedData(userID string, data interface{}) errorAddDeletedData(userID string, data interface{}) errorGetUpdated() *UpdatedDataGetDeleted() *DeletedDataClear()}type dataChange struct {updated *UpdatedDatadeleted *DeletedData}// UserItems など更新されるトランザクションデータ毎にmapを保持type UpdatedData struct {sync.RWMutexUser *user.UserUserItems map[string]*useritem.UserItemUserCards map[string]*usercard.UserCardUserCharacters map[string]*usercharacter.UserCharacter}// UserItemPKs など、削除されるトランザクションデータ毎にPKを保持type DeletedData struct {sync.RWMutexUserItemPKs map[string]*useritem.PKUserDeckPKs map[string]*userdeck.PKUserPointPKs map[string]*userpoint.PK}> datachange.go > datachange.go12
Data Changefunc (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.updatedswitch 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.go13
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.go14
まとめ大規模ゲームを開発する上でContextを以下のように利用している● Game Context … 認証情報やユーザー情報● Query Cache … DBアクセスした内容をキャッシュしておく仕組み● Data Change … トランザクションデータを伝搬する仕組み(その他、ステータス管理や課金情報の管理にもContextを利用している)15