Funplus杨超:Funplus如何打造次世代服务器框架?

GameLook报道/6 月 16 日,一年一度的亚马逊云科技游戏开发者大会在线上举行。本次大会以“科技成就伟大游戏”为主题,从构建、运行和增长的全游戏生命周期,向行业展现云科技力。

其中,在大会的“Run 运行”分论坛,Funplus上海服务器中台负责人杨超,带来以《次世代服务器框架》为主题的演讲,介绍了研发体系、插件体系、运维体系组成的三位一体的次世代服务器框架。并主动分享Funplus的相关技术干货,以及针对次世代框架正在做的事情、未来的计划。

以下是演讲实录:

杨超:大家下午好,欢迎来到game tech技术论坛,我今天分享的题目是《次世代的服务框架》。我先简单地做一个自我介绍,我叫杨超,是上海Funplus服务器中台负责的同学,那我们正式进入今天的分享。

在去讲述整个次时代框架具体内容之前,我可能想跟大家先分享一下,我们为什么要去做这样一个次时代框架,那我也会先从一个具体的问题引入。

PPT上的是9个游戏,大家可以想象一下:作为一个服务器从业者,从这些项目立项到最终上线被玩家体验,我们能做什么样的事情?

我认为,服务器部分主要涵盖两大层面。第一是研发层面,首先我们需要为项目提供一个服务器引擎。这样的服务器引擎不仅有底层的网络io通信、RPC的通信框架,甚至是基于分布式的一些编程范式。

那我认为现在的一个服务器引擎,需要确保项目组非常方便、快速、稳定地开发他们的核心玩法。那核心玩法是什么意思呢?对于不同的游戏品类来说,核心玩法的含义是不一样的。对于《Dota2》来说,它的核心玩法可能是一个酣畅淋漓的团战;对于一个MMO、如《魔兽世界》来说,它的核心玩法可能是PvE的团本,也可能是PvP的竞技场;对于一些卡牌游戏来说,它的核心玩法可能是整个卡牌收集和养成的过程。

除了这些核心玩法之外,还有一些外围系统。怎么定义外围系统呢?它并不是一个游戏独有、特有的玩法。比如登录系统、排队系统、公告系统、好友系统、聊天系统、邮件系统等比较基础的部分,都被我统称为“外围系统”。

有了这些外围系统之后,整个项目上线之前,我们还希望能提供比较完善的工具链。比如一些压测工具,可以帮助我们发现服务器在稳定性、或性能上的一些问题。像是利用 profiler工具,能够让我们细致地得知CPU和内存的瓶颈在哪里,然后去优化它,这是研发层面的一些范畴。

当整个项目开发好后,我们需要让它拥有一些上线后的能力。例如允许项目组能够自动化、快速、稳定地发布游戏内容,进行一些开关服的操作。而当游戏上线后,我们可以帮助项目的同学快速发现问题、定位问题,甚至是解决问题,我把这个能力称为全感知能力。

刚才说了那么多内容,大家觉得哪部分对于一个游戏项目来说是最重要的?我们再回到这张图,这9个游戏都深受玩家的喜爱。但我喜欢玩《Dota2》,是因为我能在《Dota2》匹配后的战斗里打字聊天吗?喜欢玩《守望先锋》,是因为《守望先锋》有一个还不错的匹配系统吗。那喜欢玩《魔兽世界》是因为它的服务器还算稳定吗?当然这是一个很重要的因素,但会是决定性因素吗?

其实只要玩过游戏的同学,他一定知道我刚才说的都是在鬼扯。

那最重要的东西是什么?是核心玩法。我们去玩《Dota2》是喜欢玩它酣畅淋漓的团战、有各种各样的决策策略;去玩《魔兽世界》是因为25人本通关之后,拿到新装备的喜悦感;我们玩《守望先锋》是因为它融合了FPS、MOBA的元素,玩起来非常爽快。

因此,“核心玩法”这个东西才是决定项目成败的关键因素,那我们的目标是什么?

首先,我们希望项目能够花费足够多精力去聚焦、打磨他们的核心玩法,把它做得足够的出彩。其次,也希望我们能提供一整套的体系,去帮助项目组高效、快速地研发他们的产品,缩短他们的铺量期,让产品尽快上线。而在整个产品上线之后,我们写希望这个游戏能够稳定地进行线上运营,防止一些事故导致整个产品或者游戏生命周期的衰退。

最后回到这个主题:什么是次时代服务器框架?

我认为,次世代服务器框架有三大体系。第一大体系是研发体系,项目立项开始后,项目组的程序能够使用我们的研发体系,包括服务器、引擎、工具链,快速地开发出他们的核心game play,并得到一个验证。

