最近被指派去优化服务器性能,因为在目前情况下,DEBUG版本服务器在城战的时候FPS从100直降到0.33,导致完全不能正常的debug。
经过Vtune以及其他手段的分析,最后确定瓶颈是在,NPC在搜索目标的开销。实际上,关于搜索本身就是一个比较大的学问。服务器现在的做法是,用网格优化,直接取一个范围内的网格,网格内保存了当前所在的NPC的ID,每个ID 16字节。在城战情况下,因为NPC密度比较大,所以每个NPC平均能搜索到大概40个对象,然后AI需要从40个对象中选取符合标准的,距离最近的NPC作为攻击目标。一共有750个NPC,所以开销大概是每帧拷贝数据量16字节 * 40 * 750 = 480K。按理说,每帧拷贝480K是完全完全完全不应该成为一个服务器的瓶颈的。这里是非常让人困惑的。
因为AI是没有必要每帧更新的,第一,网络通信的延迟至少是几十毫秒,这几十毫秒里面至少可以跑5到10帧,所以AI就算每帧更新,玩家也不会有感观上的太大区别。第二,AI是由一系列状态变更来驱动的,一般来说,一个正常AI的状态变更频率不可能变态到每帧都有,所以大部分AI的更新都没有变化可言。第三,AI的变更一般是由回调来被动通知,所以AI的更新一般只是为了持续维护一个状态,所以每帧更新除了浪费服务器资源外没有太多便宜可言。
针对以上,我们的服务器是用Timer回调来实现的。每个NPC都注册了一个Timer,Timer有自身的Interval,每次更新的时候,这个Timer会记录下更新时间。然后每帧Loop的时候,由timer管理器用当前时间去减去每一个timer的更新时间,如果大于了interval则进行一次调用。时间是系统绝对时间。这是一个相当简单逻辑的timer系统。其中也用多重链表进行了一些管理和优化。
经过了更多的排查工作,最后终于找到了这个隐晦的瓶颈。原因如下:
1. 攻城战的时候,每个NPC的AI timer大概是300毫秒一次回调(这个其实是比较可以接受的interval)。
2. 750个NPC在城战开始的一瞬间,同时启动AI。并进行搜索逻辑。这个逻辑因为比较复杂所以导致耗时超过300ms.
3. 因为第一帧耗时超过300ms,所以第二帧更新的时候,每个timer都过时了。于是每个timer又会回调一次,又是搜索。
4. 第三帧因为第二帧也超过了300ms,所以所有timer都有效,进而继续全部回调。又是搜索。
5. 这是一个恶性循环,导致了服务器每帧都会将所有的Timer进行回调,从而,timer失去了间隔的意义。也就是说AI每帧都会被回调。
仔细分析这个问题。我认为,是因为我们遗忘了使用timer的初衷。
我们之所以用timer是因为不想每帧都做AI运算,我们之所以要有interval这个值,是因为我们需要将运算按照一定的频率进行组织。
这里有个非常隐含的问题:如果所有的timer的interval都一样,会有什么样的问题?那就是interval触发的时候,服务器运算量最大,除此外,服务器没有AI运算。如果用一张时间轴来表示,那就是一张起伏超级大的表。
所以,我们不仅要考虑到,通过减低频率来降低服务器负荷,更应该考虑到要将负荷平均分配。频率只是一种减低负荷总量的手段,我们还应该要将负荷平均的分布在服务器运行过程中,也就是说,不仅要减轻服务器工作量,更要保证服务器的稳定。这才是这个瓶颈真正要暴露出来的问题。
最后优化方案如下:
1. 随机化每个interval而不再使用定值。以前的300ms定值改为300 - 400毫秒的随机值,这从某种程度上可以更加分散timer回调的时机。
2. 每帧Timer的时间增量(delta time)不再使用服务器绝对时间。而为其设定一个最大值。
第一点很简单也很容易理解。
第二点的意思是,一个interval为300毫秒的计时器,0毫秒的时候进行了第一次回调,如果第二帧检测时距离第一帧已经过了1000毫秒,我不会简单的用1000 - 0 > 300来认为需要回调这个timer,而是用一个定值来代替实际的1000毫秒。假设我用16毫秒这个值,那么我每帧检测距离上一帧的deltatime,如果大于16,那么我认为就只过了16.所以以上情况下, 我会在第二帧做 1000 > 16 ? 16 : 1000的逻辑来判定,得出第二帧不会回调的结论。
当然优化的方式很多,也可以用任务队列(也是我比较欣赏的方法),只要保证任务能够均匀的分布在服务器运算过程中即可。
实际的效果还没有看到,因为timer系统是另外一个同事在维护。但是我觉得应该可以彻底解决服务器卡死这种不可容忍的症结,很期待最后的表现。
ps。最近马上就要完成之前提到过的可视化脚本系统了,工时大概在1.5人月左右。我很感谢老大在并不太清楚我在做什么的情况之下仍然支持我写这套东西,我相信这套系统也完全能对得工时。这也是我挺佩服老大的地方,个人认为他是一个很有战略性眼光的人,勇于接受一些新的观点和他人的意见,而不是墨守成规,尽管他自己其实已经是一个牛逼的程序员,但是他从来没有停止过接受新鲜的意见和建议。我自己也是一个非常乐于听取不同意见的人,因为只有这样自己才能得到成长。反观一些没见过牛逼的东西而局限在自己眼界内的领导者,有时候真是不知道如何与他们沟通。嗯,加油吧。