过去一年中,花了很多时间在考虑服务器架构设计方面的问题。看了大量文章、也研究了不少开源项目,眼界倒是开阔了不少,不过回过头来看,对网游架构设计方面的帮助却是不多。老外还是玩儿console game的多,MMO Games方面涉及的还是不如国内广泛。看看 Massively Multiplayer Games Development 1 & 2 这两本书吧,质量说实话很一般,帮助自然也很有限。当然这也是好事,对国内的研发公司/团队来说,在网游服务器技术方面当然就存在超越老外的可能性,而且在这方面技术超越的机会更大,当然前提是要有积累、要舍得投入,研发人员更要耐得住寂寞、经得起诱惑,在平均每天收到超过3个猎头电话的时候——依然不动心。
上面有点儿扯远了,下面聊聊无缝世界架构(Seamless world server architecture)设计方面的一点儿看法。
先说架构设计的目标——我的看法,服务器组架构设计的目标就是确定各服务器拓补关系和主要的业务逻辑处理方法。主要要解决的问题就是在满足游戏内容设计需要的前提下,如何提高带负载能力的问题。
最简单的架构就是基本的C/S架构,一台Server直接构成一个Cluster,所有Client直接连接这个Server,这个Server完成所有逻辑和数据处理。这架构其实很好,最大的好处就是它架构上的 Simplicity ,Cluster内部的跨进程交互完全被排除,复杂度立刻就降下来了,而且——完全可以实现一个无缝(Seamless world)的游戏世界。但是即使我不说,大家也知道这种单Server架构会有什么问题。不过我们不妨以另外一个角度来看这个Server——一个黑盒子。从系统外部的角度来看,什么样的系统都可以看成一个整体、一个黑盒,而不管系统内部的拓补关系和实现复杂度方面的问题。在不考虑这个系统的实现的前提下,理论上Cluster的处理能力就是由硬件的数量和能力决定的,也就是说一个Server Cluster内包含越多的服务器、服务器越‘快’,那么这个Cluster的处理能力越好、带负载能力越好。那么我们要面对的带负载能力的问题,就是如何高效的利用这些Server的问题,基本上也可以理解为如何提高玩家请求的并发处理能力的问题。
CPU厂商在很久以前就在考虑这方面的问题了,CPU其实也可以看成个黑盒。看看他们用过的技术——流水线(pipeline)技术、多CPU/多核(multicore)技术,以及这些技术的衍生技术。我想了很久让 Server Cluster 内部处理并行的方法、并且有了比较清晰的思路之后,才发现其实早就可以参照CPU厂商的方法。流水线的方法就是把一个指令处理拆分成很多个步骤,这样指令的处理被分解之后就可以部分重叠(相当于变成并发的了)执行。我们的Server Cluster一样可以用这种方法来拆分,我想了个名字——
SERVICES-BASED ARCHITECTURE——基于服务的架构。在这种架构内部,我们根据处理数据、逻辑的相关性来划分组内各个服务器的工作任务。例如:位置服务提供物体可见性信息、物品服务处理所有物品相关的逻辑、社会关系服务提供行会家族等等方面的逻辑、战斗服务器只处理战斗相关的逻辑,等等。这样划分的话、逻辑处理的并发就有了可能性。举例来说:A砍B一刀这件事情与C从奸商手里买到一件武器这个事情是完全不相干的,而且这2个请求本来就在不同的服务器上被处理,他们是被不同的Service Server并发处理的。这就是 Services-based Architecture 的并发方法。
基本上,把游戏逻辑的处理拆分成一个个的service,就和设计cpu的时候把机器指令的具体处理拆分,然后设计出一个个流水线单元是一个道理。
CELLS-BASED ARCHITECTURE——基于cell的架构。每个cell都在不同的物理server上面运行着完全一样的应用程序服务器,但是他们负责承载不同的游戏场景区域的游戏逻辑。和 services-based arch. 明显不同的就是,每个cell都是个‘在逻辑上完整的’服务器。它得处理物品操作、人物移动、战斗计算等等几乎所有的游戏逻辑。尽管这么做会带来一些(可能是很复杂)的问题,但是它完全是可行的。举例来说:在吴国A砍B一刀显然地和千里之外在越国的C砍D一刀不搭界,他们完全可以被不同的Cell并发地处理。
基本上,这就相当于一个主板上面插多个CPU或者一个CPU但是有多个内核,每个CPU能做的事情都是一样的,而且能一起做。
从一组服务器的角度来看,一般来说,我们的服务器组(Cluster)内都会有登陆验证服务器(Login Server)、持久性数据服务器(DB及DB Proxy)、连接代理服务器(Gate Server、FEP Server、Client Proxy等)以及Auto Patch Server、还有用于集中管理及控制组的服务器等等,由于这些服务器基本上什么样的架构设计都会用到,所以——现在不考虑以上这些服务器,只考虑具体处理游戏逻辑、游戏规则的各个服务器。以此为前提来分析一下 Services-based Architecture 和 Cells-based Architecture 的优缺点。
对Services-based Architecture 的分析
基于服务的架构,顾名思义这种架构的实现(程序)会是和服务的具体内容(策划)相关的,这是因为——各种【服务】内容的确定是建立于项目的【需求分析】基础上的,【需求分析】的前提是基本确定了【策划设计】,至少是项目的概要设计。
我想多数做过游戏项目的人都应该对需求变更 有很深的感触,每个人都说“开始想做的那个和最后实际做出来的那个不一样”。特别是在项目的早期阶段,团队的不同成员对项目做完之后的样子有相当不同的看 法(很可能大家互相都不知道对方怎么看的),这很容易理解,谁也不可能从几页纸几张图就确切地知道这个游戏做完了什么样子,即使不考虑需求变更。涉及到项 目开发方法方面的东西这里就不多说了,总之我的看法就是——尽管我们不大可能设计出一个架构能够适应任何的游戏设计,但是不同开发任务间的耦合度显然还是越低越好,基于服务的架构适应需求变更的能力较差。
关于服务耦合
不管如何划分service,不同 service之间都一定存在不同程度的耦合(coupling)关系,不同的 service 之间会有相互依赖关系。而你们的策划设计可能会让这种关系复杂到程序在运行时的状态很难以琢磨的程度。
假设:
服务器组内的战斗处理和物品处理分别由两个不同的服务(器)提供
游戏规则:
人物被攻击后自己携带的物品可能掉落到地上
某些物品掉落后会爆炸
物品在地上爆炸可能伤及周围(半径10米内)人物
人物之间的‘仇恨度’影响战斗数值计算
被攻击时掉落的物品爆炸后伤及的人物,会增加对‘被攻击人’的‘仇恨度’
我想我还能想出很多很多“看上去不算过分”的规则来让这个事情变得复杂无比,很可能你们的策划也在无意中,已经拥有我这种能力 :) 而 且他们在写文档时候的表达还多半不如我上面写的清楚,另外,他们还会把这些规则分到很多不同的文档里面去写。好吧,你肯定会想“把这两个服务合二为一好 了”,实际上不管你想把哪两个(或多个)服务合并为一个服务的时候,都应该先考虑一下当时是为什么把他们独立为不同服务的?
实际上很多这样“看上去不算过分”的规则都会导致service间的频繁交互,所以每个service最好都是stateless service,这样的话情况会好很多,但是对于游戏来说这很难做到。
请求处理的时序问题
服务耦合的问题在不考虑开发复杂度比较高的情况下,还是可以被搞定的,只要脑袋够清醒,愿意花够多的时间,那么还有更难以搞定的么?我看确实还有,如果你对将要面对的问题,了解得足够多的话:)
上面两个序列图描述的是某个玩家做了连续做了两次同样的操作但是很可能得到了不同的结果,当然这些请求都是异步地被处理。问题的关键在于——尽管两次玩家执行的命令一样、顺序一样,甚至时间间隔都一样,但是结果却很不同——因为图(1)里面C2CS::Request_to_attack请求被处理的时候,C2IS::Request_equip_item 这个请求还没有被处理完,但是图(2)显示的情况就不一样了。因为C2IS::Request_equip_item这个操作很可能会改变游戏人物的属性,这个属性又很可能影响attack的结果。这两幅图实际上省略了 Combat Server 与 Item Server 之间的交互过程。但是已经足以说明问题了,每个Service处理每个Request时具体会消耗的时间,是无法在设计时确定的!
谁喜欢这类结果上的不确定性?举个例子:玩家很可能已经装备上了“只能使用1次的魔兽必杀刀”然后攻击了一下魔兽,但是它却没死!这会导致什么样的结果?请自行想象。另外,这种不确定性还会表现为“在项目开发期和运营期的行为差异”,或者“出现某些偶然的奇怪现象”。
那还有解决方案么?有的,其实只要序列化玩 家请求的处理,使处理有序进行就可以了。但是又一次的,这会带来新的复杂度——在某个范围(整个服务器组?一个行会?一个队伍?)内,以每个玩家为单位, 序列化他(们)的(可能是所有)操作,但是也显而易见,这在某种程度上降低了请求处理的并发性,尽管它对并发性的影响可能只局限于不大(最少是一个玩家) 的范围。
对Cells-based Architecture 的分析
基于Cell的架构有个明显的优势就是Cell如何划分和你的策划没有关系J这是真的。而且Cell间如何交互可以被放到系统的底层,具体有多底层、多隐蔽(实际上可以隐蔽到对开发上层游戏逻辑的程序员都不可见的程度)要看你的实现如何了。如果做到了某个系统的程序设计与游戏设计完全无关的话,显然,这个系统受到游戏设计变更(需求变更)的影响就会很小很小,甚至会到完全不受影响的程度,当然这是理想情况。
关于跨边界对象交互
在基于Cell的服务器架构里面,实现无缝世界(Seamless World)的主要难点在于实现跨边界对象的交互时会出现的一些问题,因为这些对象在不同的Cell进程里面,这些Cell一般来说是在不同的物理服务器上运行。
无缝世界的特点自然就是无缝,并且因为无缝给玩家带来更好的游戏体验,所以显然我们希望“跨边界对象交互”问题不把事情搞砸,那么这种交互的表现就必须满足稳定、高效的前提。一般来说,高于300ms的延迟对玩家操作来说就属于“明显可见”的程度了,不能让玩家骑着500块RMB买来的虚拟马在一片大草原上面畅快的奔跑的时候,在某个地方突然就被“看不见的墙”给“挡”了一下,因为这“墙”根本看不见,所以会很影响“上帝”的游戏心情。
关于组成整个虚拟世界的Cell之间的关系,下面来分析两种情况:
一, Cell 承载的场景不重叠
如图(1),一个连续的虚拟世界场景被分成左右两块,分别在不同的Cell Server上面运行。A、B、C分别是3个不同的游戏角色。在这种情况下B与C的交互并不存在任何障碍,因为B和C只不过是同一个物理服务器上同一个进程内的两块不同的内存数据而已。但是A与B/C的交互就不那么直接了,尽管他们所在的场景看上去是“连续的、一体的”但是事情不会像表面上那么简单。A与B发生交互时候会发生什么事情?例如A攻击了B、A与B交易物品等等,因为在这种结构下做数据同步会带来很多问题,例如对象状态不确定性、开发复杂度等等、相对来说两个Cell Server之间做网络通讯而带来的延迟可能反而是最小的问题,这些问题不需要很复杂的分析就可以得出结论,在此不再多说了。
二,Cell 承载的场景(部分地)重叠
如图(2),一个连续的虚拟世界场景被分成左右两块,分别在不用的Cell Server上面运行。A、B、C、D分别是4个不同的游戏角色。这个情况下,中间的区域为2个Cell所共同维护,中间区域的对象同属于2个Cell所‘拥有’。这有什么好处?现在,任意两个对象之间,除了A与C之间的交互,都变得更‘直接’了。变得直接肯定是一件好事儿,那么A与C之间呢?他们之间其实也没有任何问题J 因为双方都已经超出了对方的Area of Interest(AoI)区域,游戏规则可以限制他们不能直接交互。
上面提到的第二种方案算不上什么魔法,但是肯定是比第一种方案更有效。接下来怎么办?假设B是个玩家,他站在中间这块区域上面时,并不会产生“我到底是在哪里”这样的疑问J 问题的关键在于对于Cell Server来说,怎么样同步那些处于重叠区域对象的状态。游戏世界内的对象可能同时处于1个、2个、3个或者4个不同的Cell Server。如果你的Cell分隔方法不限于水平线和垂直线、或者有人故意捣乱的话,还可能会更多。需要被同步的对象也不只是玩家本身,还包括怪物、NPC、一颗会走的树、某玩家在地上吐的痰等等。
由于我们的基于无缝世界的游戏规则不大会直 接去限制游戏世界某处玩家的行为,也就是说玩家如果能相互交易物品的话,他们肯定希望在任何地方都能交易,“为什么其他地方都行,但是在某个墙角做交易就 会导致物品丢失?”所以比较可靠的方法是建立一套的用于同步的底层机制,来同步这些跨边界对象。
怎么实现?这个话题很大,恐怕再写几篇Blog我也讲不完,但是有一些东西可以作为参考,例如:DCOM和CORBA规范,Java的RMI,基于Python的 PYRO,TAO(The ACE ORB)等等。好在分布式处理的问题不止是网络游戏会涉及到,可以借鉴的东西还是很多的。
总结
很显然,这篇文章在两种架构的评价上面存在某些倾向性,但是倾向性本身只是副产品。另外一个副产品就是关于一些技术分析方法。
在考虑采用何种技术的时候,我们往往很容易地就会忽略对程序之外那些事情的影响。上面我提到的关于Services-based架构实现的时候,提到划分service及数据设计对程序设计能力的挑战、对策划设计的制约,对适应需求变更能力的影响,都不会只是空谈。这些问题也不是只在实现这种架构的时候才出现。
不要高估自己的智商,Keep It Simple and Stupid :) 应该可以让我们离成功更近一点儿。
作者:JerryWang