版本开发终于接近尾声了,最近在做一些扫尾(性能优化)工作。老实说,这是我第一次细致的测量业务逻辑的性能。
我曾一度以为游戏服务器是io密集型程序,cpu其实很轻,至少对于一般的卡牌游戏应当如此。
虽然天天跟策划㗏㗏性能,但是那仅仅只是为了追求更好性能而已。
我们的服务器程序有10w+的echo能力,再加上我写逻辑从来都是按我知道的性能最高的方式写,所以我从不认为有一天会有处理请求过慢的问题,也从来没有思考过这个问题。
这次测试深深的给我上了一课。
在最初的版本中,一个普通五连抽,竟然要花2+ms,也就是说一秒竟然只能抽500次不到。
这也意味着一个服务器,只要不到500个人同时抽一次五连抽,服务器就卡爆了。就这,还没有统计网络处理的开销。
10万和500的巨大差距是我从来都没有想过的。
带着难以置信的心情,我开始使用valgrind进行性能分析,并一个一个的解决热点。
第一次定位到最大的开销竟然是log。
我看了一下源码,发现为了可以方便的定位问题,我们几乎每个函数都有log。有些函数在条件不满足(也就是什么都不执行)的情况下,依然会打一行log代表自己处理过。
我把所有逻辑都修改为不产生副作用时不输出log。毕竟函数是否进入,可以通过caller的逻辑来确定,相信caller会有相关的log。
而一旦可以确认一个函数被调用,如果没有log输出,本身就可以代表一种状态(函数不满足执行条件)。我认为这应该算是一种比较好的折中了。
修改了log之后,紧接着的热点就是货币管理。
在招募过程中会有若干个子过程,分属不同的模块。这些模块又会分别扣钱和加钱,由于逻辑相关性和KISS原则。我们不会在逻辑中将’扣钱’和’加钱进行合并操作’。
而且,为了保证可靠性,每次进行货币操作,都会进行数据库存储操作。
存数据库前,我们一般是先将一个struct序列化成string。然后进行存储,并打一行log。
根据valgrind显示,此次货币管理模块的热点问题就是存储数据库相关的函数造成的。其中log部分的开销占了一大部分。
我采用了之前文章里提到的关键点存库的方式,将货币模块的储存降到了每个客户端请求只存储一次数据库的频率。
优化货币模块本来最简单的应该是,直接删掉存数据库时的log, 毕竟log占了一大部分的开销。
但是本着最高性能的原则我还是修改了货币的存储频率。这样可以节省一部分数据库的开销。
做完以上这些修改之后,重新测了一下。
已经可以在不开O2的情况下达到800+次请求。开了O2之后可以达到1200+次请求。
继续用valgrind分析,发现代码已经开始均匀的变慢,并没有什么明显的特征。
按说这时候想再提高处理速度就只有换CPU了,但是我察觉到相关代码中均匀的分布着C++的unordered_map类。
联系到,上次优化战斗时,我牺牲了3k内存直接将使用最频繁的一个unordered_map换成数组时,性能翻了2到3倍。我觉得如果应该还有优化的余地。
但是,并不是所有的数据都可以使用数组来替换的,这些业务逻辑需要的是一个更好更快的hash_table。如果想要进一步优化,就只能找一个第三方hash table(比如把lua的借过来)来替换掉unordered_map。
但是扫尾工作,我不太想做这么大的的改动。现阶段这个请求处理速度已经勉强可以达到要求了。毕竟不是所有玩家都是土豪^_^。
我以前说过:“底层逻辑的开销会被上层逻辑放大”,这是对框架说的。现在看起来需要修正为:“被依赖模块的开销,会被所有依赖他的模块放大”。
就这次优化经验来看,有时候所有模块都有高性能的实现,不代表整体性能就高。整体框架或机制的设计,会成为制约着整体性能的瓶颈。
这就要求在以后设计框架或机制时,不仅要做到自身效率高,还要考虑在上面实现业务逻辑时,是不是可以高效的实现。好的框架或机制对业务逻辑的性能是有导向意义的。
ps. 以往优化都是局限在某一个系统或模块。而这次优化引导着我从全局来看整个逻辑,这是一种从未有过的体验。