使用jeprof定位内存泄漏

简介

本文将介绍基于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
2
3
4
5
6
7
8
9
10
11
12
[build_requires]

[requires]
boost/1.77.0
jemalloc/5.2.1
glog/0.5.0

[generators]
cmake

[options]
jemalloc:enable_prof=True

beast是boost中的一个库,glog是日志库,下文用来分析内存泄漏的工具jeprof在jemalloc库中,需要注意的是options中jemalloc的prof已经要手工开启,默认是不开启的。

这个webservice参考beast的“高级服务器”代码实现的,正常的接口的路径是“/”,逻辑如下:

1
2
3
4
std::string HttpSession::processNormal() {
LOG(INFO) << "visit";
return "use http://www.webgraphviz.com/ ";
}

其功能就是打个日志,然后输出一句话。

内存泄漏的接口路径为”/leak”,其逻辑如下:

1
2
3
4
5
std::string HttpSession::processLeak() {
int32_t* leak = new int32_t[1024*256];
LOG(INFO) << "leak address " << leak;
return "memory leak";
}

这个接口调用一次就泄漏1MB的内存。

这个项目编译运行的步骤:

1
2
3
4
5
6
7
8
9
10
11
# 创建编译目录
mkdir build;
cd build;
# 使用conan安装依赖
conan install ..
# 使用cmake构建
cmake ..
# 编译
make
# 运行
./bin/server

dump接口开发

对于一些泄漏缓慢服务,使用定量dump的方式只会带来一堆的dump文件或者连dump文件都没有,影响泄漏定位效率。
为此,需要一种简单的手工触发dump的方式来协助定位内存泄漏。

在现有服务中,添加一个手工触发dump的接口,即在新的接口里调用jemalloc的dump函数:

1
mallctl("prof.dump", nullptr, nullptr, nullptr, 0)

mallctl函数是操作调用函数,”prof.dump”是进行内存情况dump操作。

实际代码可以参考github代码:

1
2
3
4
5
6
7
8
9
std::string HttpSession::processDump() {
if (mallctl("prof.dump", nullptr, nullptr, nullptr, 0) == 0) {
LOG(INFO) << "dump sucess";
return "dump success";
} else {
LOG(INFO) << "dump sucess";
return "dump fail";
}
}

上面接口为”/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问题。

参考