《使命召唤手游》怎么避免上线即炸服?

Jerry 腾讯互娱工程师

炸服,是近两年频繁发生、游戏厂商闻之色变的运营事故。

 

虽然从某种意义上来说,炸服意味着游戏很受欢迎,但由于炸服会致使玩家无法登录、频繁卡顿、反复重连,甚至出现数据丢失和回档,导致游戏口碑暴跌。一旦处理不好,可能会使一个原本的爆款丧失第一波爆发的机会。

 

从服务器架构设计的角度,如何避免上线即「炸服」的事故发生?

 

8月29日,在腾讯游戏学院主办的GWB腾讯游戏品鉴会上,腾讯互娱天美工作室群J3工作室《使命召唤手游》服务器主程Jerry分享了他对于服务器架构设计的心得。

 

其分享的要点如下:

 

• 从服务器设计角度看,出现炸服往往和架构设计不到位、平行扩容或负载均衡能力欠缺、单点容灾问题考虑不完善等问题有关。

• 服务器部署方式的选择要从实际业务要求出发,建议优先考虑全区全服。

• 设计分区分服模型时,也可以吸取全区全服的设计经验,避免原本存在的一些问题。

• 进程模型设计中,建议根据业务情况,将大功能解耦、拆分成多个进程,进程拆分原则:「大系统小做」

• 选择后台路由策略的通用原则:一般情况下,无状态服务使用随机分配的方式,有状态服务使用取模或一次性哈希的方式,单点服务使用主备或备份方式。

• 和故障隔离、在线更新/异常处理、削峰、第三方系统设计相关的架构设计注意事项

 

看完这篇技术干货,说不定能帮助你的产品避开炸服雷区。

 

图片

[ 《使命召唤手游》服务器主程序Jerry ]

 

(以下为分享内容,有调整和删节。)

 

大家好,我是Jerry,来自天美J3工作室,现在担任《使命召唤手游》服务器主程。很高兴有机会和大家聊一聊有关服务器架构设计的话题。

 

今天这个话题,想必大家都会有所关注:游戏上线后出现炸服,可能是因为哪些原因?在游戏设计之初,我们需要注意哪些问题,才能避免或减少炸服发生?

 

那什么是炸服?我相信大家都听说或经历过这样的情况,游戏刚开服的时候,由于玩家特别热情,可能出现登录不上、频繁卡顿,甚至反复重连的情况,严重时可能会出现数据丢失、回档,而且这些问题往往不能快速得到解决。

 

从服务器设计的角度看,这些问题一般和下面这些因素有关:服务器架构设计不到位、平行扩容和负载均衡有缺陷、单点容灾考虑得不是特别完善。

 

图片

 

---

 

▌一、一个服务器架构设计不当的案例

 

我们先从一个案例讲起。这是某款游戏的服务器架构图,我们做了适当的简化。

 

图片

 

这款游戏采用了分区分服的部署方式,架构图左边其实是一个区,主要包含登录授权服务,以及连接管理和游戏逻辑。同时,它有一个所有分区公用的对局管理模块,而它的游戏逻辑放在了战斗逻辑进程中,也是各个分区共同使用。

 

在功能层面,这款游戏的连接管理主要负责玩家的连接登录,游戏逻辑里包含了数据管理,角色养成等业务逻辑,而对局管理包含组队、匹配、开局逻辑等等,战斗逻辑主要负责局内战斗。

 

这款游戏在部署上有一个很有意思的点,开发团队认为这款游戏单区承载能力足够,因此打算一个大区用一台高性能物理机、主游戏逻辑用一个进程就搞定了。

 

这样的架构设计存在什么问题?首先业务耦合度特别高。他们在架构图里虽然把连接管理和游戏逻辑分开了,但实际上这两块在同一个进程中。连接管理和游戏逻辑放在一起可能会导致什么问题?

 

对于这款游戏来说,游戏逻辑里包含了很多游戏,所以业务逻辑会特别重,出现Bug的概率特别高,如果在线上运营,游戏逻辑可能需要频繁更新,如果将其和连接管理放在一起,会导致更新出现各种掣肘,一旦处理不当,就有可能导致玩家掉线。

