脚本引擎栈指针误操作引起内存泄露
最近,为了确定目标部署机器的配置,我们对服务器进行了玩家登录的压力测试。在 E7500 cpu,3G 内存的开发机上,同一时间登录人数不能超过 20 人。以 0.01 秒间隔登录,测试人数 5000 人,登录时一个核心在 30%-60% 之间波动,另一个核心比较空闲。稳定后,CPU 平均在 5% 一下(玩家只是登录后保持心跳),峰值内存在 2.3G 以内。但比较诡异的是,当玩家全部断开后,服务器的内存还是没有降下来。我觉得,可能是发生内存泄露了。
发现这个问题后,我排查了底层对玩家 user object 的管理。user 有两个部分组成,一个是外部包裹的一层网络统计相关的信息,另一个是内部的 lpc object。外部的 user 有 online_user_tbl 这个哈希表负责管理,提供了遍历方法、总数统计及获取所有对象的方法。而实际数据的存放,则是在 all_users 里面,这个是一个大型数组,启动时按配置设定的最大在线人数进行预分配。
通过 dumpallobj() 接口,初步发现内存泄露时,user 对象虽然释放缓慢,但是在几分钟之内,已经全部释放完毕。因此排除了 user 对象的泄露。再加上预分配的 all_users 列表,大小是可以估算出来的,大概 400M 的样子,不可能引起那么大的问题。于是又通过 memory_info(), 打印出 lpc vm 各种 string, mapping, object 的内存占用,发现还是没有异常。跟老大商量过后,他认为是内部做了内存池之类的结构,证据是内存不会无限疯长,只要没有活跃玩家,峰值就停留在 2.3G 左右。由于没有头绪,只好暂时放下。
昨天,为了测试网关服务器的转发性能,我便写了个 ping pong 测试,大约是 3Mb/s,1000 包 /s 的样子。CPU 占用有点高,一个核心占了 50%。诡异的是,做 ping pong 测试没有涉及具体逻辑,但是内存一直在不停地涨。跟老大反映后,他认为是最近新加入的脚本层 socket 有问题。于是,花了大半天的时间,仔细审核那段代码。的确发现了有一处内存泄露,在某一处错误检测里,会直接 return 掉,没有处理中间分配的临时变量。不过那个条件分支没有跑到,所以内存泄露应该是别的地方。
今天,又特意写了黑洞测试来考察脚本层 socket。在只是打数据到远端服务器,不需要序列化,不需要处理逻辑的情况下,服务器内存占用相当稳定,看来可以排除 lpc socket 了。ping pong 的逻辑只有 100 行不到,那剩下来的应该就是序列化数据和反序列化数据的地方了。当时,出于通用协议的考虑,内部服务器之间的通信,没有具体定协议,只是以 req 和 resp 两条协议做支撑,参数序列化为 json 字符串进行发送。没想到,单独序列化和单独反序列化都没有内存泄露。我原以为是序列化 / 反序列化过程中,对临时生成的字符串没处理好,造成内存泄漏的。
后来,和老大一道,审读 json 序列化和反序列化的代码,发现栈顶指针操作有点异常。对于一般的脚本层调 C 层 efun,会用宏 SET_ALL_ARGS 获得参数的个数和偏移地址,在处理完参数转换后,再通过 POP_ALL_ARGS 清除所有栈顶的所有参数(参数的引用计数减 1)。当有返回值时,将返回值压入栈顶。没有返回值时,压入空值。但是,反序列化的函数,只是暴力的将栈顶指针指向自己生成的返回值,没有对传入的参数做任何处理。就是这个地方,导致了参数的内存泄漏。其实我早该想到的,不是序列化过程中的泄漏,也不是某一单过程的泄露,那就应该是传参出问题了。。。以后要统一规范,用对应的宏来正确处理栈指针和脚本层传来的参数。