实时万人国战 《御龙在天移动版》服务器性能优化启示录
《御龙在天移动版》是一款3D MMORPG手游,以三国为背景,移植《御龙在天端游》经典玩法,主打手机上的实时万人国战,同时通过社交系统和主播语音来增进玩家间的交互。
游戏特点:合纵连横,多国万人同时参与国战;开放式场景,随时随地遭遇战;国家语音指挥,大量玩家集体行动。
二、性能优化背景
《御龙在天》手游的主打玩法是实时万人国战。
由于国战的参与人数非常多,国战的流程是分布在不同的地图场景中。玩家在不同的场景间进行切换时,需要给玩家带来良好的体验。同时由于涉及多场景,如何做到降低逻辑编码和正式环境部署的复杂度,提升正式环境扩容缩容的灵活性,考虑容灾效果和负载均衡,需要一个设计合理的架构和场景切换流程。
国战参与人数多带来的另一个问题是流量消耗大。由于国战期间,玩家是在地图场景中实时进行移动和战斗,大量释放技能,玩家视野和属性更新频繁,势必会有大量的网络包需要发给手机客户端。手机玩家对流量比较敏感,而玩家手机也参差不齐,大量的网络包也会给手机端带来性能和电量消耗,影响游戏流畅度。因此需要一些方法,对流量进行优化。
MMORPG游戏通常玩法和系统的数量都非常多,因此代码量也是非常大的。当出现性能问题需要优化时,如何从百万行级别代码的工程里,发现那些性能浪费最严重的代码,进行优化后,如何检验性能优化的效果,需要从工具、原因定位方法、优化策略几个方面入手来解决问题。
基于这些问题背景,本文将从四个方面来介绍御龙在天手游的服务器优化经验:
1.场景切换流程优化;
2.国战架构优化;
3.流量消耗优化;
4.代码级性能优化工具、原因定位方法、优化策略。
三、场景切换流程优化
3.1 背景介绍
《御龙在天》手游是一款分场景地图的MMORPG,不同国家的地图场景,可能会分布在不同的zone场景进程上。当玩家从一个场景进入另一个场景时,如果两个场景是分布在不同进程上,这时需要将玩家对象和相关数据、状态信息,从原来所在进程同步到新的进程上,也就是我们说的跳zone流程。在这个流程中,由于涉及到玩家所在进程的切换,在网络状况欠佳的情况下,可能会导致重连失败,影响玩家体验。尤其是在移动网络的环境下,受到运营商信号和网络切换的影响,增加了跳zone失败的几率。基于以上原因,需要对跳zone流程进行优化,以降低跳zone失败的几率,提升玩家体验。
3.2 原有方案
步骤如下:
原zone向目标zone发起占位请求;
目标zone向原zone回服占位响应;
占位成功后,原zone通知客户端断开原有连接;
客户端发起新的网络连接到目标zone。
方案缺点:
需要客户端参与配合流程;
客户端需要重新建立连接。
3.3 改进方案
步骤如下:
原zone向目标zone发起占位请求;
目标zone向原zone回服占位响应;
占位成功后,原zone通过agent,切换路由到目标zone的agent;
收到切换路由响应,数据同步到目标zone。
3.4 改进结果
目前项目使用改进后的方案,有2个优点:
不需要客户端参与和配合进行跳zone流程,客户端侧完全无感知;
不需要重新建立网络连接;
优化后的跳zone失败率,从千分之一下降到了万分之一以下,效果较为明显。
四、国战架构优化
4.1 背景介绍
国战是《御龙在天》手游的核心玩法之一。由各国国王发起国战,进攻方在指定国战时间内,逐一占领敌国的边境、阳平关、王城郊外,最后在王城内城杀死国家神兽,取得国战胜利。每一个地图都有一个要占领的目标点,进攻方通过杀死镇守每个地图的目标NPC,来占领该地图。若在指定时间内,守方成功守卫本国神兽,则守方获胜。国战的架构设计和优化需要考虑几个问题:
由于国战涉及到多个场景和地图,而不同的场景和地图可能分布在不同的进程上,如何设计架构,以便在逻辑开发和外网部署时,不会依赖于具体的地图,减少编码复杂度,同时也可以根据运营数据灵活调整进程和机器数量;
由于王城内城的场景地图,在非国战期间是无法进入的,通过什么方式来实现,可以达到既节约机器资源,又能较好达到容灾效果;
国战期间参与玩家人数非常多,大量玩家聚集在同一个地图和场景上,网络流量陡增,如何设计场景进程zone和网络连接管理进程tconnd的关系,使得在网络层面减少对机器的压力。
4.2 原有方案
场景进程zone与国家地图绑定,通过配置进行关联;
王城内城通过普通地图方式实现,需要配置绑定到某个特定zone上;
每个zone进程绑定2个tconnd进程,每个zone进程上的所有客户端连接都通过这2个tconnd。
缺点:
场景进程zone与国家地图绑定,在国战逻辑的开发时,对不同的逻辑需要做分支判断,是否是在对应的地图上;
王城通过普通地图方式实现,在运营后期单区人数下降的时候,缩减机器也必须保留王城内城的zone。容灾能力也较弱,如果王城内城的zone出故障,就会影响国战的流程,导致国战无法正常进行;
zone与tconnd是绑定的,在国战期间大量玩家聚集到一个zone上,会对对应的tconnd带来很大压力。
4.3 改进方案
场景进程zone与国家地图不绑定,通过world进程在启动时进行分配和创建;
王城内城通过副本的方式实现,在国战开始时,通过副本创建策略,动态选择zone进程来创建副本;
tconnd使用集群化模式,与zone进程不绑定,通过agent进程来路由消息;单区内所有zone进程的网络连接,根据负载平均分布在所有tconnd上。
4.4 改进结果
场景进程zone与国家地图不绑定,在国战逻辑开发时,不需要再根据地图来进行逻辑分支的判断。在运营后期缩减机器和zone数量时,也非常灵活。
王城内城通过副本的方式实现,可以灵活分配选择zone进程创建。同时也增强了容灾能力,在国战发起时,可以将王城内城副本创建在非故障的zone上。
tconnd使用集群化模式,与zone进程不绑定,玩家集中在某个zone上时,不会对tconnd造成压力。同时还可以减少对外的ip和端口数量。
五、流量消耗优化
5.1 背景介绍
在移动网络下,网络流量也是RPG手游面临的重要问题,特别在御龙在天国战类RPG手游中显得更为突出。御龙手游流量消耗的压力主要来自两个方面:a) 实时PVP,游戏场景实时更新,技能,属性,移动广播量大,发包频繁; b) 国战手游需要有宏大的场面,看到更多的人,但是视野大意味广播量大,广播包多。
国战PVP中,手机客户端会收到大量的技能包,视野包等,由于玩家手机参差不齐,手机上玩家对流量比较敏感,大量的网络包不仅会带来流量消耗,同时也会给手机端带来CPU,GPU消耗,影响游戏流畅度。
因此需要我们在实时PVP中降低手机流量消耗,让玩家不用担心流量消耗,可以放心的在手机上尽情享受万人国战的快感。同时通过降低网络流量,也能够有效降低手机客户端网络包处理压力,减少电量消耗,带给玩家更流畅的游戏体验。
5.2 流量优化措施
5.2.1 业务下行包合包
5.2.1.1 游戏下行包特点
发包频率高,单个业务包小。以一测国战数据为例,单个下行网络包有效载荷不足50%。
5.2.1.2 合包策略
tconnd网关增加下行缓冲进行业务包合并,当缓冲区大小超过配置阈值或者等待时间超过配置时间时,将缓冲区包发送到网络,多个业务包共用appolo头,合并后压缩。
对于需要实时下发的包,通过设置好实时下发标志,可以不用等待缓冲区满或者超时,直接实时下发。
5.2.2 技能流量优化
一测下行数据占比
从统计数据来看,包量和包个数占比前三位的分别是技能效果,属性通知,做技能包。属性通知通常是伴随着技能产生的,比如做技能中的BUFF对攻防的影响等等。通过对技能进行优化,可以有效降低网络流量和包量。
5.2.2.1 技能施法流程
5.2.2.2 技能优化措施
1.技能效果分场景去广播化处理,收敛广播包量
如果是死亡包,因为需要视野内所有人看到,此时的效果包需要在视野内进行广播。对于非死亡效果包,只广播给攻击者和受击者。对于非攻击和受击玩家,血量同步通过主动pick查询。
2.群战做技能设定广播阈值,到达域值广播降级
如果是位移技能,视野中玩家需要依赖做技能包更新对应玩家位置信息,因此该技能包需要在视野中进行广播。而对于非位移技能,在国战中大量人群聚集群战时,无需全视野广播,只需要选择部分玩家广播做技能包,已经可以体现出战场的激烈。具体措施如下:如果视野玩家不超过n人,做技能包按视野进行广播,如果视野中玩家超过n人,则随机从视野中挑选n个人进行做技能包广播(包含技能受击目标)。
3.技能效果和表现分离(一段伤害多段表现)
对于服务器下发的单次技能效果,客户端通过分段的形式表现多段伤害,这样可以在一次网络包的情况下表现出多次受击伤害效果,从而节省下发的技能效果包个数。
4.持续技能多段分时上报,去除按碰撞检测上报
对于多段技能,采用分时上报的方式而不采用实时碰撞上报,通过分时合并上报目标,减少技能攻击时上报的技能包个数。
5.2.3 移动流量优化
移动包降频率
降低客户端请求移动包频率,客户端行走预表现,增加服务器的移动误差容忍值(初始容忍n秒误差,大约m米距离误差容忍)
反作弊
加入移动反作弊,防止客户端外挂,作弊次数多,降低容忍值。
5.3 流量优化结果
经过以上介绍的流量优化方法和措施,在视野人数增加了一倍的情况下,点将测试相比于第一次测试的流量消耗情况也得到了较大程度的优化。属性包、技能包、技能效果包数量的占比,都下降为第一次测试时的三分之一到一半。同时,每秒业务包的数量减少了一半,每秒下行的流量减少为之前的大约四分之一。
六、代码性能优化
6.1 背景介绍
MMORPG游戏通常玩法和系统的数量都非常多,因此代码量也是非常大的。当出现性能问题需要优化时,如何从百万行级别代码的工程里,发现那些性能浪费最严重的代码,进行优化后,如何检验性能优化的效果,需要从工具、原因定位方法、优化策略几个方面入手来解决问题。
6.2 性能优化基础工具
要在茫茫码海中发现那些性能热点,处理掉,再检验成效,需要一些基础设施的支撑。御龙主要利用程序内部的性能统计系统以及一些常用的性能分析工具来发现代码的性能热点,使用灵活的机器人模型代码优化迭代测试,并利用监控系统发现运营环境中的不易觉察的性能毛刺。
6.2.1 程序内部性能统计系统
游戏服务器代码的执行一般有玩家行为驱动和服务器定时驱动两种方式:
由玩家行为驱动时,可以在消息包处理的统一入口处统计消息包的数量、大小、处理时间等信息。
由定时器驱动时,可以把定时器做成动态注册集中管理的方式,这样也可以在定时器执行入口处统计各个定时器的执行次数和执行时间。
所有消息处理时间和定时器处理时间的总和基本上就是整个程序的总执行时间。
构建程序内部性能统计系统的好处有几个:
有了各个消息包和定时器的执行时间,可以在业务层上进行调整尽可能降低cpu消耗,例如降低移动频率和服务器定时驱动频率等。
在使用性能分析工具对函数消耗进行排序时,会发现大量消耗时间很少的函数,形成长尾,不易逐一优化,程序内部的统计信息有利于在更高执行层次上进行整体优化。
另外,消息包和定时器的执行数量统计有利于建立机器人模型进行测试、对具体代码的总执行时间进行预估等,程序的总执行时间的统计结果也有利于程序根据cpu情况进行自动调整服务。
6.2.2 性能分析工具
良好的性能分析工具可以让性能优化事半功倍。御龙后台在性能优化中尝试过很多性能分析工具,包括gprof、oprofile、valgrind、perf、vtune等。使用哪种性能分析工具受到获取简便性、学习成本大小、是否满足性能分析需求等影响,各种分析工具之间有不少差异,需要根据实际情况来选择合适的分析工具。
选择性能分析工具时有几个因素需要特别考虑:
工具本身消耗的cpu大小。例如valgrind本身cpu消耗非常大,不适合使用于服务器负载较大时的性能分析,只适合于性能是线性增长的情况并作低负载测试。Oprofile、perf等受到系统内核支持的一般cpu消耗较低。
能否用于运营环境的性能分析。Gprof是使用编译时嵌入代码的方式,而且需要程序正常退出才能得到统计结果,oprofile需要重编系统内核支持,运营环境下使用不方便。
基于功能上的考虑。Gprof不能统计内核调用消耗,perf、vtune等基于硬件性能计数器的方式可以提供多种事件的统计分析结果,还有需要考虑可视化操作是否简便等。
6.2.3 机器人测试验证系统
服务器代码执行是玩家行为驱动的,像视野广播等性能消耗与玩家数量非线性关系的情况下,需要模拟大量玩家的群体性行为来测试代码的性能优化结果。
把机器人做到灵活可配置,可以根据程序内部的性能统计信息,配置机器人的登录数量、各消息包发送频率和外网一致,模拟运营环境的各种实际场景对优化结果进行迭代测试。
6.2.4 性能监控系统
除了国战、集体过边任务等可预见的场景会出现性能消耗高峰外,还可能在其它不易发现的场景中出现性能毛刺。Mmog游戏的功能非常丰富复杂,玩家群体行为难以完全预估,服务器机器数量有一两千台,这些因素都导致服务器性能毛刺发现困难。这时候就要通过加入服务器性能监控来发现潜在的性能问题。
机器整体的cpu负载情况以及程序内部的性能统计信息都可以上报到公司的网管平台,方便我们对各类性能信息做针对性监控。
6.3 原因定位方法
程序内部性能统计系统以及性能分析工具的运用,有助于我们更快发现程序的热点所在,但是并不能帮助我们定位到哪一行需要修改的代码,也不会告诉我们该如何作代码修改,因此借助工具以外,我们仍然需要积累代码优化的经验。
6.3.1 热点粒度
大多数性能分析工具都是以函数为单位进行性能统计的,有些分析工具如perf支持定位热点到指令级上,但是要求有较高的采样精度,需要系统内核的支持,并且带来较高的性能损耗,不适宜使用到运营环境上。以函数作为性能统计单位时,如果一个函数代码量过于庞大,就为定位性能热点到代码行带来困难,因此热点区域的代码适宜写成简短的函数。
当无法定位热点到函数中具体代码行时,可以利用程序内部的性能统计系统在函数中若干位置插入统计代码,根据统计日志逐步缩小热点区域的范围。
6.3.2 代码分析
确定热点代码行的另一个重要手段是仔细分析代码。一个函数成为热点的原因可能有很多,包括函数本身的指令数量多且调用次数多、高频调用带来的栈管理开销、磁盘操作、缓冲命中率低、分支预测失败率高、缺页、上下文切换等,需要根据具体代码仔细分析性能消耗原因。也可以使用perf等性能事件统计结果进行辅助分析。
6.3.3 经验数据
一行代码是否可能够成热点,可以通过积累一些经验数据进行计算确定。例如memset 1MB需要消耗多少时钟周期、各种时间操作函数消耗的时间、文件操作消耗的时间等。积累各种代码消耗的时间经验数据后,可以根据统计日志等信息得到代码的执行频率,再计算代码占用的cpu百分比。
6.4 代码优化策略
定位到性能热点并分析原因以后,需要根据具体代码选择合适的优化方向。下面将根据御龙后台优化经验谈谈常用的优化策略。
6.4.1 内存操作
MMOG后台充斥着大量数据,御龙zone进程的虚拟存储空间已经达到几G级别,各种数据的存储、修改、回写、大数据包发送等潜伏着大量内存操作,对大内存的频繁操作将会消耗较多cpu,因此内存操作的优化是经常使用的优化手段。下面以御龙遇到的其中一个内存操作的例子做说明。
在运营环境使用perf进行性能分析时发现,memset操作在所有函数性能消耗排名中排在第一位,其中技能定时器使用的memset耗时最多。
分析发现技能定时器中有定时发送持续伤害技能效果包的功能,在消息包字段填充前有memset整个包的操作,而由于这个包比较大,只要每秒发送几百个技能效果包,就占用整体cpu的10%。这样的技能包数据是比较容易达到的。
直接去掉memset可能隐含部分字段未初始化的风险,我们发现这个技能效果包的最大目标对象数定义为100个,而实际使用中是对单个对象使用的,最后我们通过缩小包的大小解决了这个问题。
6.4.2 时间操作
游戏服务器中经常会使用到一些时间操作函数,特别是在服务器定时检查和驱动的操作中,可能需要遍历每一个数据对象进行时间消逝检查,时间操作函数是比较耗cpu的,如果对象数很多,而且检查每个对象都调用localtime、time等获取时间的函数,就会浪费较多cpu。
对一些对时间精度要求不是很高的操作,可以在一个定时器中获取到时间并保存起来,其它需要使用时间的地方直接获取这个缓存的时间即可。
时间函数的不合理使用将会耗费大量cpu,下面以御龙的一个例子作说明。
我们在检查御龙国运时的性能统计日志时,发现一个任务相关的定时器处理耗时非常高,使得进程整体cpu在国运时也很高,使用vtune采集性能数据发现任务定时器中调用的一个时间相关的转换函数耗时占整体进程的60%以上:
使用perf分析,发现_IO_vfscanf_internal和__offtime两个函数耗时最高。但这两个函数是标准库函数,由于去掉了debug信息,无法看到调用栈情况。
使用pstack多次打印进程的栈信息,发现进程经常性执行到这两个函数上,并且是从这个时间转换函数中的__mktime_internal调用的。
因此热点的耗时原因就较为清晰了,因为国运玩法中需要不断定时检查是否任务超时,在计算消逝时间时调用了开发框架库中的一个时间转换函数,这个函数实现时调用了标准库的时候转换函数__mktime_internal,而这个函数在字符串操作和时间计算等较为耗时。
最后我们使用了自己编写的mktime时间转换函数来解决这个问题,它是通过计算与已知时间点的差异来实现的,效率提升在20倍以上,完全消除了这个性能热点。
6.4.3 cache-miss
由于游戏后台存在大量的数据处理,很容易出现cpu缓存的cache-miss,造成程序执行效率的下降。使用perf的stat工具分析御龙国战的zone性能,发现cache-miss情况比较严重。
这是与大量的内存数据引用和内存拷贝有关的。减少cache-miss的方法是使用局部性原理,减少存储器变量的引用,或者降低整个内存操作的频率,需要针对具体代码进行优化。
下面是cache-miss引起性能严重下降的一个例子。在分析御龙聊天服性能的过程中,发现在消息广播中一个非常简单的获取player身上一个字段的函数非常耗cpu。
把这个函数改成内联后,cpu有所下降,但是整体cpu还比较高。Perf stat发现进程的cache-miss情况很严重。分析代码发现聊天服上有几万个player对象,每次对一个国家范围进行广播时会遍历所有几万个玩家找到对应国家的玩家进行广播,以cache-miss消耗的时钟周期和使用的cpu主频来计算,每秒最高发几百个消息包,由于国战时文本消息广播数量很多,每秒过百的消息包广播请求是可能达到的。解决办法是对玩家进行分国家管理,减少广播时遍历玩家的数量。
6.4.4 分支预测
分支预测失败会破坏cpu的流水线操作,降低代码执行效率。从御龙zone进程的perf stat分析结果来看,该进程有较高的分支预测失败率:
可以使用perf工具指定branch-misses事件可以查看哪些函数的分支预测失败率最高并进行优化。御龙使用perf对非国战zone进行性能分析时发现,某个时间比较函数占用的时候排在第二位。
这个函数在branch-misses事件排序中排在前列。查看代码发现函数实现非常简单,但是里面包含一个计算和条件判断较多的分支判断,优化时我们把这个把函数里的分支判断去掉,由上层调用保证传入参数的正确性。优化后,这个函数就退出函数耗时榜前面位置。
6.4.5 文件操作
文件数据读写涉及磁盘操作,效率较低,这个容易预估。数据应该尽量在内存中进行缓存,减少频繁的文件存取操作。另外有些隐秘的文件操作也可能导致性能问题。例如zone的vtune分析结果发现某个函数占用cpu较高。这个函数是判断一个操作是否是敏感操作,对每一个上行数据包都会调用这个函数进行检查,函数中有判断一个含敏感操作列表文件是否存在的access操作。测试发现access操作在国战高峰中调用次数非常多。最后我们通过分析后优化掉了这个access文件操作,使这个函数的cpu占用下降到可以忽略了。
6.4.6 高热点代码精简
性能热点的存在可以跟代码本身的指令执行数量有关,因此精简热点函数的代码是非常有用的优化措施。
例如御龙的视野广播函数原来占用了较多cpu。在函数内部,遍历视野列表、获取视野对象、包发送等有较多安全性检查以及异常处理的操作,便这些安全性检查正常情况下不会出现,或者在其它代码中已经做了安全性保证,或者出现异常不影响后续逻辑的继续执行,所以可以直接把这个代码精简掉。由于视野广播执行很频繁,每次广播都要遍历整个视野代码,循环内的代码执行频次非常高,所以精简代码的优化措施非常有效。
6.4.7 内联
简短函数的高繁次调用会带来栈管理的开销,很容易想到把这些函数改成内联函数。一些日志打印、状态检查等函数调用频次非常高,如果使用的总cpu较高,就可以改为内联函数,御龙在这方面的例子包括前面6.4.3中的GetStatus函数和6.4.4中的时间比较函数,都取得了较好的优化效果。
七、结语
通过以上的优化方法、工具和策略,《御龙在天》手游服务器在以下几个方面都达到了理想的优化效果:
通过场景切换流程优化,大大降低了玩家场景切换失败的几率;
通过国战架构优化,降低了逻辑开发复杂度,提升了正式环境部署的灵活性和容灾效果,同时也均衡了网络流量对机器带来的压力;
通过合包策略和技能、移动流量优化,降低了客户端侧的流量消耗,也提升了客户端的运行流畅度;
在代码级性能优化部分,通过结合御龙在天手游的实际运营案例,介绍了在服务器侧常用的性能优化工具、原因定位方法和优化策略,希望能给各位读者在遇到性能优化问题时提供相关帮助。