« Pixel light 中的场景管理 | 返回首页 | 温故而知新 »

内存泄露排查小记

最近我们的游戏服务器又发生了一起内存泄露事故。 由于泄露速度极其缓慢,所以有很大的隐蔽性。

Bug 最后是这样确认并解决的:

由于 Skynet 本质上是由唯一的时钟模块驱动的,我们首先修改了时钟部分的代码,让系统可以以 10 倍速运作。这样,原本耗时几天的泄露现象,可以在半天内就确定了:一定存在某种内存泄露的 Bug, 而不是正常流程导致的内存占用持续上升。

我们原本就预留了各个独立服务的调试接口,大部分服务都是用 Lua 编写,利用调试接口,可以清楚的看出每个 Lua 虚拟机占用了多少内存。

但是原有的 Lua 调试接口有一定的缺陷,它以来虚拟机本身要能对外提供服务。一旦服务阻塞,就不能自行报告调试信息。这次事故和这点无关,但这次我们改进了 Lua 内部的内存占用汇报机制。

我们给 Lua 虚拟机定制了内存分配器,把使用报告汇总到一个全局的 C 数组中。这样,直接用 gdb 切入进程,也能直接看到报告。调试接口也不依赖服务自身的响应来汇报内存状态了。

这次排查事故的过程,我们排除了 Lua 对象的泄露可能。因为 Lua 虚拟机中占用的内存都没有呈现不正常上升的状态,所以利用之前编写的小工具 很难派上用场。

在 C 代码层面,我们使用的 jemalloc 可以很好的辅助分析。

这次的现象比较典型,在服务器正常关闭后,几乎所有的内存都正确释放了。我们给服务器退出过程增加了更详尽的 log ,确认在一张地图退出时,关闭 Lua state 的同时,C 里面有大量的内存被释放掉了,远超 Lua 占用的内存数量。

这几乎可以确定,在 Lua State 中大量引用了 C 对象,在运行过程中没有解引用,却在退出时正确释放了。可是,我们又没有观察到运行过程中 Lua State 内存的暴增。一开始,这种现象颇能迷惑人。

经过一番思考,我确定了问题。这是因为,地图服务应用了一个 AOI 管理的 C 模块。这个 C 模块以整数 handle 的形式对内部对象做了封装。在写 Lua 封装层的时候,简单的提供了几个 api ,用于创建和删除 handle 。所以,C 对象在 Lua 虚拟机中是以 handle 的形式存在的。使用的同学不慎忘记了在合适的时机,显式去删除这个 handle 。

具体到这个问题上,应该在怪物死亡的时候删除怪物的 aoi 对象 handle 。这个 bug 存留了很久,之所以没有被发现,是因为怪物死亡频率不高,且 AOI 模块中相应的 C 对象消耗内存很小的缘故。直到策划重新布了怪,不同阵营的怪相互厮杀,血流成河的缘故。


这次事故提醒我,永远不要把显式销毁对象的责任追加到动态语言使用者上。尽量利用 gc 的机制,当然还需要更贴切的使用。这对 lua 的 C 绑定库的实现者要提高要求。

比如这次 C API 使用的是整数 handle 来引用对象,为这些 handle 创建独立的 userdata 虽然运行成本略高,但更安全一些。即,我们分配 4 字节的 userdata ,里面放上 C handle ,并给出 __gc 方法确保对象即时销毁。

关于封装库的话题,恰巧最近刚刚写过

Comments

期待云风大哥新的游戏
看看
FreeBSD内核有一种方法,就是malloc带有2个参数,一个是尺寸,另外一个是关于这个malloc的统计数据,例如你声明一个 MALLOC_DECLARE(SPRITOBJ). 然后malloc(sizeof(sprit), SPRITOBJ),那么这个申请的统计数据就会在SPRITOBJ这个统计信息中加一,free的时候也必须带上这个参数,统计数据会减一。 这就存在一种可能,当长时间运行后,需要检查memory leak的时候候,可以把这些统计信息dump出来,某些统计数据暴涨的MALLOC DECLARE就可以被怀疑是内存泄漏了。
希望早日看到用心做的游戏~ 希望是个好游戏~
云风老大,为什么直到现在了还在调试这种错误呢? 我觉得游戏应该已经快到了可以内测的状态了吧。
或者,给C对象进行计数,创建的时候++,销毁的时候--,大于某个阈值的时候,发出警告,这样可以避免大部分错误。 毕竟,直接用handle,手动free比用userdata gc 效率高,而且在handle量巨大的时候,更不应该给gc带来压力。
向云风大哥学习
> 直到策划重新布了怪,不同阵营的怪相互厮杀,血流成河的缘故。 突然有一种临场感是怎么回事
直到策划重新布了怪,不同阵营的怪相互厮杀,血流成河的缘故。
来看看
占个沙发,呵呵。游戏服务器还是太复杂了

Post a comment

非这个主题相关的留言请到:留言本