从游戏角度看后台开发

发表于2015-04-29
评论2 8.4k浏览

前言

本人以前所从事企业软件开发,按传统的软件工程方法参与过大量软件系统的建设。加入IEG这几年,有幸经历多个优秀的游戏团队,通过产品对比思考总结了相关的差异。从后台开发的角度对游戏开发过程、后台技术架构两方面的要点做一次简要总结和回顾,希望能抛砖引玉,也希望能让游戏行业新人快速全面的了解游戏后台的开发方式,因为写得比较粗略,请大家多多批评指导。

 

一、开发方法简述

      首先需要提到的是产品的开发模式(当然开发方式不仅限于后台),从前传统行业的项目或产品按顺序贯穿需求分析、业务建模;概要设计、详细设计,编码测试、集成测试、上线等等过程,系统分析设计的时间占项目总周期的60%可能更多,基本上到详细设计已经差不多是伪代码了,而且正式上线后功能会保持稳定,后续的版本变化的周期也相比漫长。这种方式也就是传统的瀑布模型,基本的过程可以简化如图:

 

http://km.oa.com/files/post_photo/929/224929/330d9d3dc2664260cdceae11010234881418262511.png 

 

       传统的瀑布模型其实有几个问题:

      1. 需要准确的理解业务需求,导致分析周期过长;

      2. 因为前后依赖,很难精确的评估开发时间;

      3. 总体进度不好控制,容易造成延迟风险;

     

而我们熟悉的游戏开发,普遍应用迭代模型,因为在没有先玩到游戏前,无法对产品进行评估。而迭代的优点很明显:能立即从结果对设计进行反思

      如下图:

 http://km.oa.com/files/photos/captures/201412/1418261778_53.jpg

在迭代开发的过程中需要花大量的时间玩游戏,所以需要对游戏做prototype,其实这个prototype也可以是非数字化的,比如某些类型的游戏可以使用纸模或者卡片进行还原。

迭代开发模型也被称之为“敏捷开发”,是游戏行业的标准开发模型,有段时间最流行的敏捷开发方法是SCRUM     SCRUM方法中最主要的是Sprint概念,简单的说就是指定一个短期的时间(比如我们是每周)周期交付一个功能可测试的版本。这个版本中需要实现的功能特性,叫做Sprint Backlog(最终发布功能中的一部分功能子集)。每次冲刺后需要对功能需求再做重新评估。本文不对具体的SCRUM方法进行深入探讨,有兴趣各位google之。

http://avocado.oa.com/fconv/files/201412/4f9adb82fa6b2be0563270b82a27e008.files/image003.png 

SCRUM方法

可见相对几种开发模型就可以看出游戏开发的需求变动和版本发布周期必定更加剧烈,如果需要在产品经历中积累后台经验,需要不断的思考、重构再提炼,不仅要上升技术抽象层面,还需要从业务层面进行抽象,去积累和强化自己的游戏开发思维。

基于这一点,我们对比传统企业软件从游戏产品角度的角度出发,从技术和业务两个方面做出如下回顾总结。


二、游戏后台架构

游戏后台与其它产品相比有什么特殊的呢,没做游戏之前,我也非常好奇。回顾一下,传统软件形成了非常多的架构应用模式,简单的比如Client/Server,Browser/Server,3Tier,MVC模式等。传统的软件架构在演化的过程中,从早期的二层架构进化为三层架构,如下图:

 

 

 

 

 

http://avocado.oa.com/fconv/files/201412/4f9adb82fa6b2be0563270b82a27e008.files/image004.jpg

 

http://avocado.oa.com/fconv/files/201412/4f9adb82fa6b2be0563270b82a27e008.files/image005.jpg

 

 

 

 

因为使用了关系型数据库,上面的架构最常见于事务型交易系统。这也是早期很多互联网应用的架构原型,比如Client Tier变化为Browser,Business Logic Tier变化为Web Server+Application Server,然后最后面是Database Tier。举个例子,如LAMP技术中的ApacheWebServer/Mysql/PHP。

伴随互联网的发展,技术体系的架构开始不断发展创新,相对传统应用场景出现明显的特点,也由于应用场景的复杂化,需要不断挖掘各个层面的技术深度。例如以下后台需要面对的两个核心问题:

l        网络环境:相对传统企业应用,客户端不再集中在一个局域网环境,而是分布于互联网任何一个角落,它们有的通过ADSL接入,有的通过Cable接入,有的是无线接入;它们的地理位置也大不相同,所对应的运营商也千奇百怪。此时所面临的就是接入问题。