有了核心game play之后,他们可以使用我们的插件体系、及外围的微服务,还有各种各样的插件库,即插即用地把这个东西融合成一个真正的游戏产品。

再有了这样的游戏产品后,就能使用我们提供的运维体系,即自动化部署、全感知能力,为玩家提供稳定的线上服务。我认为,次时代服务器框架就是这样三位一体的一个框架体系。

介绍完次世代服务器框架的概念后,第二章节我想跟大家分享一下目前我们的部分成果。但这毕竟是一个技术分享,它不是工作汇报,我还是想给大家带来一些干货。

我们先从一个场景开始,这是一个典型的游戏分布式场景。首先,我们有一个game,就是游戏的逻辑服务器,玩家可以在上面进行各种各样的游戏逻辑开发。然后我们有一个Mongo,它是玩家数据落地的一个数据库。在市面上绝大多数的游戏场景里,我们都会使用定时存盘的策略。即每隔十分钟之后,会把整个game上产生的玩家数据通过定时的方式存储到Mongo中。

那这样做可能会带来一个问题,大家有没有想过?比如在game上,我玩了十五分钟之后,等级从10级提升到了100级。但这时当我发生定时存盘的时候,如果产生了一些异常,不管是game或是Mongo,还是game和Mongo之间的网络链路,交换机或路由器再也回不来了,那我们是不是就要产生一些线上的回档?

大家都知道,游戏在线上回档是非常严重的一个事故。玩家花了几千块钱打造了一个装备,以极低的概率给爆出来了,你现在告诉我回档,那我是不是就要流失掉了?

怎么避免这些恶性事故呢?我们开发了一个Journal的系统,它是一个毫秒级别的数据容灾方案。当然,它的本质和所有的数据不一样,它是一个binlog的系统,可以自动化地进行存盘数据的备份和恢复。并且我认为最重要的一点,它是业务代码0侵入的、是0心智负担。

那我们怎么做到这点呢?因为它是一个binlog的系统,所以肯定要先讲下我们的日志写入模式。

在传统的数据库服务binlog写入模式里,一共有三种。第一种叫Full-Sync,指的是把我们app内的数据先通过内存的方式copy到一个buffer中,在这个buffer中通过syscall的方式提交给操作系统,拷贝到操作系统的内核态的buffer里去,然后再通过io的方式最终落地到磁盘。到数据真正落地到磁盘之前,整个流程不会继续开始,大家可以看到这是一个非常长的流程。

下面的流程叫Write-only,会稍微简单一点,它只需要把数据从app侧提交到操作系统的内核态就可以了,它只有一个系统备用的开销。

No-Sync是最简单的,它只需要把app的数据提交到我自己app本身的memory buffer里就可以了。从上到下,应用程序付出的代价是越来越低的,对于我们来说,只需要No-Sync就足够了,为什么呢?

做情况分析之前,我想要给大家科普一个前提。在我们的体系下面,数据是以玩家为单位去存储的。当我每次发起一次调用的时候,玩家都有一个version的概念,它是单调递增的。

有了这样一个概念后,假如以日志落盘的成功失败、Mongo落盘的成功失败排列组合之后,一共有四种情况:

假如日志落盘成功、Mongo落盘成功,那么version就是匹配的,我们并不需要做一些恢复;如果说日志落盘成功、Mongo落盘失败,Mongo的version比较小,我们可以用日志的数据去恢复;又假如日志落盘失败、Mongo落盘成功了,那Mongo的version较大,更没有恢复的必要,因为Mongo本来就是我们存放玩家数据的最终目的地。

但第四种情况需要细细探讨了,比如说日志落盘失败了,Mongo落盘也失败了,这种情况是一定是没法恢复的。那我请大家思考一个问题,日志落盘的瞬间,这个时候发生了crush,那么数据没有落盘的时间间隔有多少?

我们实测过,最慢是毫秒级,最快是微秒级,因此玩家只有一个毫秒级别的回档。但其实毫秒级别的回档玩家是感知不到的,为什么?因为玩家的客户端到服务器的一个链入,一般来说是30毫秒到50毫秒的延迟,慢一点的可能是百毫秒的延迟。

可能当他点购买装备那一刹那,这个时候他并没有收到回调,其实此刻服务器已经宕机了,玩家并没有获得到什么东西。他并不会因为获得到了一个绝世装备玩了15分钟导致的回档而产生愤怒感,他只知道在那一刹那服务器crush了而已。因此,我们认为毫秒级别的一个回档的数据容灾,已经能够满足99.9%游戏的一个容灾场景。

