【开发日志补全计划】Gridlock小组、名字对象、以及单用户CPU占用率
原作者:CCP Masterplan
原文:http://community.eveonline.com/devblog.asp?a=blog&nbid=2326
我们的Gridlock小组一直致力于解决游戏中的延迟卡机问题以及改进游戏编码。稍早前我们对正式服务器进行了一些优化,现在几周过去了,我们也能够开始衡量优化带来的结果了。在最近举行的峰会上我们将这些结果与星际管理委员会(CSM)的成员们做了分享与交流,之后我们决定将其公之于众。
首先,我们来看个图表,大家都爱看图表不是吗?

图表展示的是8周内EVE世界服务器上单用户CPU占用率的变化曲线。纵轴可以表示任意单位,但是它所代表的硬件指标总是相同的。这条曲线表示的是特定时间内每位在线用户的CPU占用率,这个数字自然是越低越好。红色的横线代表的是我们对TQ改进之前及之后CPU占用率的变化趋势,而蓝色的线代表的是TQ改进的日期。我们以在编码中实际使用的标签名称来命名这些更改(本篇开发日志是基于我们的内部报告写成的,所以我尽可能使它简单易懂一点)。
每一个标签代表着一次特定的优化,这个过程并不需要给服务器打补丁。这一点很不错,因为我们可以先将标签转化为一个节点子集并且密切监控未知错误的发生,之后再将其在整个服务器范围内实行。
标签(flag)
4月26日,我们添加了一个服务器标签'ballparkUsesInventorySelfLocal'。之后,整个服务器的单用户CPU占用率下降了大概8%。这是个很不错的结果,因为实际上我们只对代码做出了很小的改变。(我们还要做许多工作来评估这种变化,之后会进行一些测试和分析来验证。)
5月2日,我们添加了另一个服务器标签'crimewatchUsesInventorySelfLocal'。它并没有实质性地改变单用户CPU占用率,不过这也是意料之中的。这个标签的初衷是只在舰队会战及低安地区等特定情况下发挥效用,所以并不会对整个服务器的平均表现产生很大影响。
如果你只是想知道我们在改进延迟方面取得了什么进展,那你就不必往下看了,上面的图表已经说明了一切——8%的性能优化。如果你还想知道我们具体改变了哪些,那么继续往下读。
名字对象(moniker)小科普
这两项改变都遵循着同一个主题:以限定对象(bound-object)代替名字对象,并且与该对象建立直接引用。这是什么意思呢?那么,好好听着,我要解释很多内容,并且会告诉你为什么这样处理是好事情。
服务器各部分之间的通信机制是通过名字对象和限定对象来实现的。名字对象作为限定对象的“开关”,而限定对象作为一个部分的前端,负责追踪有多少个名字对象指向它。这个基本上就是一种代理服务器的设计模式。
有趣的是,名字对象和限定对象可以处于不同的进程中,甚至可以处于不同的服务器节点上(EVE世界服务器集群现有大概200个节点)。事实上,它们甚至可以分处地球的两端——你的EVE客户端使用同样的工具与我们服务器上的对象进行互动。程序员们把这种现象认为是RPC机制的一种具体体现。这一点很不错,它意味着我们可以将服务器的逻辑部分分布在不同的节点上,它们可以通过一个程序界面互相通信,就像它们都在同一个节点上一样。名字对象会占用一定的系统资源——与常规的函数调用相比,每一个通过名字对象的连接都需要一些额外的检查工作,这是因为名字对象也能提供一些额外的特性,例如生命周期管理(如果任何一个连接终端被关闭了,或是某个对象被破坏掉了,会发生什么事情呢?)、调用同步(举例来说,我可以保证对同一个限定对象来说,不会同时出现两个调用)、以及每个函数的许可控制(任何用户都可以调用函数A,但是只有GM才能调用函数B)。