l        用户数量:因为用户群和使用方式的限制,传统企业应用只会有一定数量的访问量,比如银行的ATM,数量受到网点的限制,用户还需要排队处理。而互联网应用则完全跳出了这个限制,游戏也一样面临海量问题。

 

而网络游戏相对常见的互联网应用又有如下差别:

l        更强的应用实时交互:网络游戏具有丰富的表现力和堪比现实的虚拟场景,尤其是主机游戏和大型端游。玩家在游戏中成为虚拟世界的主导者,需要实时与游戏中的环境实时交互,并得到反馈。所以如何设计游戏的逻辑分层变得至关重要,游戏逻辑中哪些放在客户端,哪些放在服务器需要仔细斟酌。这也成为决定游戏架构的核心问题之一。

l        更强的多人互动体验:不可否认,网络游戏中能够如此吸引人的原因之一,是虚拟世界中人与人的互动引发的情感联系。当身处游戏世界,与你互动的虚拟形象背后是分布在世界各地的不同的真实面孔。如何让你的游戏高效的处理多人状态同步,准确的判断多人交互事件的结果,也是游戏开发所力求的独特技术能力。

 

那么,总结以上游戏产品的技术需求,从后台的技术层面上看,常见架构可以抽象为以下几个简化的层面:

http://km.oa.com/files/photos/captures/201412/1418300752_10.jpg

 

上图中,由五个抽象层组成整个游戏服务端,这些功能抽象层即可以是单机共存的也可以是分布式的;有的层在根据实际应用场景合并为一个进程。所以可以灵活的按照应用场景进行组织实施。 

需要重点说明的是,各抽象层的数据交互通常都使用异步处理完成。

      下面我们就来看看各个抽象层面所需要面临的问题和常见的解决方案。

1.     接入层

      把接入层分离独立出来主要目的是为了单独关注以下几个内容:

      针对性的解决网络问题

      如文章上面提到的复杂网络环境,接入层需要处理的问题有:

      使用什么样的通讯方式与客户端通讯,TCP还是UDP,长连接还是短连接?

      面对海量用户时,如何接收更多的客户端连接?

      如何更高效的处理客户端请求的分发?

      如何高效的定位网络层的异常问题?

      如何处理跨运营商接入问题?

      如何部署来应对请求的负载均衡?

以上问题任何一个都可以写一篇深入的技术分析,这里做为全局型的总结回顾性的文章只提一提原理上内容。也正因上述问题属于纯粹的技术细节,完全与业务层无关,所以在游戏后台通常会将接入层设计为一个独立的网络服务器。

接入层通常是一个支持多种连接方式的Socket Server守护进程,支持通过配置决定是以长连接或是短连接进行服务。也支持通过配置开放TCP或者UDP协议的服务。

在性能上,基于Linux的接入层服务器通常都是epoll+多进程服务的形式,有重造轮子实现的也有复用开源组件Libevent的。做为Linux后台开发,还必须了解如何调整操作系统参数设置网络层相关的配置,比如最大连接限制、又或者是kernel中net.ipv4.tcp的相关参数。

在跨运营商接入的问题上,早期使用支持多个运营商线路的机房,也有在多个运营商机房分别部署服务的方式。另外还需要通过DNS解析支持,比如在联通接入的客户端,DNS会将它接入到联通线路的服务器等。现在则通常使用公司TGW来解决这个问题,多通接入和IP收敛都可以关照到方便多了,具体信息可以从KM中了解TGW的功能和特色。

 

      更灵活的访问权限控制:

设计游戏后台,首先要考虑的就是玩家账号数据的管理。有了账号则面临的是登录鉴权的处理。在腾讯,海量用户天然形成了完整的用户账号体系,在外部的联合运营平台也同样把用户账号体系全权承包了,所以游戏产品通常是接入游戏发行平台的账号体系。

我们在把接入层单独剥离成独立服务进程后,交给它处理第二个任务就是与发行平台的账号体系结合,比如接入公司的MSDK进行鉴权处理。把鉴权交给接入层有以下几个优点:

