文章目录
  1. 1. 分析思路:
  2. 2. 工具篇之Valgrind
  3. 3. 内存申请/释放配对检查
  4. 4. 内存空洞/碎片
  5. 5. 反证之脑洞大开
  6. 6. 几篇关于内存方面不错的资料:

背景
最近遇到一个问题,现象是主备反复倒换(产品的一个测试场景,对应到进程内多个线程反复起停),进程内存占用持续上涨直到系统OOM。


从操作步骤及现象来看,第一感觉是有内存泄漏,但内存相关问题定位一般都比较棘手。可能由于近期代码做了比较大变动(日志优化),虽然无法确认是近期优化引入的问题,临近版本过点,压力还是蛮大。经过几天的不懈努力,问题也得以分析清楚,期间把内存相关知识又过了一遍,收获还是蛮大的,记录一下。

分析思路:

  • 使用工具Valgrind跑一下,做初步筛查;
  • 所有malloc/free配对检查,看是否有内存泄漏;
  • 使用glibc的malloc_stats()/mallocinfo()/malloc_info()打印内存分配统计,看是否有内存空洞
  • 修改验证;

工具篇之Valgrind

由于产品OS是定制的,Valgrind运行时,整个世界都停止了,定制的系统各种限制,跨部门沟通成本很高,这条路先被Pass了。如果环境允许的话,Valgrind还是首选的方式,简洁明了,能够快速检测出大部分的内存问题。
参考用法:

  1. Linux C/C++ 内存泄漏检测工具:Valgrind-张宴
  2. 应用 Valgrind 发现 Linux 程序的内存问题-developerworks-cn
  3. Memcheck: a memory error detector-官方的

内存申请/释放配对检查

代码使用了glibc自带malloc/free,为了对所有的内存分配进行跟踪,我们对malloc/free进行了封装,可以打印分配/释放的内存地址,最后根据收集到的日志,使用脚本去做地址的配对检查,看是否有未释放的内存。按这个操作了结论是:没有内存泄露。该方法简单粗暴,除非之前malloc/free已经做过封装,否则改的地方会有点多。另外像scandir这类函数内部实现也调用了malloc,这种分配跟踪不上,配对时需要特殊考虑。

内存空洞/碎片

排除了内存泄漏,基本就只有内存空洞一种可能性了。
关于内存空洞比较全面的解释:

  1. 关于内存空洞的一个解释(转自我在水木社区的一个回帖)-pennyliang
  2. 频繁分配释放内存导致的性能问题的分析-百度分享(强烈推荐)
  3. 内存分配的原理__进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。
  4. 内存分配的原理__Linux虚拟内存管理

使用malloc_stats()函数可以打印出glibc内存分配统计信息,包括brk和mmap内存分配情况。brk可能包括了多个arena分配区域总共分配的空间和正在使用的空间,差值基本就是已经释放的空洞内存了。
如果想知道堆内究竟有多少碎片,可通过mallocinfo()调用拿到的mallinfo 结构中的 fsmblks 、smblks 、ordblks 值得到,这些值表示不同大小区间的碎片总个数,这些区间分别是 0~80 字节,80~512 字节,512~128k 。如果 fsmblks 、 smblks 的值过大,那碎片问题可能比较严重了。
备注:Information is returned for only the main memory allocation area.一般为arena 0
也可以通过调用malloc_info()函数获取以XML格式表示的更为详细的包括所有arena的内存分配信息。
通过malloc_stats()的输出我们基本可以确定有内存空洞。

下面需要排查是什么导致了该空洞?

  1. 是小内存分配太多导致有大量的碎片但是达不到M_TRIM_THRESHOLD 128k无法触发内存内存紧缩?
  2. 还是GLIBC内存分配机制引发的“内存泄露”这里提到的Glibc的新特性:M_MMAP_THRESHOLD可以动态调整,一次mmap之后,阈值被调高,导致后面的大块内存申请使用了brk?

起初对小内存太多有些怀疑,最近为了加速日志读,增加了日志内存索引,基本都是几十字节的小内存分配且长时间不会释放,为了验证是否是这个导致内存空洞,我们屏蔽了所有与索引相关的内存分配,使用上面的手段发现还是会有内存空洞,跟小内存关系好像不大。。
M_MMAP_THRESHOLD可以动态调整这个也可以被排除,因为不管是显示的设置M_MMAP_THRESHOLD值为128K还是把原本的申请的大内存块儿申请改到128K以下,都不能改变空洞的情况。

网上有说法讲jemalloc等优秀的开源实现是内存碎片的救命稻草,当时都想用tcmallocjemalloc来替换glibc做内存分配了,但是glibc这么广泛的使用应该不至于这么弱,而且产品这边替换这个影响还是蛮大的,所以替换的可能性很小。

反证之脑洞大开

为了证明最近的日志优化没有问题,我们通过代码开关切换到原来的使用SQLite的方案,该方案下没有索引(小块内存长期持有);没有大块儿内存申请。按道理说应该不会有内存空洞的问题,因为上个大版本用的SQLite就没有问题。看到测试结果,已经比较明朗:即使回滚到SQLite的方案,还是有内存空洞。
经过SQLite引入方的分析,上个大版本到现在替换了SQLite的版本:由3.7.17升级到3.8.6,这个改动我们事先并不知情,所以也没有往这方面想过,测试换回老的3.7.17版本,再用
日志优化方案测试,无内存空洞问题。应该不是用法不对,SQLite的新版本在内存处理可能上还是有问题的,盲目追新有风险。因为我们日志优化已经弃用SQLite了,精力有限,未作深究。

内存问题,有的时候水蛮深,但是掌握内存布局及工作原理,分析定位方能如丝般顺滑:)

几篇关于内存方面不错的资料:

  1. jemalloc源码解析-核心架构
  2. jemalloc源码解析-内存管理
  3. ptmalloc,tcmalloc和jemalloc内存分配策略研究
  4. Scalable memory allocation using jemalloc
  5. 更好的内存管理-jemalloc
  6. Linux系统下/proc/meminfo详解
  7. 内存分配奥义·jemalloc(一)
  8. 内存分配奥义·jemalloc(二)

—EOF—

文章目录
  1. 1. 分析思路:
  2. 2. 工具篇之Valgrind
  3. 3. 内存申请/释放配对检查
  4. 4. 内存空洞/碎片
  5. 5. 反证之脑洞大开
  6. 6. 几篇关于内存方面不错的资料: