1. 背景
我们项目为ARPG手游(也没啥见不得人的,就叫暗黑血统手游,后期不少坑钱活动的实现出自我手,轻拍。。。)。我们的服务器框架设计源于某大厂,c/c++和luajit的实现,这次要说的是项目上线时(2014年11月左右)的一次luajit对象内存泄漏(废弃的数据没删,我们都叫泄漏)和由此产生的luajit的表记录监控工具。
2. 问题表现
内存增长,速率大概为200~300MB/天。
我们日志会周期性打印Tcmalloc内存(Tcmalloc分享另见同事Wallen的博客TCMalloc解密)和lua部分内存。获取方法如下:
1 | //tc malloc部分 |
- 通过日志发现在线人数相似时,lua部分内存和总内存在同步增长,c/c++部分内存(即上两个部分差值,主要是网络库、对象体系对象和World部分的内存)基本稳定。
- 日志显示lua的gc正常。
3. 分析
- c/c++部分没有泄漏,player/monster等对象释放没有问题,c层对象析构基本由lua层触发,lua层对应的对象内存释放也是没有问题的。
- 通过日志显示,场景/副本管理的释放也是没有问题的,日志监控的数据也都正常。
- 最后怀疑是一些非主要的lua对象,存在表里没清除。因为lua的代码中,我们没有太复杂的表结构,因此这些泄漏的内存会扁平地存在少量的几个表里。因此,只要能知道各个表的记录数,结合在线人数推算其最大可能数,二者相比较,就能找出泄漏的表,在检查表的增删逻辑,就可以找到泄漏的逻辑。
4. lua表记录数告警方案
如前述,测试发现如果直接用lua全遍历_G下所有表(因为table的值也可能是table,实际上要遍历一棵树),是分钟级别的。按泄漏的速度,内存可以撑到下一次维护,所以,先不慌,看看有没有更好的办法。
然后看看c层luajit表的相关操作,看看有没有更有效率的获取方法(我们c层代码不热更,改c层代码需要等维护重启后才能生效,按泄漏速度,可以接受)。
代码里主要关注的是lua table的增加和删除记录。然后看到lua table的resize(下面luajit相关代码都是luajit2.1分支代码,和1会长得有些不一样,我们已升luajit2.1,将就一下)
1 | /* Resize a table to fit the new array/hash part sizes. */ |
逻辑其实就是数组段或者哈希段每次超过2的n次幂,会重新分配内存。
我们不需要精确的数值,其实只要在他每次resize的时候打条日志就能知道这个table大概的记录数,比如上一条日志是1024->2048,那么记录数在1024~2048之间。日志的优化见工程化部分。
怎么确定是哪个table?这里我们能取到的是table的地址,取不到table的名字。这里我们曲线救国,既然能拿到lua_State,可以把lua的堆栈打出来,根据文件名和行号可以定位到代码行号,一行代码没几个table,这样就能确定下来了。
1 | //打印lua层堆栈,编译lua加上调试信息 |
到此,表中的数据量我们能拿到个粗略的值,也知道是哪张表了,每张表最大的数值可以根据在线人数估计(大部分表记录数约等于在线人数+暂时断线人数+跨服人数,Buff之类的可以乘以一个最大倍数)。二者的比较就由人工比较了,随着时间的增长,有问题的表二者的数据会相差得越来越大。人工过滤存在嫌疑的表,再分析增删逻辑就能找到哪些逻辑忘了清数据了。
5. 工程化
前面讲的只是方案,真正应用的时候,需要减少日志的条数,以减轻分析的工作量。减少日志通过下面两种方式:
- 超过一定大小,才打日志,我们一个服在线是3k左右,阀值取4096。
- 实践发现,如果表在2的n次幂边界发生频繁切换时,resize日志会重复打,所以给lua的表结构加了两个阀值,实现每个边界只打一次。
所以,如果漏的不严重(<4096),随时间增长的很慢,一个维护周期都没到4096,是查不出来的,这种就算了。
有些配置文件很大,这种就选择忽视了,没做特别处理。
5.1. lua table表结构的修改
1 |
|
5.2. 初始化(luajit1的数值是不一样的)
1 | /* Create a new table. Note: the slots are not initialized (yet). */ |
5.3. 写日志
为了解决库编译依赖的问题,将上层日志函数定义为一个函数变量,进程启动时注册赋值。函数实现赋值就不贴了。
1 | /* -- Table resizing ------------------------------------------------------ */ |
5.4. 性能影响
每个表多8字节(我们64位了,对齐后一样的,32位自己抠一抠),对大部分表多两个逻辑判断,对大表每次日志边界打一条日志和lua堆栈,内存和cpu基本都没没感觉。
6. 后记
这个方案做好之后基本是我们项目上线必检查的日志了,总会有一些不小心就没删的。