其一、接入层做访问权限控制,未鉴权的客户端请求第一步就被拒之门外。

      其二、接入层做为鉴权服务的客户端,单独处理与鉴权平台的权限控制,比如只需要接入层的机器打开与第三方鉴权平台机器的网络访问权限。

      其三、接入层可以将鉴权处理封装成可配置或可动态载入的模块,这样接入一个平台只需要轻松加载一个鉴权模块即可,不会对其它系统有任何影响。

      第四,接入层与客户端定义通讯协议,对于探测包、格式异常包、非法数据包都将无法到达核心业务逻辑层;并且可以在接入层处理协议的防重放处理。

 

      与逻辑层分离降低耦合:

      独立了接入层之后,与客户端的网络层的处理就与逻辑层无关了,由接入层负责与客户端通讯,通讯协议、通讯方式、通讯数据的字节序以及格式均由接入层全权负责。接入层只需要把收到的合法数据仍给通讯层,逻辑层就也责任更单一了,只需要从通讯层拿请求然后按命令字处理业务逻辑即可。由于耦合性降低,带来了明显的优点:

      首先,逻辑层与接入层各自的维护互不影响,比如更新逻辑层代码、重启逻辑层,都不会影响接入层的客户端连接状态,不会因为进程重启导致客户端连接被强制断开。

      其次,接入层的变化对业务逻辑的影响可以降到最低,例如针对服务器与客户端的通讯可以单独进行优化,只需要在接入层增加压缩处理打开压缩开关,即可增加通讯层的压缩处理达到降低带宽的目的。

     

      一个简化的接入层服务进程结构简化如下:

 http://km.oa.com/files/photos/captures/201412/1418261721_19.jpg


      针对负载问题,接入层通常还提供统一的接入分发处理,比如游戏按区切分后,接入层还有必要配合游戏的开服方式提供选服服务,这就是目录服务器(我们这喜欢称之为DirServer)。其实目录服务器也属于接入层的服务器模块。 

      相信不用提,大家也都知道在互娱研发部提供的公共组件中,TConnd就是很多产品应用的接入层组件了。

      需要注意的是,如果是多客户端间实时同步(比如同步模式)的游戏,可以把接入和逻辑放在一块,以更快的响应同步逻辑。

     


2.     逻辑层

      企业级应用的逻辑层通常部署或运行于容器(Container)内,抽象了资源接口(Resource Interface),让逻辑层专注于业务流程实现。而游戏后台的逻辑相对简单多变,但游戏客户端的Game Engine及相应基础设施和这个概念比较相似。如果在逻辑层进行抽象,形成平台和脚本接口的概念就相同了。

      游戏后台的逻辑层的逻辑实现通常可以分为两部分内容,首先是按核心玩法确立的游戏主逻辑,我们可以称之为核心逻辑;其次,除核心玩法外通常会提供其它玩法围绕游戏的经济系统展开以丰富游戏的体验,这些可以称之为外围逻辑。

      逻辑层在技术实现上,与游戏的核心玩法密切相关,不同的玩法将导致完全不同的逻辑层架构的实现方式。不过可以肯定的是,逻辑层的核心任务就是玩家属性相关的状态控制

      如下图,逻辑层的表现形式通常是服务器守护进程,从通讯层取得客户端请求,然后按命令字处理相应的用户状态处理逻辑。比如,注册是创建用户数据,登录是拉取用户数据并鉴权,行走是修改用户坐标等等。图中红框为Main Loop。

http://km.oa.com/files/photos/captures/201412/1418300870_6.jpg
       从技术角度看,逻辑层的任务仍然脱离不了Input/Process/Output这几个步骤:收取客户端请求、按请求处理相关业务逻辑、返回处理结果。

       但因为游戏服务器需要承载足够多的玩家,并且需要实时同步状态到游戏场景的特点,对请求的响应有很强的需求,所以逻辑层需要精心设计以达到最佳的性能

       通常,逻辑层的服务会按功能粒度拆分成单一功能的组件进行异步交互以提高效率。这样做的好处,就是把串行的任务步骤分拆成多个子任务步骤,核心游戏循环控制任务步骤的顺序,把子任务步骤分发给各个相应的功能服务组件去分布式处理,以达到分担计算量的效果,能有效的提高逻辑层的吞吐量。单进程串行处理升级为分布式异步处理形式的基本架构如下图:     http://km.oa.com/files/photos/captures/201412/1418300986_75.jpg

      上图右侧的架构中,可以把Logic Server看成是Master,而专用处理某种任务的服务器可以看成是Worker。以某游戏为例,逻辑层按业务功能可以分为大区服务器、地图服务器、PvE服务器、PvP服务器,排名服务器等。其中的ZoneServer大区服务器就可以看成是Master服务进程。用户的请求首先到达这里,然后处理按具体的功能分发到相应的专用逻辑服务器组件进行处理。

      所以逻辑服务器可以表现为单进程形式,也可以是多进程形式,由于线程安全以及竞争带来的复杂性导致很少使用多线程方式;而且因为将任务步骤拆分后已经实现了类似并发的效果,所以对多线程化的需求并不强烈。

      如上所述,逻辑层的结构可以通过把同步串行打散成异步并行,同时通过分区切分解决掉海量问题和性能问题。