下面说一下接口和调用时机。我们一共有五个接口,分别是base和baseEnd,这是一对接口。玩家上线后会把当前的version、snapshot写入到整个日志系统中。这里的snapshot是我们用来数据恢复的一个基本的数据集,它的量非常的小。baseEnd是玩家下线时,收到下线存盘的服务器确认消息后,会把数据库已经确认的version写到日志系统中。

save和saveAck又是一对儿。当玩家发生定时存盘时,会把当前的version写入日志系统中。而saveAck就是我收到数据库的确认回调之后,会把确认的version写到那个日志系统中。

operation比较好理解,即玩家任何数据发生变更的时候,会直接把变化的数据写入到日志系统中。

我们看一下数据结构的设计。刚才介绍过,所有的数据是以玩家的形式存储的,每一个玩家都会有一个叫DataLine的结构。而右图是一个DataLine,可以看见,每个DataLine下面会有很多的BastLine。刚才介绍了整个接口的调用时机,BaseLine指的是一个玩家从上线到下线整个过程中所产生的所有数据的集合体。VersionLine的概念指的是,两次定时存盘之间的所有操作和数据的变更。

那我们以右图为例,详细地带大家过一下这三种数据结构。首先玩家a有了,我们会为他建立一个玩家a的DataLine。当玩家a上线,他从数据库中读到他的version是100,这个时候呢我们会产生一个BaseLine。

过了一段时间,它有三次op的操作,可能是我等级的提升、是我背包的变更,也可能是我货币的增减。过了一段时间,定时存盘发生了,然后我们调用了save,这个时候会有一个save101的一个version出来。后面会紧跟3个op,它们是什么都可以。

然后又过了十五分钟,又发生了一次定时存盘,这个时候生成了一个save102。大家注意下,这个绿色的ack101表示的是,我收到数据库确认101这个版本的version已经写入成功了。这个时候又有一个op,然后又有一个ack102,表示我102 version的数据已经写入成功了。

最后我收到了end,指的是我这个base100、version102下线的消息,并且确认已经成功。过了一会儿玩家又上线了,他能说出version是102,这个时候会产生一个base102,然后又发生了三次op的变更。大家可以继续往下去不断地去写入。

大家非常容易能想到一件事,随着整个服务器的运行,玩家不断上下线、各种各样的数据变更之后,整个文件会越来越大。那我们当然不能允许这种情况发生,肯定要想着能不能去做一些数据剔除,把不需要的数据给剔除掉。

大家再回想一下,我们整个Journal系统日志的数据是为了干什么?是为了恢复数据库的数据,对不对?换而言之,数据库已经确认的数据,在我们的真正系统中是不是就不需要了呢?

从这个角度出发,大家继续看刚才这张图。当我收到ack101的时候,是不是我第一行的所有数据都是可以不需要的。当我收到ack102之后,是不是我第二行的数据就不需要了?因为当我调save102的时候,这个102 vision所有的全量数据或增量数据,已经和数据库达成一致了,所以我之前所有的数据是不是不需要了。当我收到baseEnd的时候,是不是我整个base都不需要了?因为我整个base的数据已经放到数据库中了。

顺着这个思路来看,我们就有了整个冗余数据剔除的策略。

我希望再跟大家强调一件事,我们整个文件因为是binlog,所有的数据是以一个流式的方式存在文件中。我们没办法对一个文件既产生数据、又不断地写入,又把它读取进来,然后修改、再做剔除。大家都知道,在一个流式的文件里面,这样的操作是行不通的。

因此我们一定会设计一个文件的处理,我认为一共有三个步骤。第一个步骤是产生,怎么产生这个文件;第二个步骤是怎么拆分这个文件;第三部分是当它拆分好了之后,我们怎么合并这个文件,在合并的时候把冗余的数据给剔除。

大家可以看一下这张图,当我们的app一开始运行的时候,会产生一个cur.1的文件。我们的Journal系统会往这个文件里不断地写数据,当这个数据量到达一定的阈值。我们这里是10M之后,会把它rename成一个pre.1的一个文件,然后继续往一个叫cur.2的文件里面去写。

同理,过了一段时间之后,会产生一个pre.2的文件,还有cur.3的一个文件。然后呢我们会运行一个程序,他发现了pre.1和pre.2,这时他会把两个文件合并,做一些数据上的剔除,合并之后就是merge.2的文件,最终就形成了一个merge.2、以及cur.3的文件。