上面的图表呈现的是一个普通对象'ServiceObject'接受两个玩家对象连接的情况。每个连接都是通过名字对象进行,而所有的名字对象连接都通过一个限定对象界面处理。黑色的箭头表示逻辑连接,可以是在同一个存储空间内,也可以是在同一本地网络的不同终端中,甚至还可以是通过互联网连接。
付诸实施
在服务器上,每一个恒星系都由几个相互关联的部分负责处理,本文涉及到的三个部分分别称为Ballpark、CrimewatchLocation以及InventoryLocation。具体到每个恒星系,我们可以举具体实例说明:Ballpark负责处理太空中的物品(例如Destiny物理引擎、向客户端发送状态更新、通过星门跳跃等等)。CrimewatchLocation负责追踪罪犯标记,战斗规则、击杀记录以及统合部NPC生成。InventoryLocation负责追踪物品的位置,并作为物品数据库的前端而存在。
特定恒星系的InventoryLocation部分可以通过名字对象从任何其它节点获得,你可以知道它的它所在的节点的ID,并且可以获得对应的权限。Ballpark以及CrimewatchLocation系统便是利用此功能,通过一个名字对象接入InventoryLocation的。

这个图表展示了Ballpark、CrimewatchLocation和InventoryLocation三个对象在改变之前及之后的连接关系。之前,由于与其它对象的共同依赖性,名字对象连接被限定为相同的方式。
这很不错。我们最初的构想是将每一个部分都移到它自己的节点中,然后试图通过平行机制实现运行。但是,在任何一个恒星系中,这三个部分总是处在一个节点中的。随着时间的推移,他们通过其它非名字对象的部分生根发芽,彼此紧密联系在了一起。那么,把他们分离开来就成了主要问题。实际上,Ballpark、CrimewatchLocation和InventoryLocation三个部分已经像连体婴儿一样密不可分。因此,如果不做重大结构性调整的话,它们之间联络的消耗,将抵消平行机制所能带来的效率提升。
我们仍然想要重构并消除这些连接,但是我们也可以用其他事半功倍的方法。
修正
因此,我们将这三个部分通过RPC机制联系起来,它们就会一直处于同一个地方了。这样做的话,就需要注意名字对象所带来的额外效应,我已经开始着手去除这个中间步骤,将名字对象变为一个足够简便的直接引用地址——它已经可以实现这个功能。
我们做的改变如下:
之前:
初始化:
# Get the moniker to an inventory location:
inv = GetInventoryMoniker(solarsystemID)
运行时:
# Use the inventory to get stuff from the DB:
item = inv.SelectItem(itemID)
之后:
初始化:
# Get the direct reference to an inventory location:
inv = GetInventoryMoniker(solarsystemID).GetSelfLocal()
运行时:
# Use the inventory to get stuff from the DB:
item = inv.SelectItem(itemID)
就这些,我们多加了15个字符,禁用了一个功能,为服务器节省了8%的效能,不赖吧?
对“修正”的修正
不过等等!还记得我提到过的事吗?名字对象除了提供RPC机制之外,还能做些其他有用的事情,比如引用计数的周期管理什么的。限定对象只是因为名字对象对其的引用而存在,名字对象被移除了,那限定对象也不复存在了。这个是我们在测试时发现的问题。结果,InventoryLocation只是因为CrimewatchLocation对它的名字对象而存在,这会导致一些奇怪的Bug。举个例子,如果某些玩家带有犯罪标记的话,那这个恒星系可能没什么问题,但是如果没有这样的玩家,CrimewatchLocation也就没什么事情做了,就会导致InventoryLocation关闭,尽管这个时候Ballpark可能还在通过直接引用在使用它。
我们发现并修正了这个问题之后,还进行了一系列的测试,并且将它应用在了测试服务器上一段时间,以找出一些特殊情况。
结论
在结束了测试并对结果感到满意之后,我们以一种谨慎的方式在正式服务器上面添加了这些修改。4月底的时候,我们不太忙(因为没有热修复以及更新发布),于是我们就开始着手修改,首先在4月26日加入Ballpark,然后在5月2日加入CrimewatchLocation。我们选择在这个时候进行修改是因为没有版本等其他方面的更新,我们可以单独地观察修改的效果以及可能存在的不良影响,而且也比较容易将其恢复原状,只要禁用其中一个优化标签就可以。
几天之后,单用户CPU占用率的数字看起来不错,我们也没有遇到负面问题。几周之后,我们有了足够的数据,并最终得出了8%服务器效能优化的结论,只是多加了30个字符而已(没有统计对一些对象周期性问题的修正),这很不赖吧?
这个故事只是Gridlock小组的日常工作的一个例子。我们还做其他一些工作,诸如各种测试、借鉴其他团队的好点子以保证我们不会走下坡路、以及对未来的规划,比如改进实时的节点重分配(如果某星系中发生了舰队会战的话,我们就把它移到一个专用节点上去)和时间膨胀等。