关于玩家数据的状态控制

        对服务器来说,状态化的判断是指两个来自相同发起者的请求在服务器端是否具备上下文关系。如果是有状态,那么服务器端一般都要保存上下文相关信息,每个请求可以默认地使用以前的状态信息。而无状态请求则不行,服务器端所能够处理的逻辑,其处理信息必须全部来自于请求所携带的信息以及其他服务器端自身所保存的、并且可以被所有请求所使用的公共信息。

无状态

      很多游戏使用了无状态,无状态简洁实用,每次交易都是没有关联的,上下文信息保存在用户数据对象上,每次请求拉取玩家数据,处理完逻辑记录状态再回写回去。但无状态也有天生的缺陷:

1.    实时交互:因为没有在线保持玩家的状态(即上下文),无法得知用户的在线状态,不能即时交互,比如实时聊天;

2.    并发修改:当并发请求或多人交互时可能产生数据状态的读写顺序问题,比如两个玩家同时PK第三个玩家,第三个玩家是先被哪一个玩家修改无法控制。所以无状态服务需要提供锁服务器控制数据的并发写。

有状态

      即玩家对象的做为上下文状态保持在服务器上,客户端与服务器的交互允许有上下文关联(也同时控制了访问顺序),服务器逻辑可以方便的主动触发玩家数据的状态。但有状态的服务器有几个关键的问题:

1.    状态的迁移:由于现网的容灾问题,单个服务组件通常会部署多台机器提供服务,这就需要严格谨慎的处理玩家对象的状态迁移,比如一个玩家同时只允许存在于一台服务器上,即踢人逻辑。否则将导致状态的错乱。

2.    状态的同步:由于逻辑服务器上保持了玩家对象的数据状态,这就产生了同步状态到数据层的问题。如何保证状态的一致性或者是实时同步,也需要细致的控制,否则将导致用户数据的回档。

 

      逻辑层服务通常使用平行扩展的方式扩容。如果是有状态的服务,因为状态迁移的问题,导致扩容时通常需要停服处理。而无状态服务,可以做到不停服扩容,因为不存在状态迁移问题。

      逻辑层的部署,需要考虑各功能模块的硬件资源消耗情况统一进行规划,比如吞吐量、承载容量、网络流量、CPU/内存/磁盘消耗情况。通过TPS/内存消耗可以有效的设计各功能模块的配比。

      另外,需要提醒的是逻辑层公用模块的部署形式,建议使用统一的无状态的公共服务集群为所有的游戏大区服务以控制成本,而不必每个区单独提供。


3.     通讯层

      相对企业级应用,游戏后台的通讯层抽象相对简单。理想的通讯层用于业务组件之间的解耦,只提供简洁的API调用,然后让程序员关注业务逻辑即可。

      通讯层的表现形式可以是通讯库、通讯服务进程,或者是单独的通讯模块组件,比如相应的有ACE/ZeroMQ,TBus,或者Router组件等。

      我认为通讯层所关注的问题主要有以下几点:

l  高性能

      通讯层要达到高性能,必须无所不用其极。首先,游戏后台各组件的运行环境高度一致,不像企业级应用那样需要考虑Legacy System,只需要专注挖掘指定环境的细节即可,即只需要考虑Linux环境下同机通讯与跨机通讯两种形式的效率,保证用最高效的方式进行通讯处理,如本机通讯可以使用IPC中的共享内存,跨机通讯只能使用Socket长连接。这一点上,开源的ZeroMQ可以通过代码来选择IPC/TCP等形式,比如使用IPC通讯时,如果发现是在本机通讯,由于是跨平台的通讯层会自动使用Unix Domain Socket方式通讯;而TBus就不需要考虑异构情况,直接使用的是Shared Memory的方式。

      其次要提高性能就是吞吐量的问题,通讯层需要适当的机制进行合包、分包处理,以最适合的包大小和读写频率控制通讯效率。这一点上,应该TBUS有所处理,不过看不到源码不太确定它的处理方式。另外,很多通讯库也考虑zero-copy的问题,nanomsg在功能中把RDMA(Remote Direct Memory Access)都使用上了,允许从User Space直接发送给网卡;有些通讯库则使用共享内存中的同一块内存进行通讯达到zero-copy的效果。

 

