现在的位置: 首页 > 综合 > 正文

讨论游戏服务器压力的那点事儿

2018年02月08日 ⁄ 综合 ⁄ 共 5185字 ⁄ 字号 评论关闭

对于游戏服务器来说,压力无非来源于三座大山:数据库、网络以及系统资源(包括CPU、内存、硬盘、网卡等)


总体思路概括:

(一)数据库:1、复杂语句分开写-->简单句(查询

                         2、建索引

                         3、能集中处理的集中处理(减少数据库的访问次数)

                         4、建多级数据库(根据数据优先级建立优、劣数据库)

                         5、用缓存(memcache)

(二)网络:    1、Tcp超时断开(防止链接过多)

                         2、数据按优先级分队列发送(防止交互数据量过大)

                         3、数据合并发送(减小网络压力)

                         4、为Socket对象建资源链(减小系统调用,加快资源分配)

(三)系统资源:CPU、内存、硬盘、网卡等

                            

首先谈谈数据库

1、将一条复杂查询语句分解为多条简单查询语句,可以大大提高加载速度。

2、建立好数据库索引。

      MySql使用的索引是B-树,B-是一种平衡搜索树。

假设记录数为N,则它的搜索时间复杂度为O(logN)。而对于无索引的数据进行顺序搜索,则时间复杂度是N/2,即O(N)。根据logN/N的函数曲线,以及lim N→+∞ [O(logN)/O(N)] = 0的极限可以看出,当数据量(N)小的时候差别不明显,但对于海量数据的话,差别就相当大了。

因为B-树是一个顺序树,也就是在插入时已经排好序了,因此对索引列进行ORDER BY排序的操作耗时几乎是可以忽略的。
当然索引也有一定的缺点,就是对于插入和删除操作,它需要锁定并重建索引表。这会耗费数据库操作时间,所以对于插入频繁的表进行索引反而会适得其反。

3、数据库定时瘦身计划

 
     面对开服后日积月累的玩家与日志数据,无论再怎么优化对性能的影响已经无能为力了,这时就需要对数据库进行瘦身整理。

整理的方法一般有两种:
一是简单粗暴的删档。对某些条件,如超过x天没登陆的账号进行删除,其相应的其余记录也连带删除。

二是数据库分级,就像银行系统那样,建立两个数据库:活跃库与休眠库。定时把不活跃账号搬移一个休眠库中。当需要查询用户信息时,先从活跃库查询,查询不到再去休眠库查询。如果从休眠库中查到,就将这咸鱼翻身的用户记录搬回活跃库。

4、利用缓存应对玩家刷新(比如:memcache的配置)

      把数据记在内存中肯定比记在硬盘中高效。玩家下线后并不急着析构玩家对象,而是放在缓存内,因为我们无法得知他是刷新页面还是真正下线,这样可以缩短他再次登录的加载耗时。

同时还要根据系统内存使用状况进行整理,优先释放那些最近没被命中的对象,保留最近命中的对象,也就是缓存调度LRU算法的实现。

根据经验特别是在系统有问题或网络卡时,玩家最喜欢频繁刷新页面,因此缓存机制在这种情况下能大大减轻数据库的读取压力。




然后探讨一下网络端的压力

服务器是玩家数据的交通枢纽,客户端所有数据包都在这里汇集,并由这里分发出去。特别对于网游服务器来说,其最主要的压力就是来自于网络流量,情况严重时会导致服务器相应缓慢,甚至卡死。

网络流量就是像风一般的男子,残酷地摧毁了服务器的优雅。当繁华落尽,尘嚣散去时,只留下你如烟花般寂寞……(呕吐中)

1、钟山风雨起苍黄,百万雄师过大江——应对大量网络连接资源竞争。
我们曾经遇到一个问题,当开新服导量的时候,大约过了两个小时,登录就很不稳定。有时会卡死,但刷新一下有时就好了,但失败的几率依然高企。
后来我们发现建立的TCP连接数超高约8千多个。我心里的第一个想法就是遭到了TCP SYNC Flood洪水的DDoS攻击。但是查看链接源地址,没有规则的分布,而且又觉得新平台上的新服应该不会这么引人瞩目被枪打出头鸟吧。
直到后来通过询问平台导量方式后才得知他们是弹出窗口式的,而我们游戏客户端机制就是在弹出页面时就已经建立好了TCP连接。
这样就导致很多用户只是弹出窗口,但什么都不操作,不关闭窗口一直挂着,而服务端也一直维持着这个连接,这样就形成连接数爆炸的问题了。
后来我们做了心跳检测机制,在弹出页面不发送心跳包,让它超时断开释放连接。这样同时也能防止某些简单类型的DDoS攻击。

2、让领导先走——数据包分优先级处理
在我们进行压力测试时,发现在密集的地图中数据流量非常大。通过数据包统计分析,原来在RPG类型游戏中,玩家移动的数据量占很大比例。
比如某玩家视野内有N个其余玩家,那他的移动信息要广播N份,如果每个玩家都在移动,那么移动信息就要广播N*N份,形成恐怖的广播风暴。
针对这种情况,我们对数据包进行优先级划分,分为移动信息队列和命令信息队列:
视野内其他玩家的移动信息优先级低,即使被丢弃也不会对玩家的游戏功能使用上造成错误。
命令数据包优先级高,因为如果数据包被丢了,会导致前后端的状态信息不一致,以至于某些功能使用出错。

在平常状态下优先发送命令包,然后才发送视野内其余玩家移动信息包。
当网络数据产生拥塞时,先尽力发送命令数据包,再循环增量地尝试丢弃移动信息包。当命令数据包发送缓存队列也被塞满时,说明该用户的网络状况非常差,很多命令数据收不到,再玩下去也没意义了,只能强行将他断线——大家好才是真的好。


3、落红岂是无情物,化作春泥更护花——利用数据包环形队列以减少垃圾内存
在使用C++程序时,一个压力环节是来自频繁的系统调用。像new和delete这样的操作,都要向系统申请和释放内存空间,同时会产生内存碎片,严重时会触发系统内存整理。
这种情况在对socket数据包处理时特别严重,因为预先无法估计数据包的大小,所以一般都只能临时根据当前数据包长度向系统动态申请。
为了解决这个问题,我们就设计了一个socket buffer的环形队列,然后定两个浮标——读浮标和写浮标,先写再读,一追一赶。
这个环形队列在账号对象创建时就向系统申请内存,长度给一个默认值,不够用的时候再扩展。这就是类内存池的一个做法。


4、人有悲欢离合,月有阴晴圆缺——数据包的分分合合
在Mongos系统中对socket数据包发送有一个合包处理,就是把多个数据包合并在一个buffer中再一次过调用send发送出去;在收包时也有对数据包进行分离解析。这种做法是为了减少系统调用,以及额外的网络开销。
众所周知,在校车接送孩子的时候用一辆大车一次性运输所有人,比用一辆小车分多次来回运输的效率高。
当前网络设备的最大传输单元——MTU是1400字节,而我们游戏内容数据包一般不过上百字节。所以尽量把数据包合并发送以减少系统负担。
当然,合包发送时也要注意延迟问题,我们在程序中设定约50ms一循环来发送buffer中累积的数据,也就是玩家数据堆积最多不超过50ms,一般玩家感觉不到这种延迟。



最后说下系统资源,包括CPU、内存、硬盘等。由于现在采用了一机多服的部署,原来还算宽裕的系统资源一下子紧缺起来。

(1)垂死病中惊坐起,暗风吹雨入寒窗——利用看门狗检测死循环或死锁(~~~此为程序的逻辑问题~~~)

先探讨一下CPU压力。由于游戏不是计算密集型项目,所以平时cpu上的压力并不大,最主要的负担还是来自死循环。一旦某进程进入死循环,其中一核就会100%被占用,导致其余进程无法使用该核,系统运行效率下降,同时CPU温度加速上升,导致工作不稳定乃至重启,或因为温度预警CPU进入降频运行状态。

当进程进入了死循环或死锁状态,程序就会一直卡在那里,不重启也不产生core,但同时也不响应用户操作。
这种服务器半死不活的情况我们遇到过几次,于是我们就做了一个看门狗的机制。

程序中启动一个独立的看门狗线程,其余线程定时去喂狗。如果某个线程超过一定时间没访问过看门狗线程,就说明它要不结束了,要不就卡死了,然后调用assert(0)将服务端进程强行产生core文件以便分析并自动重启进程。

(2)那一段被遗忘的时光——内存泄漏
内存泄漏是C++服务器程序的一大通病,因为没有GC机制,所有动态申请的内存都需要程序自行去释放。
我们通过valgrid工具对内存进行检测,但valgrid必须随程序启动时启动,结束程序进程后才能产生报告文件。
于是我们自己编写了一个内存记录的模块,重载了new和delete操作符,记录每次new函数调用堆栈以及内存地址,delete时又从中删除相应记录,还可以通过console或GM命令对内存记录功能打开或关闭并生成报告。

这对于某单一操作的内存检查非常有用,否则系统的启动和关闭会牵扯到非常多无关的操作,使真正的问题淹没在茫茫大海中。

(3)千帆竞发,百舸争流——尽量避免多线程并发引起的同步问题
通过参照mongos系统的代码,它对所有玩家数据包都用一个统一的线程处理,这就避免了很多线程间同步的问题。这也是由于游戏的业务逻辑较简单,没有需要耗时的复杂计算,以及对实时性要求不高所决定的。线程间同步需要加锁解锁,增加系统调用,加大额外的开销,并引发很多同步问题。
我们曾经遇到过一个当机问题,就是当玩家登录时,我们从数据加载玩家信息较久,这时玩家不耐烦地刷新一下页面,就会导致玩家对象的重复创建与加载,并在地图上留下一个‘鬼影’。当对这个鬼影进行操作时,系统就是宕机了。这就是对象创建与加载分线程异步处理产生的副作用。

(4)硬盘
曾经有几次,我们发现mysql进程是在的,但无论游戏进程、后台页面、数据库工具都死活查询不到数据,无论重启多少次都没用。后来经数据库高人指点,用df命令一查,原来硬盘占用率已经100%。
所以我们后来也增加了对硬盘空间的检查,以免重犯类似的错误。

(5)网卡
我们服务器也曾经发生过网络间歇性抽风,TCP连接断断续续,但同一个版本在其他机器上却没问题。
抓包发现是TCP RESET,或者SYNC包没回复。检查系统的网络配置sysctl.conf,里面的fin_timeout、keepalive_time、reuse、recycle等配置都没错。

最后试一下升级网卡驱动,现象居然消失。这里不得不赞一下Centos系统的驱动模块,升级网卡驱动竟然不用断开网络,因为我们的SSH会话一直连接。

看来分析问题时我们不能光着眼于我们的程序逻辑,有时也要考虑系统底层的原因。

最后探讨一下服务器优化的工作方法

程序优化可以说是一个全新的工作方式,它即不像写新模块似的,一切从零开始;也不像修复bug那样有明确的错误现象。

它就像一个人对医生说,我觉得不舒服,但不知道到底哪里不舒服。对于程序也是一样,我就感觉玩起来很卡,但不知道到底卡在哪里,该从哪下手。

以前在其他公司也做过性能优化的工作,但性能优化往往是安排在项目后期才进行的。一些人迫于项目日程与版本计划的压力往往来不及细想,只是靠猜测去幻想系统的瓶颈在哪,然后就在哪动手。

但这样仓促上马的结果多数是事倍功半,一顿功夫忙活下来,系统性能只得到一点的提升。可以说是花了200%的努力,只获得1%的成果,而同时人力物力、时间精力也已经花掉了。

我认为这种情况是由于动手前没有做好调查而导致的。记得在以前在三星优化CDMA手机TCP/IP协议栈时,前先做了一个packet trace的报告,就是跟踪一个数据包从物理层解调结束后开始,到传输给应用层为止,记录其在每一层逗留的时长,以及每个处理所耗费的时间。再根据这个报告来定位优化点,及其可行性。
最后还要定一个目标值,比如参考目标是硬件配置类似的另一款型号,其传输速率达到300kbps,那我的优化目标是在两星期内使目标机型的传输速率不低于参考机型的90%,优化工作不能没完没了的无休止干下去。
所以我们在优化游戏服务器时也做过大量的分析记录,如sql语句的执行时间,每个数据包的处理时间,每种数据包出现的频率,socket发送环形队列的等待情况,socket接收队列的等待情况,socket发送情况,TCP滑动窗口状况等。
所有动手的依据都是基于大量的测试与实验的基础上,以避免像盲头苍蝇似的到处乱碰。

当然,有时项目日程较紧,把优化工作推后,而且开服后的一段磨合期中必定会面临这里宕机、那里卡死等突发性问题,优先解决紧急状况,延后优化工作这都是合理的安排。
算法设计大师唐纳德·克努特(Donald Ervin Knuth)曾经说过“过早优化是万恶之源”(premature optimization is the root of all evil)。这句话虽说有些夸张,但非常有道理。
在基于现有的程序上进行优化,即使优化失败了,也至少有个能运行的但效率不高的程序可跑。但是如果连功能都没实现好,就去考虑细枝末节的问题,就会导致项目进度没完没了地一再拖延。

最后两句话来总结优化工作:
1.一切行动都依靠于事前调查与测试

2.避免过早的优化


抱歉!评论已关闭.