1. 背景

我们项目为ARPG手游(也没啥见不得人的,就叫暗黑血统手游,后期不少坑钱活动的实现出自我手,轻拍。。。)。我们的服务器底层设计源于某大厂,c/c++和luajit的实现,这次要说的是项目上线时(2014年11月左右)的一次luajit对象内存泄漏(废弃的数据没删,我们都叫泄漏)和相应的解决方案。

2. 问题表现

内存增长,速率大概为200~300MB/天。

我们日志会周期性打印Tcmalloc内存(Tcmalloc分享另见同事Wallen的博客TCMalloc解密)和lua部分内存。获取方法如下:

//tc malloc部分
size_t tc_memory = 0;
MallocExtension::instance()->GetNumericProperty( "generic.current_allocated_bytes", &tc_memory );//luajit部分
int lua_memory = lua_gc( _L, LUA_GCCOUNT, 0 );
复制代码

通过日志发现在线人数相似时,lua部分内存和总内存在同步增长,c/c++部分内存(即上两个部分差值,主要是网络库、对象体系对象和World部分的内存)基本稳定。日志显示lua的gc正常。

3. 分析

  • c/c++部分没有泄漏,player/monster等对象释放没有问题,c层对象析构基本由lua层触发,lua层对应的对象内存释放也是没有问题的。
  • 通过日志显示,场景/副本管理的释放也是没有问题的,在线导出各个场景/副本内的对象个数/类型,经过分析,也没发现什么问题,日志监控的数据也都正常。
  • 最后怀疑是一些非主要的lua对象,存在表里没清除。因为lua对象里,我们没有太复杂的表结构,因此这些泄漏的内存会扁平地存在少量的几个表里。因此,只要能知道各个表的记录数,结合在线人数推算其最大可能数,二者相比较,就能找出泄漏的表,在检查表的增删逻辑,就可以找到泄漏的逻辑。

4. lua表记录数告警方案

如前述,只要知道各个表的记录数,结合在线人数推算其最大可能数,二者相比较,就能找出泄漏的表。但是如果直接这么做,势必影响性能,即使热更gm指令用lua全遍历1次(因为table的值也可能是table,实际上要遍历一棵树),都是分钟级别的。分帧做?这坑好大,如果不行也只能这么干了。按泄漏的速度,内存可以撑到下一次维护,所以,不慌。

然后看看c层luajit表的相关操作,看看有没有更有效率的获取方法(我们c层代码不热更,改c层代码需要等维护重启后才能生效,按泄漏速度,可以接受)。

代码里主要关注的是lua table的增加和删除记录。然后看到lua table的resize(下面luajit相关代码都是luajit2.1分支代码,和1会长得有些不一样,我们已升luajit2.1,将就一下)

/* Resize a table to fit the new array/hash part sizes. */
void lj_tab_resize(lua_State *L, GCtab *t, uint32_t asize, uint32_t hbits)
{
...
}
复制代码

逻辑其实就是数组段或者哈希段每次超过2的n次幂,会重新分配内存。

我们不需要精确的记录数,其实只要在他每次resize的时候打条日志就能知道这个table大概的记录数,比如上一条日志是1024->2048,那么记录数在1024~2048之间。日志的优化见工程化部分。

怎么确定是哪个table?这里我们能取到的是table的地址,取不到table的名字,根据地址取名字也是一场噩梦。这里我们曲线救国,既然能拿到lua_State,可以把lua的堆栈打出来,根据文件名和行号可以定位到代码行号,一行代码没几个table,这样就能确定下来了。

//打印lua层堆栈,编译lua加上调试信息
int32_t c_bt( lua_State* _L )
{   lua_Debug ldb;LOG("[LUAWRAPPER](lua_stack) begin .......... ");for(int32_t i = 0; lua_getstack( _L, i, &ldb)==1; i++){lua_getinfo(_L, "Slnu", &ldb);const char * name = ldb.name;if (!name)name = "";const char * filename = ldb.source;LOG("[LUAWRAPPER](bt) #%d: %s:'%s', '%s' line %d", i, ldb.what, name, filename, ldb.currentline );}LOG("[LUAWRAPPER](lua_stack) end .......... ");return 0;
}
复制代码