l  稳定性

      做为通讯层,最重要的责任就是保证消息送达。通讯层必须提供冗余机制来保证消息被实际被处理,并且是按照顺序被处理。所以满足这个条件的通讯层必须提供消息持久化的处理,通常的实现方式是提供可持久化的消息队列用于保存消息数据,消息必须在成功处理后才能从队列中删除。比如ZeroMQ2.x也是把Queue放在内存中,有当机丢失问题;而IBM Websphere MQ则提供多种持久化方式来保证消息的完整性。

      在被许多消息队列所采用的"插入-获取-删除"范式中,在把一个消息从队列中删除之前,需要你的处理过程明确的指出该消息已经被处理完毕,确保你的数据被安全的保存直到你使用完毕。同时需要确认一个消息只能被处理一次,即只送达一次。这一点上,TBus还提供不删除的模式,比如在队列中查看消息而不删除,这种情况下可以理解为把消息从队列拿出又放回去。

      再者,通讯层组件还需要保证可恢复性,比如通讯进程被kill后,启动后需要还能从中断点继续服务。

 

l  多功能

      完整的通讯层组件需要支持多种通讯模式,比如同步、异步通讯;上升一个层次,可以通过商业级MQ的功能来对比这些模式的支持,比如各MQ提到的几种模式:

n  Point-Point:点对点,即发送者和接收者的通讯模式

n  Request-Reply:请求/回应,消息的消费者要应答生产者处理结果

n  Publish-Subscribe:发布/订阅,消息按Topic订阅进行消费。

      很多只提供P2P的模式的消息中件间,通常是简单的FIFO的队列,消息接收方需要使用轮询的方式主动查询取得消息,导致逻辑响应处理的延迟。

 

l  易管理

      之前提到通讯层组件的形成主要的目的就是解耦,让通信双方只关注业务逻辑。由于通信环境的差异,以及通信模式的需求,需要让通信层组件提供易于管理的应用界面,以及在运行过程中提供方便的查错、监控功能。

      从通讯组件的易管理易应用的层面,业务逻辑层关注以下几点:

      是否提供简洁的应用接口,可以程序化控制通讯模式

      是否能够灵活控制通讯通道,比如动态建立,权限控制

      支持消息的事件通知回调,是否还需要轮询检查

      是否有水位控制,队列满的情况下如何有效的应对

      是否能够方便监控消息的流量

      在游戏后台最常面临的管理问题就是扩容、缩容的操作,这个过程中需要增加后台服务组件,这时如何方便的建立通讯层关联就需要通讯组件的支持了,通讯通道是否需要重建;重建过程是否需要停服等等。


4.     数据层

      游戏后台数据层,用于持久化玩家的数据。因为玩家数据之间的弱关系特性,和其它互联网应用一样,也从SQL形式逐渐进化到NoSQL形式。所以通常数据层也同时是缓存层,主流的数据层分几类

l  使用SQL型数据库

      多见于早期的网络游戏,尤其是日韩的产品。使用Amazon的服务,基于LAMP的架构最多,很多产品仅使用了PHP+MYSQL的形式,因此性能较低。早期还有基于Socket Server+SQL的形式,比如使用Windows Socket Server访问MSSQL或者其它品牌数据库的产品,据了解韩系的游戏较多是这类。

      此类架构的游戏因为性能问题,逻辑通常偏重于客户端,容易导致外挂风行。有一些产品会使用某些定制的数据层封装提高性能,比如MySQL的Handler Socket Plugin。还有一些产品会直接使用NoSQL层做为缓存,比如应该广泛的Memcached或者Tokyo Cabinet;在缓存之后再使用MySQL做为落地的数据库,这样做的目的主要是把数据层的操作做读写分离。我们的产品所使用的方式就类似,应用层访问自制缓存,其后使用多进程的读写进程将缓存与MySQL对接。

 

l  使用NoSQL型数据库

      互联网应用的需求让NoSQL迅速流行,游戏的NoSQL应用也非常多。常见的NoSQL还是主流的那几款开源产品:Redis、MongoDB等。个人觉得公司级的产品,CKV和TCaplus非常好用,都是久经考虑的NoSQL型数据层服务了,KM上有非常多的资料就不多说了。

 

     数据层和缓存层在整个互联网应用的非常多,这里就不赘述了。对数据层的要求也是能灵活的搭建,即可以形成单机服务,也可以支持分布式服务。对缓存的建议是支持热淘汰、支持持久化文件落地,以及方便的实现数据导出及统计。

 

三、结束语

      回顾盘点了一堆自己的体会与总结,关于游戏后台当然还远远不够,游戏后台不仅仅包含开发模式、技术架构;更多的是游戏业务的抽象与设计,需要理解游戏玩法的特性构建对应的架构,还需要权衡游戏后台运营方式对后台架构的影响的需求,比如全区全服和分区分服的架构部署方式。

      在这个竞争激烈的手游时代,这些的需求也越来越强烈。我想,这些方面后续还需要再做全面的思考与沉淀,才能算满意的总结吧。

 

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引