简介
本文将介绍基于jeprof定位内存泄漏问题,支持缓慢内存泄漏场景、瞬间oom场景等。
jeprof介绍
jeprof是jemalloc提供的一个内存优化的工具,jemalloc是facebook开源的内存管理工具,类似ptmalloc和tcmalloc,在多线程场景具有较好的性能。
默认情况下编译jemalloc后并没有jeprof工具,需要在编译时添加–enable-prof参数,然后在编译目录的bin目录中就能找到jeprof程序。
开启prof功能的jemalloc根据环境变量MALLOC_CONF和mallctl接口操作prof功能。
MALLOC_CONF变量接收的参数参考jemalloc prof功能。
缓慢内存泄漏定位方案
这个方案适用于服务内存泄漏不是特别快,从服务启动完毕到oom的时间大于30秒以上,开发者有充足的时间去触发dump。
这个方案是在服务中添加dump接口,然后让开发在服务启动完毕(数据字典、索引加载完毕)后,触发一次dump,然后刷一定流量后,再触发一次dump,对比两次dump前后内存差,确定内存泄漏的位置。
需要注意的是,这种方法一定要服务启动完毕,不然服务启动过程中加载的数据就会在diff结果中,影响判断。
其次,这种方法需要要求开发者把服务里的cache关闭,cache的存在也会影响diff结果的判断。
示例服务介绍
示例服务是一个基于beast开发的一个webservice,使用cmake构建,使用conan管理依赖,代码可以在github地址上找到。
这个webservice提供两个接口,一个是正常返回数据的接口,一个是内存泄漏的接口。内存泄漏的接口用于模拟内存泄漏的场景。
采用http协议的服务是因为http服务测试起来方便,不需要手写客户端。
项目的conan配置(conanfile.txt)内容如下:
1  | []  | 
beast是boost中的一个库,glog是日志库,下文用来分析内存泄漏的工具jeprof在jemalloc库中,需要注意的是options中jemalloc的prof已经要手工开启,默认是不开启的。
这个webservice参考beast的“高级服务器”代码实现的,正常的接口的路径是“/”,逻辑如下:
1  | std::string HttpSession::processNormal() {  | 
其功能就是打个日志,然后输出一句话。
内存泄漏的接口路径为”/leak”,其逻辑如下:
1  | std::string HttpSession::processLeak() {  | 
这个接口调用一次就泄漏1MB的内存。
这个项目编译运行的步骤:
1  | 
  | 
dump接口开发
对于一些泄漏缓慢服务,使用定量dump的方式只会带来一堆的dump文件或者连dump文件都没有,影响泄漏定位效率。
为此,需要一种简单的手工触发dump的方式来协助定位内存泄漏。
在现有服务中,添加一个手工触发dump的接口,即在新的接口里调用jemalloc的dump函数:
1  | mallctl("prof.dump", nullptr, nullptr, nullptr, 0)  | 
mallctl函数是操作调用函数,”prof.dump”是进行内存情况dump操作。
实际代码可以参考github代码:
1  | std::string HttpSession::processDump() {  | 
上面接口为”/dump”。
接口使用与数据分析
服务启动前设置环境变量MALLOC_CONF=’prof:true,prof_prefix:jemalloc’ 开启prof功能。
服务启动并加载完数据后,调用dump接口生成一份基准的内存分析数据,在本例子中,使用下面命令即可:
1  | curl 'http://localhost:8080/dump';  | 
然后调用内存泄漏的接口,模拟内存泄漏,命令如下:
1  | curl 'http://localhost:8080/leak';  | 
多次调用内存泄漏的接口之后,再次调用dump接口,生成第二份内存分析数据。这时我们得到了两份数据,第一份是基准,文件名是“jemalloc.10391.0.m0.heap”,第二份是“jemalloc.10391.1.m1.heap”。接着我们使用jeprof工具来分析这两份数据,jeprof可以从conan下载的jemalloc中找到,命令如下:
1  | jeprof --dot ./bin/server --base=jemalloc.10391.0.m0.heap jemalloc.10391.1.m1.heap  | 
终端中会输出dot语法的图,将其贴到在线dot绘图网站,生成内存分配图,然后进行分析。
样例数据可以在这里找到,生成的图如下:

从图中可以看到,两个快照比较多余部分(泄漏部分)的内存在”processLeak”函数中。
快速oom定位方案
有时候某个流量触发了服务中的一个死循环,然后死循环里会申请内存,但这份内存需要在循环外的其他地方才会释放。
这时,服务就会出现瞬间oom的情况,速度之快,无法人工dump。
面对这种情况,就要开启定量dump和推出dump,需要的参数如下:
| 参数 | 含义 | 
|---|---|
| prof:true | 启动profile | 
| prof_final:true | 表示退出时prof | 
| lg_prof_interval:N | 每流转 1 « N 个字节,将采样统计数据转储到文件 | 
| prof_gdump:true | 打到新高dump | 
| prof_active:false | 不激活,用于手工激活 | 
| lg_prof_sample:N | 平均每分配出 2^N 个字节 采一次样。当 N = 0 时,意味着每次分配都采样 | 
备注: lg_prof_sample是分配计数的采样频率,lg_prof_interval是分配统计,只有被计数了才会被打印。
建议的prof环境变量设置:
1  | export MALLOC_CONF="prof_leak:true,lg_prof_sample:0,prof_final:true;lg_prof_interval:30"  | 
每分配1GB内存就打印一份数据,并且在服务退出时,也同时dump一份。
然后比较每份dump的数据的diff确认内存泄漏的位置。
需要注意的是,这种方法也没法处理cache问题。