图片

 

其次,他们的对局管理模块包含了很多功能,比如匹配、开局、负载管理等等,由于全局只有一个对局逻辑,所以可能会存在单点容灾的问题。而对局管理和游戏逻辑放在一起,平行扩容方面也会存在问题。同时,由于他们把负载管理和匹配之类的功能放在同一个进程里面,在负载管理上可能也存在一些缺陷。

 

拓展一点说,在分区分服部署模式中经常会碰到另外一个问题:由于开发者认为游戏是分区分服的,每个区的玩家数量相对会比较少,所以他们只部署一个进程来搞定,从单区的角度来看形成了事实上的单点。

 

图片

 

而这款游戏全服共享的「对局管理」只有一个进程,我们认为如果单进程能支撑所有的请求,那么至少应该设计成主备部署的架构。但在游戏上线前,你拿不准用户量会达到什么样的程度。万一你的游戏成了爆款,这种单纯的主备方式有可能满足不了实际的需求。因此,我们推荐将其设计成可以平行扩容的方案。

 

说到平行扩容,刚才那种事实单点是无法支持平行扩容的。对于这个项目,他们负责服务器设计的同学说,单区承载目标是5K-6K,但随着开发推进,游戏业务逻辑越来越复杂,通过压力测试发现,在这种逻辑架构下,游戏逻辑进程承载的极限只有3K。由于这种架构不支持平行扩容,游戏上线时很有可能发生炸服。

 

图片

 

这款游戏的对局管理为什么不支持平行扩容?因为对局管理进程中包含的功能太多了,这些功能在同样的业务逻辑下,对性能的要求可能不一样。如果想要对其中某个功能进行扩容,在这种把所有功能都集成在一起的设计中显然做不到。

 

除此之外,这款游戏还存在负载均衡的问题,这个和架构设计可能没有太大关系,我们发现,这款游戏的对局管理存在算法缺陷。因为他们没有考虑到一台机器的瞬间负载能力,在新加入「战斗模块」机器时,可能发生雪崩。

 

图片

 

因此我们设计负载均衡调度算法时,除了定时上报负载外,一定要设计异常上报机制,负载均衡算法必须能及时处理异常情况,并且实例——对于这款游戏就是战斗模块——必须要有主动熔断机制,避免出现负载本身已经承受不住时,负载调度模块还不断发送开局请求,当然这需要负载调度模块和实例之间互相配合。

 

同时,我们还需要设计提前预判机制。举个例子,目前我只剩下10%的负载,下一次上报时间可能在3秒以后,如果这3秒内调度模块发送了过多请求,很有可能会导致雪崩。

 

总体看下来,如果这款游戏以这样的服务器架构上线,万一玩家特别热情,刚才提到的任何一个问题爆发,都有可能引发「炸服」。

 

在这个案例中,我们指出了很多问题,也相应提供了解决方案,但项目组反馈说,可能没有办法做这样的修改,因为他们在设计之初就把服务器架构限死了,并且服务器底层架构缺乏相关的支持。

 

那我们在架构设计之初,能不能做一些预见性的设计,规避这样的问题?

 

---

 

▌二、服务器架构设计和技术选型怎么做?

 

接下来我想谈谈游戏服务器架构设计和技术选型要怎么做。

 

-如何选择区服模型?

 

先看区服模型。一般来说,游戏部署方式分两种,分区分服和全区全服。下图左侧是分区分服的简化模型,右边是全区全服的简化模型。

 

图片

 

分区分服模型一般在前端会有一个导航模块,用于区服选择;区与区之间隔离,它们会有各自的DB。区与区之间如果需要通信的话,往往会使用跨服模块。对于一些需要跨服的功能,比如包含所有区的排行榜,可能还会为此增加一个公共DB。这些是分区分服的主要特征。

 

而全区全服没有分区的概念,但它会有一个中心通信模块,并且整个大区在逻辑上只有一个DB。

 

这两种部署方式对比下来有一个很特别的现象。从业务逻辑来看,分区分服架构里每个区只画了一个「World」,而全区全服画出了很多业务进程。

 

