作者博客:懒人居 - Coding for fun
高性能MMORPG通用服务端引擎设计之->基本概念篇二
鉴于公司保密协议,本系列文章将不涉及具体的游戏细节以及实现。由于本人也是第一次参与此类引擎的设计,所以难免有所失误,如有异见欢迎业内人士讨论,发表本系列文章的目的不在于说教,重在分享以及讨论。
MMORPG的服务端引擎是驱动整个游戏的总要部件,而且对于现在外挂满天飞的年代,服务端的地位变得愈发重要,很多游戏都将很多原来由客户端处理的逻辑交给了服务端来处理,以避免各类外挂对游戏公平性造成的影响。要设计一款通用的且高性能的MMORPG服务引擎是一个非常艰巨的工作,总的来说,一款网游能够承载多少人,能够实现什么功能,能够开展什么运营活动,都是跟有一款够不够强悍的服务端引擎有密切的关系,可以说服务端引擎就是游戏的心脏,如果设计不好,就会“心率不齐”,“心肌梗塞”,然后不能支撑足够的在线人数,操作“卡”,最后死掉翘辫子。设计精良的引擎,比如WOW,EVE Online等,则给运营和策划留下来大量的空间,这样游戏的成功也就不存在“技术问题”了。
一般来说,当然是想一个游戏装的用户越多越好,当然这里有很多现实上的制约,第一是运营上,游戏里总是老玩家等级更高、实力更强,由于游戏的竞争性,玩家都喜欢进新服,所以一组服务器总是在开服的时候开始在线人数持续攀升,然后在某个点后就会下降并趋于稳定。其二是因为游戏中的场景不是无限大的,有固有的玩家密度限制,如果一个游戏屏幕上到处都塞满人,估计你也不会觉得好玩。并且根据这两个特点就决定了,MMORPG的服务引擎需要有高性能,这样才能在开服的时候承载尽可能多的用户进入,并且要具备高度的可伸缩性,在开服用户在线高峰的时候可以用比较多的服务器资源来拉动用户,在平稳运行后能够省出多余的服务器资源来挪给其他的服务器组。
从最简单的来说,MMORPG游戏基本上就是经典的C-S结构的系统,所以最简单的结构就是
Server
|
|
Client
这个时候Server端什么都要处理,就是忒累点,所以承载能力也就不用指望了,扩展能力就更不用想了,不过胜在简单,如果好好设计性能还是有保障的。有个开源的C#开发的UO的服务引擎实现大家可以借鉴一下,http://www.runuo.com/。其实从本质上来说MMORPG的服务端和企业应用没有差别,所以我们解决问题的方式也是很相似的,如果性能有问题,一台服务器无法解决问题,就用两台,属于叫分而治之的策略,所以要提高性能,我们需要从系统的最慢的地方开始。如果有点经验的开发人员都知道,系统中最慢的就是数据库了。所以要提高系统的响应,我们一般是在用户的角色登录的时候直接将角色的数据读进内存来处理,但是内存的数据不怎么保险,万一服务器当掉或者掉电了怎么办呢,这个问题就需要定时将用户的数据变更发给数据库了,这样就不存在读写数据库的延迟了,当然掉电的时候在上一次保存后的数据还是会掉,这个时候就叫服务器回档,这种小几率事件一般我们不用特殊处理,一般游戏偶尔出现故障回档的话基本都是对在线的用户赔点经验装备了事。以后架构还有可能扩张,而为了让你不必将SQL写得满世界都是,我们将数据库操作抽象成数据库服务,为了多台服务器都可以一视同仁的使用,也也可以单独用一台服务器来作为数据库服务的主机,如下图:
DB-Service
|
Server
|
CLient
这张图看着很眼熟吧,恩,啥三层架构不就是这么画的嘛。囧 这个时候系统的压力就不在数据存储上了,于是压力到了游戏的逻辑上,现在游戏既要同时处理大量的并发网络请求,且还要对游戏的逻辑进行运算,一边是高IO应用,一边是高CPU运算,都放在一块那就是一半火焰一半海水,不是烧死就是淹死了。高网络IO并不需要很强的CPU运算性能,比如路由器,一台城域网的核心路由器负担了整个城市的出口网络路由,IO高得吓人,路由器其实也就是一台特殊的电脑,但是这台核心路由器的CPU其实并不比我们的家用PC的CPU高,甚至有可能低一点。但是游戏的逻辑运算对CPU的消耗就是实打实的消耗了,假设游戏有1000个玩家在线,这个时候每处理一个事件需要1ms,那么假设每个玩家每秒有4个事件需要处理,那么1000个玩家就把一颗4核CPU或者2颗双核CPU的计算能力耗尽了,这里还是假设的理想情况,如果再加上NPC,怪物,物品之类的要处理,那就需要非常多的CPU资源,如果所有CPU资源都给了逻辑运算,网络消息处理所需的CPU能力就不足了,虽然消耗低,但是总是消耗的,所以我们继续采取分而治之的办法来处理,我们把前端服务器和逻辑服务器分开,前端服务器用来处理大量的用户链接和消息分发,而逻辑服务器用来处理游戏逻辑。由于游戏逻辑更消耗CPU,所以一个前端服务器可以带N台逻辑服务器,所以结构改成了下图:
Logic-Service Logic-Service DB-Service
| | |
-------------------------------------
|
Front Server
|
Client
我们可以继续将游戏逻辑进行分类,由于MMORPG的每一个玩家从经入游戏开始就是处于一个个的场景当中的,所以逻辑服务器也可以叫场景服务器(地图服务器),有的游戏即使没有场景切换,其实也是分了很多场景的,只是采用了无缝拼接的技术让你觉得是没有分开的。玩家的逻辑就可以分为连续事件逻辑和瞬时逻辑。连续事件逻辑是在场景中需要和其他用户发生反映的事件,也可以称之为场景事件,比如我攻击了一个怪,这一个事件需要通知场景里所有的玩家知道,并且会影响怪接下来的行动。所谓的瞬时事件,就是只会影响玩家自身的状态且不需要通知其他玩家或者说是对场景产生影响的(当然如果对场景产生了影响势必需要通知场景内其他玩家)。有的事件甚至会跨场景通知系统内所有用户(比如某玩家击杀了某著名BOSS,或者通知师父自己的徒弟在某处遭到了攻击[假如要实现师徒系统的话])。玩家大部分的逻辑都是在场景内完成的,所以场景逻辑的实现非常的重要。在这个部分的运算涉及到多个对象间的互动,如果想用多线程来提高并行度来提高性能其实反而会适得其反,因为要保证计算的数据安全避免脏读,在多线程的环境下就需要处理大量的锁,相信在游戏的业务逻辑里还需要处理锁,防止死锁这里的处理会宁所有的程序员抓狂,对于这种高CPU运算的场景来说,大量的线程也会将宝贵的CPU时间浪费不少在线程切换上,所以一般来说游戏的逻辑服务器都是单进程单线程的结构,通过一个大循环来驱动整个事件逻辑。那么你可能会问,如果有单进程单线程岂不是只能利用一颗CPU内核了么?当然一个游戏也不可能只有一个场景嘛,我们可以在一台服务器上跑N个场景服务,处理N个场景的逻辑(根据经验来说,N=CPU内核数量性能最好)。所以你可以看到为啥上图我不写Logic Server而是写的Logic Service了。
当玩家连接上Front Server后怎么知道要把数据发到那一个Logic Service呢?这里就需要场景管理服务了,根据我的想法,我想在场景管理服务里提供一个用户代理,用户代理知道用户在那个场景中,代理了用户登录场景,离开场景的行为,以及帮助用户将数据转发到正确的场景服务上,所以结构图继续进化成下图:
Logic-Service Logic-Service DB-Service
| | |
------------------------------------
|
Scene Manager
|
Front Server
|
Client
由于Front Server的数据都由场景管理服务器转发到场景上,所以我们在前端可以部署多个Front Server,因为一个Front Server只有100M的带宽,当Front Server性能不足或者带宽不足的时候我们可以通过两个来搞定,而Scene Manager只通过用户代理来做包转发,所以承载能力相当的高。再次增强后系统如下图:
Logic-Service Logic-Service DB-Service
| | |
------------------------------------
|
Scene Manager
|
----------------
| |
Front Server Front Server
| |
Client Client
考虑到Front Server有可能会存在CPU剩余的情况,所以也可以将一部分瞬时事件交给Front Server来处理,当然有一个公共的很耗时的瞬时逻辑需要被剥离出来,那就是登录的逻辑,登录的逻辑每一组服务器都一样,所以我们可以将N组服务器的登录交给一个统一的登录服务器来处理,所以我们再次进化
Logic-Service Logic-Service DB-Service
| | |
-----------------------------------
|
Scene Manager
|
-------------------------------------
| | |
Front Server Front Server Login Server
| |
Client Client
现在看起来大体上已经有点模样了,但是其实还可以根据不同的需求做一些细节上的调整,比如怪物的AI运算,如果仅仅是很简单的逻辑运算,其实就可以直接在场景服务器的逻辑中处理了(其实有点像把怪物当作了场景的一部分)。如果需要很复杂的AI,就需要将怪物的AI拿到单独的进程里运算了,其实就是把怪物当成一个特殊的玩家来对待,这种模式比较特殊,由于比较耗资源,一般都对大BOSS采用这种方式。写了这么多也不知道是不是说明白道清楚了,不过今天先说到此处打住,下一回我们从细节上来看这每一部分的具体设计,或许还有部分实现。
感谢iiegg的刘洋以及王秋婷同学给我的大力支持,作为一个新人来说得到业界前辈的无私帮助真是非常幸运的事情。
本文为亚历山大同志独立思考之结果,有可能在最终结果可能于某些网络上已有资料不谋而合,皆因为相同产品设计理念相同,非抄袭之过。本文欢迎讨论,欢迎转载,如转载请注明出处,谢谢。
书接上回<高性能MMORPG通用服务端引擎设计之->基本概念篇>
上回说道我们将服务器组的职责划分为了,前端服务器,场景服务器,登录服务器,数据服务器...etc.
如图:
Logic-Service Logic-Service DB-Service
| | |
-----------------------------------
|
Scene Manager
|
-------------------------------------
| | |
Front Server Front Server Login Server
| |
Client Client
不过经过思考后发现这个结构有点问题。
问题何在呢?我们来好好分析一下游戏的逻辑后会发现,整个MMORPG的服务端逻辑,其实是对服务端事件的响应,这里有两种事件,一种是对自身属性的改变(比如切换技能,向腰带放入药瓶之类的),一种是改变其他玩家属性,或者是需要通知其他玩家知道的自身属性改变(比如,攻击对方,自己回血,换装,移动等),也就是前文中我们分析发生在场景中的事件,第二种事件远远超过第一种事件,而且大部分的业务逻辑也发生在这些事件中,所以这类事件的处理需要消耗更多的CPU。这样就带来问题了,前端服务器的压力主要在IO,而场景服务器的压力主要在CPU,那么如果分开在不同的机器上部署,那么这两边服务器的压力就不均衡了,前端服务器的CPU剩余很多,场景服务器的CPU又不够用。所以我决定将这两个部分再次合并起来,合成Logic服务,然后多个Logic服务并行运行。
所有的游戏逻辑都在逻辑服务中运算,但是按照MMORPG的业务,所有的逻辑基本都是按照场景来分布的,所以这个时候我门又面临2难的选择题了。第一种方案是,我们可以按照场景来组织逻辑,比如服务器A负责1,3,5,7场景,服务器B负责2,4,6场景,因为场景之间的人数不平均,有的场景人多(比如某某城内,正在被大批人追杀的BOSS所在地),有的场景人少(比如运行一段时间后的新手村),而且游戏中的热点场景是动态变化的,随着游戏进程的发展会有变化。所以这个情况下我们需要一个场景管理服务,这个服务用于监控每个逻辑服务,将热点场景所在的服务里的其他场景迁移到空闲逻辑服务器。这个逻辑需要客户端提供支持,要实现服务器的软切换。还有一种方式是将场景的逻辑随机分布到所有逻辑服务器上,场景间的联系通过消息来传递,由一个消息服务器来为所有场景提供广播组来分发消息。这样就会多个消息服务器出来。这两个方案中第一个对单个玩家来说延迟最小,但是如果场景管理做得不好就会造成CPU资源分布不均匀,还有就是场景容量受到单个进程处理能力极限的局限。第二种方案延迟肯定大过第一种,但是场景的容量不受单个进程计算能力的影响,扩展性更好一点。
最后我决定用Python来实现这个想法,一,有丰富的基础框架可以选择,比如用高性能的Gevnet或者Eruasia来作为TCP Server的基础。二是Python本来就是动态的,所以不需要再嵌入另一个动态语言来适应多变的业务逻辑了。其三是Python是动态类型的,在实现消息服务的时候更方便。本来打算用erlang的,不过暂时对OTP还不是很熟悉所以暂时作罢,不过如果采用第二种方式的话我可能会用Erlang来实现消息服务器。