到此,表的记录数我们能拿到个粗略的值,也知道是哪张表了,每张表最大的数值也可以根据在线人数估计(大部分近似在线人数+暂时断线的+跨服的,Buff之类的可以乘以一个最大倍数),剩下的就交给时间和人工分析日志比较了。过滤掉正常的日志,就能得到包含了泄漏对象的表了,在分析增删逻辑就能找到废弃又没有清楚的数据了。

5. 工程化

前面讲的只是方案,真正应用的时候,需要减少日志的条数,以减轻分析的工作量。减少日志通过下面两种方式:

  • 超过一定大小,才打日志,我们一个服在线是3k左右,阀值取4096。
  • 实践发现,如果表在2的n次幂边界发生频繁切换时,resize日志会重复打,所以修改了表结构,实现每个边界只打一次。

所以,如果漏的不严重(<4096)又不会随时间增长,是查不出来的,也就算了。

5.1. lua table表结构的修改


typedef struct GCtab {...//extended by ludongint32_t max_sizearray;int32_t max_sizeobj;
} GCtab;
复制代码

5.2. 初始化(luajit1的数值是不一样的)

/* Create a new table. Note: the slots are not initialized (yet). */
static GCtab *newtab(lua_State *L, uint32_t asize, uint32_t hbits)
{...t->max_sizearray = 4096;t->max_sizeobj = 4096;return t;
}
复制代码

5.3. 写日志

为了解决库编译依赖的问题,将上层日志函数定义为一个函数变量,进程启动时注册赋值。函数实现赋值就不贴了。