也就是说,由于分区分服架构下,大家认为每个区玩家数量比较少,也有可能受限于开发时间、开发资源、技术储备等原因,没有把业务逻辑拆分得足够细,因此一般只有一个「World」,或者把游戏逻辑放在少数几个进程当中。

 

其实这两个模型并没有明显的高下之分,在实际选择的时候,主要是从业务需求角度出发来选择。如果业务本身存在合服的需求,那采用分区分服的模型自然更加合适。

 

图片

 

不过我们看了很多采用分区分服模型的游戏之后,有一些建议:

 

作为分区分服,其实也可以吸取全区全服的设计经验,避免分区分服原本存在的一些问题。

 

比如分区分服中单机容量可能受限,我们建议要适当考虑平行扩容相关的设计。像刚才说的单个「world」做平行扩容比较困难,是不是可以把核心逻辑拆分出来?

 

很多时候,我们会看到分区分服采用真实的物理分区,DB也做了分隔,在合服迁移的时候,成本就相当高,那是不是能采用虚拟分区的方式来处理?在虚拟分区下,我们可以采用组合key,即用玩家的分区ID和Player ID组合,区分他们所属的区服,这样合服时不需要做物理上的数据迁移。

 

另外,我们需要完善自动化工具,因为分区分服必然会遇到合服的问题,合服可能需要处理数据上的、运营策略上的各种各样的问题,而在设计方案之初完善自动化工具,能够避免早期的架构设计或数据结构的设计,没办法很好地支持自动化的问题。

 

从游戏服务器设计角度来说,我们建议,如果有可能的话,尽量采取全区全服的设计方式,虽然这对于整个架构设计要求更高,但它有以下几点好处:

 

首先,后续运营运维成本会比较低,因为它物理上只有一个环境,我们投入的运维人力更可控。

 

其次,全区全服可以共享机器。我们知道,游戏后台会有很多进程,但每个进程的资源消耗其实不一样。而我们虽然会有很多区,但做运营活动时,每个区参与的用户数量可能会有所区别。如果采用全区全服的方式,我们就可以共享机器资源。

 

当然,这也会导致对资源调度、动态扩缩容和容灾备份的要求较高。

 

-如何设计全区全服的进程模型?

 

既然我们建议采用全区全服,那么在全区全服下,进程模型有什么特点?首先是Kiss原则,说白了就是让每个模块负责的功能尽可能单一,让各种模块互相配合,完成一个复杂的系统功能。

 

因此,在进程模型设计中,我们建议大家根据业务情况,将大功能解耦、拆分成多个进程。在拆分成多进程之后,需要设计一个统一的消息通信。

 

图片

 

拆分成多进程有哪些好处?我们先看看如果不拆进程,会出现什么问题。

 

图片

 

首先,不拆进程不利于协同开发,因为系统耦合度太高。

 

现在一款游戏大作,开发团队动辄几十人,多则上百人,其中负责后台开发的团队也有大几十人,这么一个系统,大家都在同一个进程里面写,责任不明晰,出了问题很难判断问题在哪。

 

其次,后台很多系统、子系统之间性能有差异。比如登录模块在10万PCU下所需的进程数、机器数,可能跟负责对局的模块不一样,聊天模块、活动子系统之间也存在性能差异。如果不拆分进程,就不能做到按需部署,只能很暴力地把进程按某种倍数扩容,这必然会浪费机器资源,甚至有可能增加机器、部署更多进程,都解决不了性能瓶颈。

 

第三,现在很多游戏都会寻求出海,出海必然会存在地域差异,如果想要提供好的游戏体验,就必须考虑就近部署的问题,如果进程不拆开的话,就近部署其实比较难做。

 

第四,在容灾上会受到一定限制。因为我们没有把关键子系统拆分出来,而人力成本、机器资源、项目资源都有限,只有将这些有限的资源投放到关键子系统上,才能让我们获得更大的收益。

 

那么要怎么拆分进程呢?在腾讯内部,有个原则叫「大系统小做」。一个大系统具体要怎么拆分?标准有很多,我们通常会从功能需求、处理流程和通用组件这几块来拆分,比如功能可以拆分成好友、公会、邮件、聊天这几个部分。

 

图片

 

