负载均衡
幸运的是,负载均衡这门学问虽然玄奥,却并不是设计师需要直接操心的事。不过,技术专家最终采用什么方案、会带来什么后果,设计师还是得心里有数——尤其是因为碰上这件事,多半会有人来征询他们的意见。
理想情况下,虚拟世界应该跑在一台性能极强的计算机上。文字世界已经是这样了 1。图形世界可能也会走上同一条路 2,但就目前而言,服务器端新添的算力仍然有很多地方可花:
增加每个化身里同时存在的玩家数量(100,000 名玩家分布在 50 台服务器上是一回事,100,000 名玩家挤在 1 台服务器上又是另一回事)。
提高虚拟世界运行时的细节精细度(例如在雪地上留下脚印,脚印会随着雪融化或新雪落下而渐渐消失)。
改进虚拟世界居民的人工智能(质量与数量两方面)。
因此,在相当长一段时间里,虚拟世界很可能会把抛给它们的额外算力统统吸走。便宜的机器仍然会成集群使用,因为用 8 台算力为 X 的计算机,总比用 1 台算力为 8X 的计算机更省钱(不过,随着新机器不断加入,管理开销会拉低整体效率,所以没人会用 64 台算力为 X/8 的计算机) 3。
那么,既然分布式服务器注定会长期存在,这对设计师有什么影响?
好吧,虚拟世界太大了,装不进一台计算机,所以必须划分到好几台计算机上。
算是吧……
如果虚拟世界是惰性的,也就是说,里面什么都不会发生,那它根本不需要任何计算机,只要待在一个磁盘组里就行了。只有当里面有事发生时,它才需要计算机。所以,问题就在于确保活动分散到各台子服务器(sub-server)上,让它们都能应付自己手上的工作;换句话说,计算负载必须以均衡的方式分担(“负载均衡”这个说法就是这么来的)。
虚拟世界里最大的活动来源是玩家群体。玩家只要身处虚拟世界,就无时无刻不在与它交互。仅仅是从一个地点移动到另一个地点,就得通知所有(在虚拟世界里)能看见你这么做的其他角色。没错,有些活动比其他活动更耗资源,但它们往往并不专属于某种玩法。问题在于正在执行的动作数量本身,而不在于单个动作的效率。因此,虚拟世界里的负载均衡,通常就意味着确保集群里的每台子服务器都连上大致相同数量的玩家。
最直白的做法,是把每个新接入的玩家分配给当前玩家数最少的那台子服务器。然而事实证明,要在不引入巨大开销的前提下做到这一点,相当困难。为了反映某个动作而更新实例化数据库时,必须锁住相关记录,以免其他子服务器同时改动它们(例如两个角色同时试图捡起同一个物件)。子服务器需要锁住它在一次动作中可能用到的所有记录,执行检查以确保该动作可行,发出必要的更新请求,然后解锁这些记录。这里的加锁/解锁操作非常多。要是有办法把实例化数据库里的某些记录长期占住,又不必交出对它们的控制权,那就太方便了。有没有哪几类记录可以轻松做到这一点呢?
从统计上看,玩家执行的大多数动作都与移动有关 4。
玩家想把角色从 A 移到 B,子服务器就得锁住地点 B,检查它是不是空的;如果是空的,就把玩家移进地点 B,然后解锁。地点 A 也必须锁住,这样,那些以为角色还在地点 A、想对它做点什么的人(例如把角色传送到地点 C),就不会在不经意间把事情搞砸。许多其他常用命令(尤其是 get/drop 以及跟通信有关的命令)同样是基于地点的。
出于这些原因,服务器通常按(虚拟的)地理界线来划分对虚拟世界的职责。子服务器可以长时间锁住实例化数据库里的地点记录;事实上,只要系统编写得正确,它根本不必给这些记录加锁——仅仅因为别的子服务器都不这么做,它就已经获得了隐式的控制权。
把到目前为止的论证归纳一下:我们希望把玩家尽量均匀地分散到各台子服务器上,可是直白的做法会给数据库访问带来太多开销;效率高得多的做法是按地理位置来划分。问题在于:用这种划分模型,能不能得到负载均衡?
答案是:能。至于具体怎么实现,则取决于虚拟世界本身。
基本上有两种做法:固定负载均衡与动态负载均衡。前者以《无尽的任务》(EverQuest,EQ)为代表,把一块预先划定的地理位置(也就是一个分区,zone)交给一台子服务器负责 5;后者以《阿瑟龙的召唤》(Asheron’s Call)为代表,在子服务器之间转移对各地理位置的职责。
固定负载均衡:
更容易实现。
可以拆分实例化数据库,并让它留在子服务器本地,从而提高效率。
允许客户端提前判断需要哪些纹理贴图,并预载入显卡内存。
动态负载均衡:
地形无缝(能一直望到地平线 6)。
边界不是实体边界(追着你的怪物不会卡在分区边缘)。
对负载的均衡效果更好。
这一切对设计意味着什么?
这是技术反过来给设计设限的一个例子。采用固定负载均衡时,可以把各个分区做得更有各自的个性:它们之间那些“实体”屏障,让相邻分区之间的差别可以非常悬殊。玩家从一个分区走到另一个分区,可以指望在另一头看到不一样的东西。而在无缝的世界里,这种反差必须有更充分的道理,否则就会显得格格不入。
怪物之类的内容无法越过分区边界,这意味着玩家在这种世界里的战术,会不同于在另一种世界里的战术——在后一种世界里,他们要是把某个生物惹得火冒三丈,那东西随时可能穷追不舍。
维多利亚时代伦敦的警察(在法律上)不能越过辖区边界:开膛手杰克(Jack the Ripper)在一个警区杀了人,再跑进另一个警区,警察就不许追过去;要是他们能追,他就得重新考虑自己的逃跑策略了。
分区式世界在“快闪人群”(flash crowd,指人们看到有意思的消息就瞬间一齐出现在相近的地方;这个词出自拉里·尼文 1973 年的一篇短篇小说 7)上有些麻烦。大多数时候,每台子服务器处理的玩家数量都差不多。但有时候会出点事,让所有人都想往同一片地方凑 8。也许是一头气派的龙罕见地刷了出来,也许是婚礼、公会集会之类的社交活动。不管是什么,总之突然之间,想进某台服务器的人就超过了它能承受的数量。遇到这种情况,设计师必须决定怎么办。人们想进去时,就直接给他们显示一条“分区已满”的消息?还是放他们进去,让客户服务部门去应付随之而来的延迟投诉?又或者,把虚拟世界的虚构设定安排成:所有人都想挤进同一个分区,本身就是说不通的?
《卡米洛特的黑暗时代》(Dark Age of Camelot,DAoC)把它的玩家群体划分成三个“界域”(realm)。一个界域的成员不能进入另一个界域的地盘。不过,他们可以进入两个界域之间的无人地带,界域对界域的战争就发生在这里。由此带来的结果是:除非 DAoC 的玩家群体分布严重失衡,否则任何一个界域里通常只有三分之一的在线玩家。这对负载均衡来说再好不过。而且,界域是分区的集合,它们本身并不是分区。每个界域由 13 个户外分区(64K × 64K 的方格)、5 个地下城加 1 座城市组成。一台子服务器没有任何理由只能处理来自一个界域的分区;如果(出于某种原因)75% 的玩家都在同一个界域里,那么负责该界域的那些子服务器虽然从这个界域分到的负载很重,但它们同时还管着其他(更稀疏的)界域,从那些界域分到的负载自然就更少。
《无尽的任务》的玩家能注意到子服务器是如何按分区分担责任的,而 DAoC 的玩家注意不到。为什么注意不到?这要归因于后一款游戏的世界设计。
直到我把《MUD2》那台 33MHz 的服务器换成一台气派的 50MHz 新服务器,处理器速度才终于不再是《MUD2》的问题。 ↩︎
《子午线 59》(Meridian 59)采用单服务器架构,因此上线时每个化身最多只能容纳 200 名玩家。《魔剑》(Shadowbane)也是单服务器架构,但跑在性能略强一些的硬件上。 ↩︎
其实还没有人试过。虽然这种并行处理架构在许多商业计算应用里经常出问题,但对虚拟世界的某些划分方式来说,它也许没问题。 ↩︎
《MUD2》里这个比例超过 50%;图形化的虚拟世界还要更高——90% 甚至更多——因为在图形世界里,玩家要去任何地方都得走更多步。随着点击式界面的出现,这一点正在改变:你想去什么地方,点一下那里就行,不必再朝那个方向指过去。 ↩︎
应当指出,一台子服务器可以同时处理不止一个分区。 ↩︎
采用固定负载均衡的世界,如果像《网络创世纪》(Ultima Online)那样由许多地块拼成,也可以让跨服务器的边界看不出来。不过,交互跨越服务器边界时,可能会出现怪事(例如把箭射过边界)。在采用动态负载均衡的《阿瑟龙的召唤》里,互相发生交互的角色会被移到同一台服务器上,以减少跨服务器通信造成的混乱。 ↩︎
拉里·尼文,《快闪人群》(Flash Crowd)。拉里·尼文,《马的飞行》(The Flight of the Horse)。纽约:巴兰坦出版社,1973。 ↩︎
如果人群聚得特别集中,无缝世界也会遇到同样的问题。这种情况比分区式世界少见,但也正因为少见,一旦发生就更难处理。 ↩︎