记一次内存增长排查的曲折过程

    博客分类: 经验感悟 阅读次数:

记一次内存增长排查的曲折过程

最近排查了一个内存泄漏问题,过程可谓一波三折,这里做个记录。本文只记录过程中遇到的坑,分析中使用的工具在另一篇文章中有介绍。

栈内存也可能增长

一般内存增长第一反应是堆内存有泄漏,但实际上栈内存也可能导致增长。我这次分析的这个问题,就是这样。起因是引擎原本只有短会话场景,但这次新加了长会话。支持的方式也很暴力——短会话一直不释放(当然,做了相应的功能修改)。而短会话中有很多栈上的变量,在整个会话中会一直存在。不仅如此,这些变量还会因为持续输入而不断变大(主要是一些vector和map)。由于短会话很快就释放了,原本这并不会带来内存问题。但是在长会话中,这导致内存几乎是线性增长!

这个问题是预料之内的,因此完整理了一遍代码,将其中持续存在的栈变量做了修改,内存增长确实大幅下降,从直线增长变成了偶尔涨一点。虽然涨的不多,但还是在涨啊!而剩下的这部分增长,耗费了我大量时间去排查!

tcmalloc 的坑

使用tcmalloc对内存进行分析(不用massif是因为这玩意的可视化工具需要GUI,而tcmalloc可以生成pdf等格式的可视化输出)。tcmalloc的使用方式见另一篇文章

根据tcmalloc的输出,有三处堆内存增长。这三处是类似的,都是在一个线程中申请内存,然后传递给另一个线程使用。嘿嘿,这里用完没free,找到问题了,改完下班!然而事情并没有简单。在使用数据的线程中进行释放后,会报重复释放的错误!仔细分析后发现,线程管理模块会在线程结束后释放传递给线程的数据。经过试验,线程结束后确实及时释放了。

那么问题来了,为啥tcmalloc会指示这里存在问题呢?而且根据tcmalloc的结果来看,其他地方也没有明显的内存增长了。难道是释放的不对?这三处传递的是自定义类,可能没释放完全?经过一番分析和试验,可以确认释放是没问题的!

又按照tcmalloc的输出分析了很久,没有什么进展。怀疑是不是tcmalloc有问题,误报了。于是又换massif继续分析。可结果居然是一致的,massif也认为是这三个地方导致的增长。这下有点崩溃了,明明释放了呀。

问题到这里就卡住了,后面几天都没进展。后来拉凯哥一起看,最后终于找到原因了。数据在A线程生产,B线程消费。当B线程消费的速度比A线程生产的慢,那么数据就会产生堆积。由于内存是在B线程结束后释放的,因此堆积的数据会导致内存增长。打断点看,生产的数据index到几千了,消费的数据index才几百,且index差值越来越大,证实了这一分析。

问题分析到这里,已经可以结束了,这个问题不用修——虽然可以在生产者和消费者之间加一些同步,但这只会将堆积提前,而不会消除堆积。因为最原始的输入是实时的,后续每一步都必须要能实时处理,否则就会在这一步产生堆积。问题的根源不在内存上,要解决这个问题,只能优化程序的效率,提高实时率。

真的就这么结束了吗?那本节标题为啥叫“tcmalloc的坑”呢?实际上,本着尽量和线上一致的原则,后面又用release编译的程序验证了上述数据堆积的分析。在程序里加了打印index的功能。然而,打印的结果却与之前的分析不一致!生产者和消费者几乎都是实时的,即使偶尔少量堆积,也很快就都被消耗了,不可能产生持续的内存增长!

那么tcmalloc看到现象为什么和release程序不一致呢?这是因为tcmalloc会极大的降低程序的性能(因为要对程序运行做记录),因此使用tcmalloc会导致实时率不够,从而产生了数据堆积(massif也一样)。

也就是说,使用tcmalloc分析了这么久,其实都白分析了,因为它报的这个问题,其实是它自己导致的!

不要轻易动看起来“奇怪”的“蠢代码”

tcmalloc这类工具会降低程序的实时率,导致真正的内存增长分析不出来。那么就只能继续靠“看”了。继续在代码里扣,发现有几个map很奇怪:这些map的key是一个整数,打断点看,这些整数还是递增的。这为啥不用vector之类的呢?第一节“栈内存也可能增长”中提到的会一直增长的栈变量包含这些map,当时是修改是当这些map涨到一定大小之后,每增加一个,我就删掉一个早期的元素。

会不会是这个修改导致的内存增长?map删除元素后内存不会立即释放,这个是有可能的。那么把这些map改成循环队列不就好了!反正他们有现成的下标!然而改完发现内存增长趋势并没什么变化。本以为这一波虽然没修好内存问题,但改了原来的“蠢蠢的”代码,也不算白干,结果却捅了个篓子。我改成循环队列后只在长会话下进行了测试,没有测短会话。这个修改会导致短会话中出现数据覆盖的问题。

经过一番折腾,最后还是将这块改回了map。中间试图用队列、双队列等去实现,发现都有坑。看来最初写代码的人也是摸过这些坑,最后才用了map。

你以为你free了,内存就还给操作系统了吗?

上一波虽然很折腾,但起码弄清了一个问题——内存增长和map没什么关系。可是问题分析到这,感觉真的没什么可能导致增长的地方了。难道是检测脚本的问题?这个脚本大家都用了很久了,应该不会有什么问题。后来凯哥提出了一个猜想:我们虽然free了,但malloc库的实现并没有将内存还给操作系统。

从监测脚本的结果看,涨的是物理内存,虚存稳如大山。这是符合这个猜想的。但是如何验证呢?和凯哥查了一堆资料,也没找到很好的验证手段。后来瑞哥过来说可以用jemalloc替换glibc默认的ptmalloc库,jemalloc的申请和释放十分即时。直接从瑞哥那里拷了jemalloc.so过来验证,跑了几个小时,内存果然稳住了。趁着周末又跑了两天,依然稳如大山。真的是ptmalloc库的问题,难怪一直分析不出来。

总结

  1. 内存增长不一定是堆内存泄漏,持续存在的栈变量也可能导致内存增长。
  2. 数据的生产和消费不在同一个线程中,那么要考虑线程间不同步导致的数据堆积问题。
  3. tcmalloc、massif这类分析工具会导致程序性能大幅下降。在对实时率有要求的程序中,要考虑这一因素带来的影响。
  4. free、delete只是告诉malloc库“这块内存我不用了”,malloc库不一定会及时释放。
  5. 一些看起来很“奇怪”的代码,很可能是前人摸过的坑,改的话要慎重。