-如何解决拆分进程可能带来的问题?

 

同时要注意,拆分可能会带来一些问题。如果我不拆分,就只有几个进程,拆分完以后,可能部署上又使用了全区全服,系统就会变得非常庞大,用到的机器也会非常多,那么监控要怎么做?

 

我们原来在一个进程内,通过函数调用就解决的通讯问题,变成多进程之后,要用什么方式进行通信,协议要怎么定?

 

在部署方面,原来只有几个进程,我们把物理机拉过来往上面一丢就可以了,现在这么多的进程,哪些进程应该部署在同一台机器上?它们的数量配比应该是什么关系?系统之间要怎么协作?出了问题要怎么定位?这些都成了问题。

 

图片

 

今天我打算就前两个问题和大家进行探讨。

 

系统之间要怎么样进行通讯?在设计之初,我们要有一个比较好的底层设计框架,也就是刚才架构图里面的Router模块。这个底层框架要负责做系统功能解耦,还要作为各个子系统之间的路由器,并且为容灾和扩容提供基础,所以它本身必须是无状态的。如果它存在太多有状态的数据,那它本身的平行扩容就会成为问题。

 

同时,因为我们知道不同的业务逻辑、业务特性、性能要求会导致不同业务模块在路由策略方面会有各自的选择,所以它还要支持多种路由策略,这对于我们的平行扩容、容灾备份非常关键。

 

图片

 

-如何选择后台的路由策略?

 

接下来我们看看后台的路由策略有哪些?应该如何选择?

 

我们经常用到的路由方式可能就下面这5种,取模、一致性哈希、随机分配、主备和备份。这些路由方式应该怎么选?

 

图片

 

通用原则是对于无状态服务,就是说它只是处理流程,处理完以后不需要保存玩家数据的,我们一般使用随机分配的方式。随机分配有什么好处?如果某台机器挂掉了或网络出现故障,它对后续流程的处理其实不会有太大影响,最多只影响当前处理的逻辑。

 

取模和一次性哈希一般在有状态服务中使用,因为它需要保持一些数据,这些数据可能在下一步操作中用到,或者某个处理流程可能需要二步或者三步,或者有两、三个协议的请求才能完成,那我们肯定希望这两三个请求放到同一个进程中处理,不然可能会发生问题。比如说对特定的玩家,从结算来说可能是对特定的房间,它们必须要能够路由到一个固定的进程上。

 

此外,其实还有一种情况,我们工作中发现它有需要在同一个进程中处理,比如对同一个玩家的同一张表进行写DB操作的时候,放在同一个进程中能避免出现写冲突。

 

而对于「单点」服务——为什么会出现单点?可能是为了避免出现决策困难。比如进行全局负载管理的模块,它本身的性能要求并不是问题,因为它的整体请求数(比如开局请求数)存在一定上限,用一个进程就可以搞定。同时,如果这个进程中做负载管理和分配策略都非常好做,那我们就希望在全服中用一个进程去搞定。

 

对于这种情况,如果这个进程挂了,或这台机器挂了,我们就不能正常进行下去,这时候就需要用到主备方式或备份方式进行处理。

 

这里的主备方式和备份方式有什么区别?在主备方式中,两台机器都会接到请求,但同一时间只有主机器进行服务,而对于备份方式,一般只有主机器挂掉以后,备份机器才会进行服务,平时它可能完全不接受这种请求。

 

这是有关路由的选择,那路由还需要具备哪些特性?

 

图片

 

从业务层面来说,我们尽量希望它无感知。这有点难度,但尽量做到。

 

举个实例,我们写DB时,DB的代理进程通常用的是随机分配的路由策略,因为它是无状态的。但后来我们发现某一段时间里的超时请求特别多,为了查问题,我们临时把DB的路由方式改成了一次性哈希,来定位数据发送到了哪一台DB的代理服务器上。

 

由于我们在开发的时候,没有假设服务器采取的路由策略,在设计业务逻辑时没有施加限制,因此在实际运营中,我们可以比较容易切换路由策略,处理一些特殊场景。

 

同时,路由模块还要能够自动处理网络异常,并能识别业务的通讯异常,这才能实现容灾的功能。

 