/* -- Table resizing ------------------------------------------------------ */typedef void (*lua_large_table_warn_func)( lua_State *L, char* fmt, ... );
lua_large_table_warn_func g_lua_large_table_warn = NULL;/* Resize a table to fit the new array/hash part sizes. */
void lj_tab_resize(lua_State *L, GCtab *t, uint32_t asize, uint32_t hbits)
{...//--------------------------------------------------// by ludong// check resize timeif (t->asize > t->max_sizearray || t->hmask > t->max_sizeobj) {int32_t old_max_sizearray = t->max_sizearray;int32_t old_max_sizeobj = t->max_sizeobj;t->max_sizearray = (t->asize > t->max_sizearray) ? t->asize : t->max_sizearray;t->max_sizeobj = (t->hmask > t->max_sizeobj) ? t->hmask : t->max_sizeobj;if (g_lua_large_table_warn) {g_lua_large_table_warn( L, "[ltable.c](resize) table:0x%x, array check size:%d, now:%d, node check size:%d, now:%d",(size_t)t,old_max_sizearray, t->asize,old_max_sizeobj, t->hmask);}}}
复制代码

5.4. 性能影响

每个表多8字节(我们64位了,对齐后一样的,32位自己抠一抠),对大部分表多两个逻辑判断,对大表每次日志边界打一条日志和lua堆栈,内存和cpu基本都没没感觉。

6. 后记

这个方案做好之后基本是我们项目上线必检查的日志了,总会有一些不小心就没删的。

luajit表记录监控(忆一次项目上线中遇到的luajit对象内存泄漏)相关推荐

  1. 【错误记录】Android 内存泄漏 错误排查记录 ( FinalizerReference 内存泄漏 )

    文章目录 一. 报错信息 二. 内存排查 三. 代码分析及修改 四. 不同版本说明 参考以下博客 : [Android 内存优化]Android Profiler 工具常用功能 ( 监测内存 | 内存 ...

  2. 视频智能运维大单-金昌市公共安全视频监控建设联网应用项目8938万元招标

    视频监控智能运维管理系统 新建 升级 改造 显商机 2022年1月18日,中国政府采购网发布"金昌市公安局金昌市公共安全视频监控建设联网应用项目公安分平台及相关工程和视频监控增点扩面建设项目 ...

  3. [项目过程中所遇到的各种问题记录]部署篇——项目部署过程中那些纠结的问题-SQLServer...

    前一篇文章说了些有关IIS的,这篇则是说SQLServer的,相比IIS来说,SQLServer的配置过程中问题就少了许多,而且都比较有针对性,下面开始记录: 注:由于实际项目的开发都是基于SQL20 ...

  4. MySql 触发器同步备份数据表记录

    添加记录到新记录表 DELIMITER $$ USE `DB_Test`$$ CREATE/*!50017 DEFINER = 'root'@'%' */TRIGGER `InsertOPM_Alar ...

  5. GitHub轻松阅读微服务实战项目流程详解【第一天:数据库表设计及其环境搭建、项目运行】

    One Day 1.数据库表详解 2.项目环境准备 github地址:https://github.com/Zealon159/light-reading-cloud 项目服务端主要使用SpringB ...

  6. mysql 核对_核对数据库表记录的shell脚本

    项目中需要核对数据库表记录,由于表太多,一一核对是相当痛苦的事情,于是临时写了2个超级简单的脚本. 一个用来核对mysql数据库表的脚本,另外一个用来核对oracle数据库表的脚本. 有需要的朋友,可 ...

  7. Java案例:连接SQL Server数据库,显示学生表记录

    Java案例:连接SQL Server数据库,显示学生表记录 演示利用JDBC连接SQL Server数据库,在Java GUI窗口里显示表记录. 一.运行效果 二.实现步骤 1.项目结构图

  8. 项目开发中对使用的第三方库统一进行管理__添加属性表/页

    最近接手的一个项目开发中用到了很多第三方库,比如boost.gdal.xerces等等这些.从接手项目到现在从中学习到了很多之前从未见识过的东西.项目绝大部分都是前辈们写的,对于里面的对第三方库的管理 ...

  9. [项目过程中所遇到的各种问题记录]工具篇——.NET开发时常用的工具类库

    在日常开发的过程当中我们总是会根据项目的开发需求将一些公用的类或者方法进行抽象封装,这些类或方法的抽象封装可能是基于某个项目或者多个项目,最常见的应该就是SQLHelper了,这些类库在实际使用的过程 ...

最新文章

  1. Visual studio 2010出现“error LNK1123: 转换到 COFF 期间失败: 文件无效或损坏”解决方式...
  2. JavaWeb总结(六)—Session
  3. Serverless Kubernetes 容器服务介绍
  4. zzuli 2520: 大小接近的点对
  5. Object o = new Object()在内存中占几个字节
  6. 备受期待的Python深度学习来了
  7. C++学习之路 | PTA(天梯赛)—— L2-024 部落 (25分)(带注释)(并查集)(精简)
  8. 如何做规划?分享2种思维和4个方法
  9. access insert语句怎么写_码住!MySQL中超实用的几种SQL语句
  10. matlab中用数据拟合圆心,拟合圆并求圆心(matlab)
  11. bzoj 4417: [Shoi2013]超级跳马(矩阵合并+快速幂)
  12. Rust: Rangechar 'a'..'z' 能干什么?......待续
  13. PHP去除所有的空格
  14. 鸿蒙电视rom,鸿蒙系统刷机包
  15. 110道 MySQL面试题及答案 (持续更新)
  16. 射极跟随器负载加重解析
  17. 全面领跑中国DevOps云服务市场,为什么是华为云?
  18. java泊松分布随机数,C语言生成泊松分布随机数
  19. Python转义字符及用法
  20. “~i“在C语言的for循环中是什么意思

热门文章

  1. mysql delete 会锁表吗_MySQL高压缩引擎TokuDB 揭秘
  2. 发生生成错误是否继续并运行上次的成功生成_JavaScript 是如何运行的?
  3. 安卓+php推,使用 PHP 消息队列实现 Android 与 Web 通信
  4. xpath以某个字符开始_XPATH技术补充-实例
  5. 微信支付宝服务器在哪里,支付宝支付与微信支付服务端回调notify_url数据的区别...
  6. gen文件下有两个R.java_gen目录无法更新,或者gen目录下的R.JAVA文件无法生成
  7. 计算机的硬件简介,计算机基础之硬件简介(Day2)(示例代码)
  8. js 操作java对象_JavaScript 对象基础
  9. html css制作计算器,使用html+css+js实现计算器
  10. android icon在线更新,Android在线更新下载方案