大家需要注意的是,merge.2的文件已经会把很多冗余数据给剔除了。也就是说,我们所有的Journal系统文件的数据量,就是这两次定时存盘之间所有玩家数据的变化量,这个量其实非常小,我们已经做过一些性能的评估。大家可以看到这个点一点二点三,其实这个序号是有非常重要的意义。这个序号是我们实现很多原子操作以及生产程序,拆分合并程序,crush之后一些可重入的重要的指标,这里其实有非常多的细节,但因为时间原因在这里不再赘述了。

我们继续看一下自动化。为什么要做自动化?原因其实太简单了,没有人愿意在半夜被叫起来,对吧。我也说过,我们的整个框架的体系是延运一体化,是一个三位一体的体系。因此我们也非常方便地在整个开关服流程的工作流里,把我们Journal的思路和思想给嵌入进去。

当我们开服的时候,我们会先检查game这个进程所在云盘上是否有争论文件的残留。如果没有,我们就正常的启动就可以了。那如果有,我们就执行一个提前写好的恢复程序,它会以一个秒级别的延迟,把这个所有的数据给写好。

一般来说,这个过程是一定不会有问题的。假如发生了问题,我们也会报警,并且把现场保留下来,方便日后项目组和我们一起去排查问题所在。

整个Journal系统讲完了,下面是一个快速浏览部分。因为我们做了非常多的系统,没办法像Journal一样非常详细地介绍给大家,可能下面的部分会有点快。当然如果大家有任何的疑问,可以通过我的工作邮箱来跟我交流。

首先可以看下我们的架构部署图,一共有三个大块。左边是我们的引擎部分,它也是一个分布式的服务器系统。里边会有非常多的进程,这些进程都是完全可以动态扩缩容的。并且通过一些Journal有状态的服务,可以通过Journal的方式形成一些高可用的数据恢复。

右边这部分是我们的微服务体系,我们的微服务现在就和互联网领域如何使用微服务是一样的,非常方便的去做动态库缩容。然后下面的部分是我们使用的中间件,包括Mongo、etcd。

这个是我们做的一个数据同步系统,这个数据同步系统其实和Journal是联动的。其实大家更常听到的一句话可能是属性同步系统,那为什么我们叫数据同步系统?因为我们除了Server和Client之间的属性同步外,我们也做了Mongo和Server之间的数据同步。

我们可以看下定义,@property lv int 0。OwnClient指的是当我发生变化了之后,我只需要同步给我自己的客户端。PER是persistent,就是我需要把它同步到数据库。后面是玩家等级,是它的注释。

为什么刚才我说Journal系统是无侵入的呢?只要你定义了这样的一句话,当你调用self.lv=100的时候,整个Journal系统会自动地把你这个变化量放到那个日志里面,不需要你在业务层代码多调用任何的一句话。同理,我们这个lv也会同步给客户端,不需要你在业务层写任何其他代码,并且整个过程是支持增量同步的。

只要做过游戏的服务器开发,你会知道,游戏的服务器程序员会花非常多的精力和时间,去写属性同步或数据同步的各种各样的代码。但这部分代码已经被我们完全的封装在了框架层,所以开发效率会得到非常高的一个提升。

自动化部署是我们与其他团队合作开发的整个自动化部署的体系,它可以混合云平台去管理,我们有非常多的资源和服务是部署在AWS上面的。

这里核心的一个功能是资源树,它可以根据我整个项目定制化我们想要的框架和服务。然后项目可以通过一些工作流的方式去做内容的发布、开关服以及线上热更等各种各样的功能,并且对于这些所有资源的操作都是所见即所得的。

然后更多,最左边是一个监控系统。我们的整个监控系统有分布式的日志,还有open tracing、普罗米修斯,还有托斯曼火焰图、profile等各种各样的监控指标。因为它有非常非常多的来源,但其实我们希望有统一的观测界面去观测它。面板上会对这些数据来源的数据做一些基本的分析,还有报警。并且提供了一些软链接,是可以详细跳转到对应数据的详细信息。

中间是我们整个压测框架,我们压测框架呢除了boot manager、还有boot之外的一些东西,项目组可以使用行为数据编辑他们整个机器人的行为之外,我们还联动了上面提到的自动化部署系统,包括我们的boot的物理机实力的一个申请,需要被压测集群的机器和资源的申请,都是可以自动化部署系统联动的。

在整个压测过程中,发现、收集到各种各样的数据,是可以通过左边的这幅图里的面板去做展示的。整个压测执行完之后,会生成一个自动化的报告,供项目组去使用观看,让他们知道整个压测过程中产生的各种各样的问题。