上面说的是底层通讯的模块,那部署的时候要怎么做?我们开发时受机器资源的限制,可能用一台机器搞定所有的问题,但游戏实际上线时,我们可能会使用了几十台、上百台甚至上千台机器,怎样把平时的开发和上线部署统一起来?这对于自动化部署能力要求比较高。

 

要做到自动化部署,我们要设计自动化部署脚本。而从实际经验来看,我们在架构设计方面要有一些先期的考虑。

 

我们比较推荐大家采用集装箱式的部署。怎么理解这个概念?

 

图片

 

不同业务进程的功能和性能可能不一样,有些业务进程是CPU消耗型的,有些业务进程则是内存消耗型的,如果把它们搭配在一起,就能最大程度地利用到机器资源。

 

我们上线时一般会对同时在线人数进行预测,比如我们认为游戏能做到同时在线100万,但上线之后我们发现同时在线可能要到200万,甚至250万,这时你临时计算进程要扩容多少,是不是简单地按100万到200万去翻倍?这种简单粗暴的方法不一定能解决不了问题,还有可能会浪费掉更多的资源。

 

因此,在设计之初,我们就要计算不同业务进程的负载能力,区分繁忙的服务和相对空闲的服务,再根据资源消耗情况分配成各种组别,判断不同组别能支撑的用户量,比如这一组能够支撑2万人,那一组可能支持5万人,在实际操作中根据用户量进行扩容。

 

同时,我们希望做到逻辑部署和物理部署分离,这样游戏上线时可以在实体机器和云上机器中灵活选择。另外,我们还会把实际的IP地址转换成抽象化的业务IP地址,在腾讯内部会采用一种叫做 tbusid 的方式。

 

---

 

▌三、平行扩容和容灾要怎么做?

 

接下来看看平行扩容和容灾具体要怎么操作。

 

下面这个案例采用了分区分服的架构,评论服全区共享,开发团队认为评论量可控,所以评论服只部署了一个评论模块。在这个架构中,每个大区的玩家都通过公共的路由模块登录评论服进行评论和聊天。值得一提的是,这款游戏计划在海外市场上线。

 

图片

 

这种架构有什么问题?首先容灾怎么做?如果评论服挂掉怎么办?其次,开发团队先期认为评论量很少,万一实际评论量太大呢?再然后,这款游戏计划在海外部署,那不同国家和地区的舆论风险怎么控制?因此,这个架构不满足业务发展的需求。

 

这里有一些用来参考的解决方案,我们认为要允许平行扩容,并且风险要可控,就必须做分频道管理,所以增加一个频道的管理集群,这个频道的管理一般就是采用我们说的主备部署的方式,根据不同国家、地区或者主题分成不同的频道或评论区。

 

图片

 

这种方式是不是真正解决了独立扩缩容的问题呢?看上去是,如果频道不够可以加频道嘛。但大家可能没有想到一个问题:

 

在这种架构下,玩家所有评论和聊天都通过各自大区,经过跨服路由到达各个频道,如果聊天的数据量特别多,对核心玩法和核心模块在消息和包量上存在冲击,如果后期要做运营活动,像以前的大喇叭、小喇叭、全服广播,以及现在可能会用聊天频道做「飞机票」(组队),就可能会遇到问题。

 

基于上面这些需求,我们希望服务器架构能更进一步。比如像下面这个架构一样,把玩家聊天的消息数据和信令分开,让玩家直接和频道相连,而玩家具体连到哪一个频道通过频道管理的方式去做。这样不管玩家消息量多大,都可以通过扩频道的方式解决。如果用户消息量太大,最多只会导致某个频道挂了或关掉,不会对整个游戏产生影响。

 

图片

 

-故障隔离要怎么做?

 

接下来讲讲故障隔离。一款游戏上线时,如果因为某些原因出现了一个Bug,一时半会还定位不出来,这种情况要怎么办?怎样快速屏蔽故障模块,不影响核心功能,故障恢复时还能及时通知到玩家?

 

