内存泄露排查小记
最近我们的游戏服务器又发生了一起内存泄露事故。 由于泄露速度极其缓慢,所以有很大的隐蔽性。
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
Posted by: kevinjia | (11) March 23, 2013 04:43 PM
Posted by: ai | (10) February 23, 2013 08:31 AM
Posted by: davidxu | (9) January 26, 2013 06:37 PM
Posted by: Anonymous | (8) January 25, 2013 10:03 AM
Posted by: starshine | (7) January 23, 2013 05:36 PM
Posted by: haxixi | (6) January 23, 2013 04:13 PM
Posted by: 卡爱 | (5) January 23, 2013 10:15 AM
Posted by: 杨博 | (4) January 22, 2013 06:18 PM
Posted by: 杨博 | (3) January 22, 2013 06:17 PM
Posted by: ryan | (2) January 22, 2013 05:46 PM
Posted by: fnsoxt | (1) January 22, 2013 04:31 PM