1. 背景
关于为什么需要做x64升级的原因参见上篇。
这里分享的是升级之后发生的一次诡异的内存泄漏。
2. 问题表现
- 刷怪之后,内存随时间明显上涨,RES大概2MB/5s,但怪物数量无变化。
- 诡异的是,tcmalloc总内存完全没有反映内存的增长。
3. 分析过程
最初始的怀疑对象是新引入的腾讯的behaviac组件,因为刷怪之后内存涨得特别明显。经过简单地查找,发现behaviac组件确实有自己的内存分配器。进一步分析发现,behaviac组件只是有一层内存池的管理,它还是用系统的malloc等接口申请内存。也就是说,我们项目里behaviac组件也是通过tcmalloc分配内存,不是这个组件的问题。
所以,只剩一条路,gdb下函数断点。内存申请的点多,下个函数断点打日志。当时的gdb命令没记下来,信手写一下吧,大概长得这个样子。
1 | b malloc |
断了几个分配的接口,发现打印的分配值和实际增长值都对不上。最后,负责副本的骏图同学(当年招到的一位特别靠谱的同事,服务器出身,新项目负责客户端渲染,水面/人物材质做得很漂亮。低调,不玩朋友圈不发博,所以没链接,面试不招是损失)把这个函数锁定在了mmap。多采样几次,看看共同点,下面是从聊天记录里挖出来的两个堆栈,前一张来自骏图,后一张我的。
堆栈大致分三个部分,由下到上分别是
- 我们的逻辑,析构内存,上层逻辑各有不同。
- tcmalloc, tcmalloc的逻辑都是free时调用libunwind记录堆栈。free的堆栈做什么用的呢?如果一个地址被free了,他不会立马回到备用池里,而是放到另外一个暂存的池子。如果地址还继续被引用,tcmalloc可以凭此打印free的堆栈。扯远了,先回来,内存分配器改天开篇聊。不过这种逻辑,debug版本才会有,编译release版本试了一下,果然就不漏了。
- libuwind, 取堆栈
Debug版本的也得解决啊,节操问题。直觉上怀疑是tcmalloc 64bit下暂存的池子太大了(上图frame 16),调试一看,按字节算的,撑死10MB
在往上看frame 15,源码如下,MallocBlockQueueEntry传了16个指针数组给libunwind填空,和libunwind的交互不需要再分配什么内存,tcmalloc也不需要调libunwind接口释放什么东西, 所以就libunwind自己申请的得自己管理,这样子的话,就是libunwind漏了。
仔细看堆栈,libunwind部分也不完全一样,一阵小郁闷,libunwind咱不熟。不过这库挺老了,用的人多,应该会有人报bug,遂Google “libunwind memory leak”。果不其然,真找着了,链接。
我们用的版本是libunwind-0.99-beta,tcmalloc推荐版,老了,和链接里的Patch代码长得不一样,照着描,core了几次之后最终版diff如下,中间过程也不表了。好吧,老实说不记得了,其实我现在看回改过的代码也挺懵B的,算了,不是研究这个的。。。好吧,我还是记得只是一些先后顺序的出了问题,但是读者不会满意的:(。
1 | --- |
4. 后记
总结一下,这问题刚开始认为是刷怪的问题,其实是刷怪了之后,内存分配释放比较频繁,把问题暴露了出来,让副本的同学背了会儿锅。
然后,这个逻辑是debug版才有,怪不得x86和x64做机器人压测比较的时候,也没发现。