起因是 skynet 的一个 Issue ,同时,这两天我们正在开发的一个项目也反应貌似有内存泄露。
我觉得两件事同时发生不太正常,就决定好好查一下。
其实在 skynet 里查内存泄露要比一般的项目容易的多。因为 skynet 天生就分成了很多小模块,叫作服务。模块申请的内存是独立的,内聚性很高。模块的生命期比整个进程短的多,模块的规模也不会太大,可以独立分析。一般说来,如果有内存申请没有归还,应该是 C 模块里的 bug 。而 skynet 会用到的 C 模块也很少,一旦有这样的问题,很快就能定位。
skynet 实现了一个内存管理的钩子,用于按服务分开统计内存的开销。代码在 skynetsrc 目录下的 mallochook.c 文件里。
我建议同事做这样的修改:
在每次内存分配释放时,按当前服务的 handle 做文件名记录 log 。这里有考虑一点点多线程并发的问题,但发生的概率很少,如果只是做临时的调试,可以暂时不理。因为同一个服务的分配行为不会并发,只有很少的情况释放在另一个线程中。如果发生了,会导致写 log 遇到一点错乱。
比较麻烦的是,在 hook 里第一次写 log 打开 log 文件有可能由 crt 触发 malloc 导致死循环。如果要避免这种情况,需要加一个 tls 标记,在进入 hook 后就设置上标记,避免被重入。
加上十多行代码后,内存管理的 log 就可以正常工作了。
一开始我认为 lua 本身的内存管理是没有问题的,所以可以将 malloc hook 里为 lua 定制的 skynet_lalloc
绕过 log (直接调用 je_malloc
即可)。这样就可以只记录 C 模块中的内存管理调用,数据量要少的多,比较容易排除问题。
但是加上 log 后,分析 log 似乎找不到泄露。我只好转而怀疑通过 lua 的分配器分配的内存有没有释放的。
怀疑点有两个,一是通过 lua_getallocf
取到分配器调用的。这个只在 lpeg 里发现过,应该没有问题;另外就是我自己对 lua 的修改了。
为了提高 skynet 下多 lua vm 的内存利用率,我曾经给 lua 打过一个 patch ,可以让不同 lua vm 共享相同的函数原型 。第一次加载的 lua 字节码是不会释放的,但第 2 次加载相同的代码,会复用之前的拷贝。
我仔细把 patch review 了一遍,没有发现问题。所以还是得靠新的 log 分析。
接下来把 log 重新写了一下,改成在 lua 定制分配器里记录。这个要容易的多,正好前几天刚做过 。还真发现了有一些长度为 88 字节的内存块没有释放。
lua 分配器在分配新内存时,会传入内存的用途(供什么类型的数据使用),这是一个重要的线索。这个泄露的 88 字节的类型为 0 (无记录),但它的上一条分配记录的类型是 9 也就是 LUA_TPROTO
。这说明很可能真的是函数原型中有数据没有释放。
而 88 字节恰好是我的 patch 中增加的 ShareProto 这个结构的长度。
最终 bug 的修复很简单:见这个 commit 。就是漏了一行 free 。
真是撞了鬼了。
因为我自己的硬盘上的本地代码明明一直是有这行的 :( 甚至我都不记得有过漏写这行代码。(这个 patch 是根据更早的 lua 5.2 版上的 patch 改过来升级到 lua 5.3 的。老版本是正确的。) git 的 diff 和 status 都没有察觉到我本地代码和仓库里有什么不同。这也是为什么这个内存泄露在我自己的机器上从来都没有发生过。
所以最后我重新 clone 了一份仓库,提交了这行遗漏的代码。但很难找到线索为何在操作 git 仓库的时候出现这个问题。或许是几个月前某次 push -f 强制推送导致的吧。