11月17日,由Epic Games打造的年度虚幻引擎开发技术分享活动Unreal Open Day 2022在上海召开。MetaApp联合创始人兼CTO段进伟在线上发表了主题演讲,分享了MetaApp是如何构建一个全年龄虚拟世界背后所面临的的技术挑战。
MetaApp团队当前正在做的一款全年龄段虚拟世界 / 3D互动内容创作及体验社区。区别于更看重视觉表现的渲染效果的传统引擎,这个代号叫MetaWorld的项目更强调所见即所得。比如在传统引擎里你想在游戏中实现车辆的功能,并且能与其他多名玩家互动的话,你不仅需要写代码来实现车辆的各种交互逻辑和物理特性,还需要架设服务器,并攥写网络同步等代码逻辑。
而在我们MetaWorld的虚拟世界里,你可以直接从资源库中拖一辆车放进去,这辆车就能直接按物理规律动起来,并且一键发布后就能让很多玩家在同一个虚拟世界中一起和这辆车进行互动;真正释放了3D互动内容的创造力。
MetaApp联合创始人段进伟UOD线上分享
为了完成MetaWorld项目的这个宏伟目标,需要克服和解决多个方面的技术难题和产品体验问题。其中,如何能有效地实现线上大量不同类型的游戏的稳定运行,并实现高效和可靠的运维,就是项目面临的主要挑战之一,不管从技术复杂度还是从运维复杂度来说,对架构和运维的要求都比传统游戏高了不少。在这个大背景下,我们团队是通过不断迭代技术,解决不断遇到的技术挑战实现的。
MetaWorld项目的定位和性质让我们遇到以下难题:
- 同时管理大量不同类型的游戏,不同游戏的在线玩家数量差别很大,所以需要能动态增减各个游戏的游戏服务(DS)的数量;为了保证每个玩家的体验,所以需要保证服务器在请求拉起时能快速启动,在玩家推出游戏释放时快速回收资源。
- 为了保证服务器资源有效利用,需要服务器集群具备动态扩缩容能力;另一方面,因为DS自身特性对内存需求比较高,也要求我们能让多个DS能复用内存;多个方面来提升服务集群的资源利用率。
- 在高可用性方面,需要确保任何中小团队以至个人创作者所创作的游戏的各个DS副本之间都能做好隔离,保障数据一致性和运行稳定性,确保提供更好的用户体验。
为了解决运维繁琐的问题和支持资源动态扩缩容的需求,我们选择了目前互联网主流的k8s的集群部署方式;但k8s集群部署方案主要是面向互联网服务场景设计和实现的,主要针对的是无状态的服务,而游戏服务器一般都是有状态的服务,所以针对游戏服务器的特点,需要我们做一系列的适配工作以满足实际业务需求:
在讨论如何把DS 服务部署到这个 K8S 集群中时,我们讨论过多种方案,如下图所示。首先有一个 match server,这是一个房间匹配服务,它的功能是把同时发起匹配的玩家撮合到一个房间中。
下图方案一是在match server后,把agent和不同游戏 DS 都放到一个容器(container)中,agent是管理 DS 服务的管理器。这个架构跟传统的ECS集群的部署方案很相似。但是这跟 K8S 的这个理念是相悖的,它一般来是在一个容器里面只包含一个服务。
下图方案二就跟 K8S 的一个最佳实践是比较匹配的,每个容器中只包含一个服务进程,但是这样的话不同的 DS 之间就没有办法做内存共享,因为不同容器之间是不能共享内存的。
下图方案三是基于方案一和方案二的改进,这个方案中把同一个游戏的多个DS进程放到了一个容器中,包括agent,是一个父进程 DS 和它下面挂载多个 DS 子进程的结构,这个结构为了做内存共享,后面会提到。
我们最终采用的方案是方案三,这个方案跟方案一有同样的问题,即跟K8S 的理念是相悖的,但更符合我们的实际业务需求,为了把这个方案落地,我们做了一系列的适应性改造工作。另外,下图方案四跟方案三差别不大,只是把 agent 单独放到了另一个容器中。
四种部署方案示意图
对于资源的调度,通过k8s自身的能力就能很好地解决,启动容器可以达到秒级,满足服务器快速启动,释放时快速回收资源的需求。对于内存的优化,在上述方案三的前提下,通过DS父进程fork出多个DS子进程实现同一个游戏的多个DS共享内存从而很大程度减少了内存的使用。首次拉起容器会拉起一个游戏的DS进程至准备状态,然后挂起作为父进程,当需要时通过父进程fork出DS子进程给新创建的游戏房间使用。
但这样会出现隔离性的问题,为了避免出现DS进程之间抢占资源和数据不一致的问题,目前MetaWorld项目通过使用Cgroup技术,根据每个游戏设定的每个房间的最大容纳人数,对游戏的DS进程做资源上限限制,避免影响其他游戏服务器进程;也支持开发者编写TypeScript脚本实现游戏逻辑,控制客户端和服务端的运行边界,很好地解决了隔离性问题。
因为k8s自身的限制,pod创建时资源需求量需要预先确认配置,指定request和limit,不能根据实际业务需求动态增减,否则会被QOS判定为BestEffort类型,pod会不停扩大自己资源的使用,直至被kubelet驱逐,最终导致容器中的所有DS突然死亡,让大量玩家掉线,这是严重的运维事故,是要坚决杜绝的。
但如果不能动态的对pod资源进行动态增减,就会导致我们在线上实际运维中的资源利用率比较低,运维效率很低,为了解决这个问题,我们需要对k8s进行定制化的改造和优化。
MetaWorld项目基于K8s的扩展点,制定实施了Nuwa计划进行自定义化改造,自研一套适用于自身DS使用场景且支持动态修改K8s Pod资源的系统,从而提升K8s集群的资源使用率,实现更优的部署和效能。
Nuwa计划的架构如下图所示,scheduler和API server 是属于k8s集群里的模块,nuwalet 是我们仿照kubelet去做的一套管理模块,是Nuwa计划的核心,通过它来实现pod资源的动态修改。整个流程是这样,当房间管理服务发现需要去动态修改pod的资源限制时,它向API server发起请求,然后经过一系列流程后把请求转发到nuwalet的资源管理模块中,通过nuwalet的资源管理模块就能把pod的Cgroup资源限制进行动态修改;但这块改完还没结束,它还需要把修改的结果通过node resource metric 这样的动态资源视图形式上报给 scheduler,就是k8s的调度器。为什么还要上报呢?
因为k8s的pod的创建和销毁等等都是通过这个调度器来完成的,如果修改了pod的资源限制却没通知它的话,它的调度管理就会失效,整个k8s集群就会陷入混乱。另一方面,调度器需要能根据我通知他的这个node resource metric 的动态资源视图进行后续的调度才行,因为k8s默认的scheduler是使用的静态资源视图进行调度,所以我们扩展了它的调度器,实现了一个scheduler插件,接管它的调度算法和打分机制,这样它就能通过动态资源视图去进行打分和调度了。
通过上述这一套的研发改造工作,这个系统和流程就完整地实现了。
Nuwa计划架构图
通过上述多项技术的研发和落地,MetaWorld项目的游戏服务器进程平均启动时间降低了80%以上,平均占用内存降低了70%以上,整体资源利用率提升了200%以上,优化效果非常明显。
未来,团队还会持续优化迭代Nuwa计划,更加灵活地控制Pod的资源用量,进一步提升了整个服务集群的资源使用率,持续降本增效。目前,团队的这一模式还处于初步实践阶段,欢迎大家继续关注我们项目的进展。