案例分享:使用 Agones 在 TKE 上部署游戏专用服务器
术语
- DS:Dedicated Server,游戏专用服务器。在线对战的房间类游戏,同一局的玩家都会连上同一个 DS。本文中的方案采用单 Pod 单房间模式,可以认为一个 DS 就是一个游戏房间。
用户诉求
有一款 PVP(房间类)的游戏基于虚幻引擎 UE5.4 开发,玩家在线匹配到一个房间后,连上同一个 DS 开始进行对战。
具 体流程如下。
- 首先玩家准备游戏,游戏客户端请求大厅服进行游戏匹配:
- 大厅服对玩家进行匹配,匹配成功后,大厅服会根据玩家所选地图、游戏模式和最终匹配人数等条件筛选出符合条件的 DS,将选中的 DS 标记为已分配并下发玩家信息,让 DS 加载:
- 等 DS 加载完玩家信息后,大厅服通知游戏客户端开始游戏并提供 DS 公网地址:
- 客户端拿到 DS 地址后就可以连上 DS,玩家开始对战:
核心诉求:
- 玩家同时在线数量可能暴涨或暴跌,需支持 DS 的动态扩缩容,且扩容速度要快,避免玩家等待过久。
- 每个 DS 都需要独立的公网地址,且 DS 数量可能较大,需要支持大规模动态分配公网地址的能力。
- 被分配的 DS 不能被再次被分配,缩容时也不允许已分配的 DS 被销毁,避免玩家中断。
初步选型
围绕 DS 有类似 GameLift 这样专门对 DS 进行部署与弹性伸缩云服务,但价格相对较高,灵活性较低,为降低成本和提升灵活性,业务团队决定基于云厂商的托管 Kubernetes 和云原生游戏开源项目来部署 DS,并配置根据空闲房间数量和比例的自动扩缩容。
托管 Kubernetes 选型
TKE 是腾讯云上托管 Kubernetes 集群的服务,支持超级节点这种 Serverless 形态,每个 Pod 独占轻量虚拟机,既能避免 DS 间相互干扰(强隔离),也能实现快速扩容 Pod(Pod 独占轻量虚机,无需扩容节点)。
Pod 销毁立即自动停止计费,即能保证隔离性和扩容速度,又能按需使用,降低成本,所以托管 Kubernetes 服务选择了 TKE。
工作负载选型
集群选择了 TKE,接下来就是要确定用什么工作负载类型来部署 DS 了。
Kubernetes 自带了 Deployment 和 StatefulSet 两种工作负载类型,但对游戏 DS 来说,都太简陋了。它们都无法做到标识 Pod 中的房间是否空闲,缩容的时候,非空闲的 Pod 可能会被删除,影响正在对战的玩家。
OpenKruiseGame 作为 OpenKruise 项目中的一部分,也是 CNCF 的孵化项目,提供了专门针对游戏场景的 GameServerSet 工作负载,支持通过 自定义服务质量 的方式自动设置 Pod 关联的 GameServer 的扩展状态,比如增加一个名为 idle
的状态,容器内提供探测脚本,根据探测结果自动设置 GameServer 的 idle 状态,并当 idle 为 true 时,自动将 opsState
设为 WaitToBeDeleted
,以便在缩容时先缩空闲的 Pod,避免影响正在对战的玩家。
但经深入研究,发现存在一些问题:
- OpenKruiseGame 的弹性伸缩基于 KEDA,而 KEDA 最终也是依赖 Kubernetes 的 HPA 进行的弹性伸缩,HPA 在缩容时,会将 Pod 副本数降至 HPA 计算出来的期望副本数,这意味着无法绝对保证所有空闲 Pod 都不被缩容,只能做到优先缩容非空闲 Pod,还需结合合理的配置才能尽可能保证空闲 Pod 不被缩容。
- 这个自定义的 idle 状态始终是异步的,当 Pod/DS 已被分配,但 idle 状态还没来得及更新,恰好又正在缩容时,可能导致已被分配且即将开始对战的游戏房间被销毁。虽然可以通过调大
terminationGracePeriodSeconds
+ 实现优雅终止来尽量降低影响,但着实不太优雅。 - 对于游戏匹配,OpenKruiseGame 并未提供分配 GamesServer 的接口。虽然有提供 OpenMatch 的 director kruise-game-open-match-director,可以实现基于 OpenMatch 的游戏匹配(自动将被分配过的 GameServer 的 opsState 置为 Allocated,避免被再次分配和缩容被删除),但由于 OpenMatch 并不能 满足业务需求,自研了一套游戏匹配逻辑,所以这个 direcotr 也用不上,需要自行实现 OpenKruiseGame 的 GameServer 分配和管理逻辑才能适配。
- 游戏基于虚幻引擎开发,而 OpenKruiseGame 并未提供游戏引擎的 SDK,要实现业务层面的健康检查、扩展状态和信息管理(如房间里的玩家列表以及玩家信息是否加载完成)等,还是使用 SDK 来的更方便。
根据自身业务情况,基于以上原因,不考虑使用 OpenKruiseGame,转而采用了基于 Agones 来部署 DS。
原因如下:
- Agones 提供了各种语言的 SDK 和游戏引擎的插件,当然也包括虚幻引擎,生态很完善,对于开发者来说,让 DS 接入 Kubernetes 很方便。
- Agones 天然支持了 GameServer 是否已分配的状态,且提供 GameServerAllocation API 来分配 GameServer,被分配过的 GameServer 的状态自动标记为
Allocated
,可以避免被再次分配,更重要的是,可以保证缩容时Allocated
的 GameServer 一定不会被销毁。 - Agones 对自研的游戏匹配模块很友好,直接基于 GameServerAllocation API 来分配 GameServer,减少了很多 GameServer 状态管理的开发成本。
分配游戏房间(DS)的方法
Agones 是单 Pod 单房间的模型,社区也有讨论对单 Pod 多房间的支持,参考 issue #1197