我们希望在前端设计一个支持协议路由的模块,同时根据协议或者模块,区分关键流程和非核心流程,再进行区别对待。这样如果在某个请求或模块出现异常,而且短期得不到解决,我们能够尽快线上屏蔽,并知会到玩家,同时模块恢复之后也可以通知到玩家。

 

图片

 

-在线更新的异常处理怎么做?

 

还有在线更新的异常处理。此前我关注到有些项目采用了容器部署,更新方式是直接停掉旧的容器,将其替换成新容器,还有些项目更新大厅模块时,会导致部分玩家掉线。如果我们想保证玩家体验,这些都是不能容忍的。那在设计游戏的时候,要怎样避免这些问题?

 

图片

 

一般来说,我们认为可以使用共享内存解决。不过,我们之前评审过一些项目,尽管它们使用了共享内存,但使用方式存在一些问题,比如有时把一些绝对指针地址直接存到共享内存里,一旦进程重启,共享内存就会失效。

 

现在游戏的线上更新,一般是做一些策划配置或活动配置的更新,所以我们希望进程能够有一个通用的支持reload操作的框架,不重启进程,只通过发一个信号的方式通知有些资源需要更新。

 

对于一些无状态的服务,我们可以考虑支持disable操作,也就是单独禁掉某些进程,比如现在同时有10个服务进程,在承载量允许的情况下,我们可以先禁掉三分之一,对其进行更新再进行灰度发布,这样可以做到不影响玩家的情况下做到线上更新。

 

-削峰的两种常见情况

 

有时候,我们还需要考虑削峰。有些游戏会做准点在线的活动,特别是端游时代做得比较多一些,就是给在线玩家发送道具,鼓励玩家在某个时间点保持在线。

 

这对服务器内部的路由包量和流量影响非常大。我们接到这种需求要进行评估,系统能不能扛住?如果系统扛不住,可不可以考虑采用离线发放的方式?如果不能用离线,能不能使用旁路的方式?或者能不能将自动到账改成手动领取?这都可以变相进行削峰。

 

图片

 

还有一种情况和配置相关,比如游戏运营会有很多活动配置,玩家也会有很多活动进度相关的数据。很多游戏希望在玩家登录的时候,为其展现各种信息,比如红点提示、活动完成进度什么的。他们希望玩家一登录就做大量的配置拉取,这可能会导致玩家登录流程过长,还会使得服务器的下行流量过大。

 

对此我们建议客户端采用缓存+指纹的方式,减少对于不变配置信息的拉取。从服务器和客户端交互的协议设计角度来说,我们希望把动态和静态的数据进行分离。

 

何为动态和静态?比如活动配置其实相对来说是静态的,可能在某个赛季或某个活动期间配置保持不变,而玩家的活动进度是动态的,这块数据其实相对会比较少。我们可以把这两块配置分离,在玩家登录的时候,如果活动配置没有发生变化,就只拉取他们的活动进度。

 

此外游戏经常会有一些更新,有时为了方便我们会希望直接通过服务器带下去,但其实可以考虑采用CDN来降低成本。

 

-需接入第三方系统时怎么处理?

 

最后说一下第三方系统。游戏上线时不可避免要接入一些第三方系统,其中可能包括给运营、客服使用的业务受理模块,以及在线广告、支付等第三方系统。

 

图片

 

接入第三方系统会存在一些问题,比如它的访问量不可控,比如我们不知道客服调用的频次,也不清楚如果用业务受理系统做web页面的互动,它的量级会有多少。而且第三方系统的协议有可能跟我们的协议不兼容的,在访问权限上也需要进行控制。

 

从我们的经验来看,建议大家用一个模块对第三方系统集中进行包装,做独立的部署和流控。同时,这个模块还负责做协议的转换及隔离。

 

这样相当于为游戏系统和第三方模块之间建立一道防火墙。如果第三方系统的流量突破上限,我们可以及时扩容,甚至一定程度地根据实际情况来屏蔽或停止第三方系统的服务。

 

当然,第三方系统有时会做一些升级的活动,或者性能有可能出现一些问题,如果我们有独立部署的模块,在这个模块做一些针对性的适配,就能避免第三方系统的问题冲击游戏的后台系统。

 

这些就是我今天分享的内容,谢谢大家。

阅读与本文标签相同的文章