右上角的图是这样子的,大家都知道,其实在互联网领域有很多服务都是无状态的,而无状态的服务做动态扩缩容、或者做容灾,都是非常简单的。但是有状态的服务是整个游戏场景面临的非常多情况。那通过Journal系统我们可以做到,有状态服务的一个容灾。那怎么做到有状态服务的动态扩缩容呢?我们有一套概念,在框架层面实现了一个有状态服务的动态扩缩容,并且这样一个东西是完全封装在框架层,对业务是无感的。

右下角的图是我们的微服务实例,目前我们大概有十六七个微服务,项目组可以使用这些微服务实例,极大地加速他们整个铺量期的游戏进程,快速实现整个项目的上线。因为是经过多个项目打磨检验,我们所有微服务实例的稳定性和性能都是有保障的。

最后再介绍一下整个公司内项目的使用情况。目前公司内使用我们框架的产品种类非常多,涵盖了卡牌、MMO、SLG、TPS、战棋等多款不同种类的游戏,项目的泛用性是非常强的。其次,整个项目团队的核心来自国内TOP10等多个厂商,在经过两周服务器通关教程的培训之后,可以非常快速上手,因此整个团队的泛用性也是非常强。

V1.0是我们已经完成的一些情况,那V2.0其实是我们正在做的一些事情,V3.0是我们未来可能想做的事情。最后一点时间,我希望跟大家去分享分享。

先看V2.0,我认为目前我们正在做的事情里面有三件事情是非常重要的。第一件事情,是基于k8s的可跨云部署的服务化、插件化的次世代框架。当然里边比较长,定义比较多。什么是基于k8s呢?就是说我们现在整个引擎部分还在虚拟机上,我们希望像微服务那边一样,能做到一个受容器的模式,更方便我们去做动态扩缩容的调度。

第二点,我们希望把k8s当成我们整个框架的一个微型操作系统,我们只需要面向k8s去编程。至于云上各个组件的差异,我们希望k8s帮我们去解决掉。然后服务化指的是,我们不仅希望使用微服务体系,是一个靠service的概念。那在引擎内部,具体举个例子,比如说你有个 boss center,有一个叫space center去管理你的分线场景的,我们都希望把它改成一个boss service或者叫space service。以和微服务一样的调用体验,去靠service的方式把这些给管理起来,自动地做他们的动态扩思容还有容灾,就基于k8s去做。

最后一点是插件化,指的是如果有项目有非常定制化的需求,我们希望以一个插件的形式提供,去方便各个项目组做各个层次的拓展。

第二件事是混沌工程。这其实在互联网领域并不是一个陌生的概念,但在游戏领域其实还是比较陌生的。那我们能做什么事情呢?首先就是在上面的一个压测框架里面提供一个混沌注入的的能力,其次是在整个框架层面提供一个稳态检测的能力。当有了这些能力之后,项目组可以去编排他们各种各样的故障,我们可以自动化地生成,他们各个系统在一个混沌情况下的各种各样故障。

最终要干什么呢?在项目上线之前,我们希望跟项目去展开一个红蓝对抗的演练,帮助项目组提前发现上线之后产生的各种不稳定的问题,来降低上线后可能遭遇的问题。比如登录出问题、充值出问题、组队匹配出问题,带来各种各样对项目周期或者项目生命产生恶性事件的影响。

第三件事,是多层次的拓扑监控,怎么理解呢?因为刚才大家已经知道了,我们的监控数据来源非常的多,整个框架非常复杂,怎么把这些框架通过一个拓扑的形式有机地结合起来,然后能够分层次。我们希望能在一张图里边有一个全局的视角,能够看到几千个甚至是几万个进程的状态,通过层层递进的方式逐渐发现一些细节上的问题,这块也是我们正在攻克的一个难题。

3.0谈谈未来。未来的话像无无缝大世界,我们也是肯定要去支持、去做的。市面上分为两派,一个是多线程派,一个是多进程派。因为我们是一个分布式的架构,毫无疑问我们一定会选择像BigWorlds那样的多进程派。

虽然BigWorlds有一套比较完美的无缝大世界的理念,但它没有解决一个问题:怎么让项目组的同学尽可能地少写异步的战斗逻辑。这块也是我们要去攻克的一个难题,我们也希望能够解决这样的一个挑战。

第二点是一个商业级别的服务引擎。像现在Unity和UE都有自己的delete server,但是它的整个功能性是非常弱的,并不能够让市场上的一些腰部团队去使用,开发出一个稳定性足够好的服务器。

我们希望能够提供一个足够好的一个插件,基于Unity或者UE,能够让市面上的一些腰部以下的团队使用,非常方便地开发出他们理想中的网络游戏。

好,那我的分享就到这里结束了,感谢大家。

如若转载,请注明出处:http://www.gamelook.com.cn/2022/06/487463

关注微信