Lua 内存泄露

出自云风 blog


一个 Lua 内存泄露检查工具

昨天我们发现每日构建的服务器突然在一个晚上内存暴增了 8 G ,显然是发生了内存泄露。

之前,我们在 skynet 里留下了许多调试协议,使我们很快的确定了发生泄露的服务:在一张地图的 lua State 中。可以确定是地图的 lua 实现中,有些 lua 对象在不断的生成。生成速度不快,但确实没有人解开引用,导致内存持续增长。

曾经有很多人做过 Lua 的内存分析工具,但是我懒的去搜了,花了半天时间自己写了一个。(已经开源在 github 上

原理是这样的:

这个叫作 snapshot 的库,只提供一个函数,它可以对当前的 Lua State 做一个完整的快照。但由于我并不像做 Lua State 的序列化工作(虽然做法很像),为了减少分析数据,只记录了复杂对象的引用关系。

即,记录下所有 table thread userdata function 间的引用。

我们可以在不同时间,对 Lua State 拍两个快照,相比较后,就很容易知道新增加的内存处于何处了。

一开始我想用 lua 来编写这个工具,其实也的确做的到。但如果有 Lua 写就得相当小心,因为 Lua 代码的运行过程本身会影响 State ,即,你想对它观察,就可能改变它。

用 C 直接调用 lua API 来遍历 lua State ,影响要小的多。

最终得到的对象引用关系数据其实很丰富,但因为我希望结果对 State 的影响小一些,就把一些信息合并成字符串储存到结果表内了。 snapshot 的返回结果只是一张简单的 table ,每个 Lua 对象,都以指针(lightuserdata) 为 key 储存在表中;对应的 value 是一个 string 足够详细的描述了这个对象的引用关系。

比如例子中的 dump.lua 这段程序,运行后就会得到这样的结果:

userdata: 0052E760      table
00521810 : tmp : dump.lua:7

userdata: 0052EB98      table
00521810 : S1 : dump.lua:7

这表示,两次 snapshot 间,增加了两个 table 。它们是被运行在源代码 dump.lua:7 处的函数中 tmp 和 S1 两个变量引用住了。


snapshot 生成的报告不太利于人阅读,但你可以写代码做进一步分析。比如可以逐级建立 table 的引用关系,用更人性的方式展现出来。由于只是一个临时工具,它也已经快速的帮我们定位了昨天的内存泄露问题所在,暂时我就不花精力去完善它了。


内存泄露排查小记

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

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 方法确保对象即时销毁。

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



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值