编者按 在8月29日的GWB游戏品鉴会上,腾讯天美工作室《使命召唤手游》服务器主程Jerry,分享了服务器架构如何优化以及常见的服务器问题解决方案,以下为文字版本。
分享/Jerry 整理/迪亚菠萝包
炸服,是近两年频繁发生、游戏厂商闻之色变的运营事故。
虽然从某种意义上来说,炸服意味着游戏很受欢迎,但由于炸服会致使玩家无法登录、频繁卡顿、反复重连,甚至出现数据丢失和回档,导致游戏口碑暴跌。一旦处理不好,可能会使一个原本的爆款丧失第一波爆发的机会。
从服务器架构设计的角度,如何避免上线即「炸服」的事故发生?
上周六,在腾讯游戏学院主办的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页面的互动,它的量级会有多少。而且第三方系统的协议有可能跟我们的协议不兼容的,在访问权限上也需要进行控制。 从我们的经验来看,建议大家用一个模块对第三方系统集中进行包装,做独立的部署和流控。同时,这个模块还负责做协议的转换及隔离。 这样相当于为游戏系统和第三方模块之间建立一道防火墙。如果第三方系统的流量突破上限,我们可以及时扩容,甚至一定程度地根据实际情况来屏蔽或停止第三方系统的服务。 当然,第三方系统有时会做一些升级的活动,或者性能有可能出现一些问题,如果我们有独立部署的模块,在这个模块做一些针对性的适配,就能避免第三方系统的问题冲击游戏的后台系统。 这些就是我今天分享的内容,谢谢大家。关于腾讯游戏学院专家团
如果你的游戏也富有想法充满创意,如果你的团队现在也遇到了一些开发瓶颈,那么欢迎你来联系我们。腾讯游戏学院聚集了腾讯及行业内策划、美术、程序等领域的游戏专家,我们将为全世界的创意游戏团队提供专业的技术指导和游戏调优建议,解决团队在开发过程中遇到的一系列问题。
申请专家资源请前往: