0%

记一次内存优化

我们游戏已经上线五年了,线上在线并没有那么高了,但是占用的内存还是很高。对于一个老游戏来说,优化内存带来的性价比其实并不高,对kpi表现也没有太多收益。但是我们搞技术的,骨子里就是会被有难度有挑战的事情吸引。那就干吧!

我们是基于Skynet框架的游戏服务器,一个玩家一个Skynet服务(以下简称服务),一个场景分线一个服务,一个战斗副本一个服务。除此之外还有各种业务模块的服务。使用Skynet的GM Console命令,可以收集每个服务占用的lua内存和c内存。由于c内存集中在sharedata上面,这里就不展开说了,只针对lua内存来展开。进行统计发现玩家服务占总内存的46%,场景占20%,战斗副本占30%。

第一步,优化了玩家服务池的数量。当初为了让开新服的时候更加顺滑,有个玩家服务池的功能,在开服会创建一定量的玩家服务。这样玩家登录的时候不再需要创建服务,只需要从玩家服务池申请一个,再初始化数据就可以了。但是对于已经上线5年的项目来说,已经没有这种开服压力了,甚至已经填充不满池子了。这里的优化很简单,降低池子大小就可以了。

第二步,优化掉了orm的一些元表。我们还实现了一个工具,通过遍历和汇总lua的内存数据,并进行树状绘图,用来快速观察内存占用。通过分析,我们发现有很多相同的占用312个字节的元表出现。我们找到这个元表添加的代码,发现orm针对每个list和map类型的数据都会创建一个元表,这个元表包含了5个键值对。因为我们玩家数据量非常大,list类型和map类型的数据非常之多。譬如背包,卡牌,属性这些。找到问题了,优化其实也是比较简单的,元表是需要的,但是不需要每个orm对象都创建一个新的元表,所有的list类型都可以挂同一个元表,map也是。两个table就可以了,然后每个对象只有一个引用的内存占用。

第三步,优化掉了我们卡牌的历史属性。这里是业务问题,我们卡牌强化是会掉级的,所以玩法上记录了每一个强化等级的属性,以方便掉级的时候需要用。这里的属性内容比较多,等级也有15级,所以存储的内容就比较多了。但是这个属性在战斗的时候其实是不生效的。由于属性太多,如果一开始业务针对那些不会掉级的等级不保留属性并且进行保底机制处理的话,也会节省不少内存。考虑到这些数据已经挂在玩家身上了,目前也没有保底的机制。优化上,我们增加了一个缓存层,把玩家的这些历史属性进行存库处理,玩家使用的时候再加载到缓存中。
我们也把这个功能做成了通用化,别的系统也可以使用,只要业务上数据量大,查询少。缓存我们使用了最近最少使用lru算法,玩家不用的时候,到了过期时间就会删除缓存和存储。

第四步,优化掉了过期商店商品。还是因为是五年的项目了,我们运营加了非常多的商店和商品配置。并且有非常多有时效性的商品,并且早就过期了。但是这些配置仍然存在(因为我们要求不能删除配置表),玩家身上也有存储。运营上来说,我们是不会再重新开卖这些商品(可以理解为货架),就算是重新卖道具,也是会新建一个货架来卖。既然也用不上,就没有必要继续占用内存了,就把过期半年以上的商品删掉。

第五步,修改了patch代码的加载时机。原本是只要创建玩家服务,patch代码就会加载到内存。而且patch代码一般都有一些很大的表,例如玩家id,商品id,道具id这些,所以加载到内存还是不小的。patch的逻辑是每个patch每个玩家只需要执行一次。这里优化是改成要执行patch的时候才require patch文件。

第六步,优化了战斗属性字段数量。我们游戏的属性非常多,有七百多个。有好几处遍历属性配置表生成初始属性的逻辑,但是并不是每个战斗对象都会使用到每一个属性。而且真实生效的战斗属性也没那么多,很多属性都是当成状态使用的。这里优化改成有具体属性值的时候,才生成属性字段。没有的都访问一个共用的默认值(一个服务只需要一个)。这个优化是非常明显的,毕竟游戏越火,玩家越多,战斗对象越多。

手段上主要是要排查内存占用到了那里,大头是谁。lua因为存在循环引用的问题,非常不好统计。我们是通过先遍历虚拟机,然后记录对象的内存,以及父节点。之后对这些存在循环引用的数据生成一个树状结构。这一步还是比较麻烦的,首先父节点存在多个,需要选一个比较合适的,然后还要断开循环引用。这一步处理的不好生成的树就会长得不好。长得不好的树非常影响我们查看内存占用的业务结构。选父节点上,尽量选择地址小,而且是table类型的父节点。断开循环引用,如果是纯环结构,那断开地址最大对象的父节点关系。这样操作后,得到的树状结构基本上比较符合业务逻辑。

通过以上一套组合拳,我们玩家服务的内存降低了50%。场景服务内存降低了70%,战斗副本服务降低了40%。整个skynet进程,lua占用内存降低了70%,效果非常明显。