Jemalloc切片-跟踪内存泄漏和内存增长
Jemalloc切片-跟踪内存泄漏和内存增长
使用Jemalloc Profiling跟踪内存泄漏和内存增长问题
今天给大家介绍一种依靠内存分配器自带能力来跟踪内存泄漏问题的方法。保证这是一个好的方法,是经过多年检验的方法。
但是,这不是最好的,因为没有银弹。
1. 开篇问题
在一个内存密集型的代码量超过40W行的C++编写的系统中:
定位一个慢内存泄漏问题(比如跑了1天,泄漏了10GB内存),该怎么办?
启动运行10分钟后,服务器内存消耗了20GB,然后内存就稳定了,存在内存消耗过高的问题,为了优化内存消耗,现在需要找出这些内存消耗的热点,该怎么办?
如果你维护的项目代码总量不过数千行。
或者你对业务和代码细节足够熟悉,以至于你知道每一个malloc/new申请了多少内存,哪个流程最耗内存(我想你应该没有这么有信心)。
那么今天介绍的方法或许对你的意义不大。
如果你遇到了开篇提出的类似问题。
并且:
你刚接手这个规模较大的系统,
或者你正在维护由非常多的模块构成的一个大系统,
或者现在的情况比较棘手,你需要把锅甩出去,急于撇清跟自己所维护的模块的关系。
并且:
你曾经尝试过内存问题跟踪神器valgrind,但是你实在受不了它乌龟一样的速度,
或者很不幸,你的工程在使用valgrind时遇到了各种报错,根本跑步起来。
并且:
你的程序不是一个对实时性有变态要求的程序。
并且:
你遭遇的这个问题最好是能够重现的,或者很容易重现的。
如果不能重现,不好意思这不是什么问题,不用看这篇文章,直接交给运营去填坑,让他们在24点时重启下程序吧。
并且:
你有权力重启你的程序。
并且:
最好不是生产环境。
并且:
你打开了中文搜索引擎,搜索了“怎么跟踪内存泄漏”,没有找到适合的方法。
并且:
你至少得有1天的时间,不然你踩到了坑,时间不就没有了吗,所以给自己预留些时间。
并且:
你的程序在Linux上运行,如果是Windows,我想你应该有更好的方法。
Windows上应该也可以,不过我还没研究过。
没有并且了。
那么,你可以了解下本文提供的方法。
我想你应该是C/C++程序员,不然你怎么会遇到这些问题?
或者你遇到了这些问题怎么会来这里找方法?
其实今天讲的方法对java/python等其他语言也适用,不过你可能还得借助些其他的工具。
2. 关于jemalloc
C/C++程序员都使用过内存分配器,不管你知不知道什么是内存分配器,只要你调用过malloc函数,你就使用了它。
在linux上,ptmalloc是glibc的默认内存分配器,它提供了malloc和free这样的动态分配内存和归还的能力。
本文讲的jemalloc是另外一个,除此之外还有好多内存分配器,比较出名的还有:tcmalloc,mimalloc。
如果你之前不知道它们的存在,你可能需要花几分钟去了解一下了。
之所以有这么多种内存分配器,主要还是为了追求内存使用的性能表现,包括减少CPU开销,减少内存使用量。
内存分配器的性能问题是另外一个非常大的话题,本文不展开讨论。
jemalloc出自facebook,tcmalloc出自google,mimalloc出自microsoft。
并且,都开源:
jemalloc:
https://github.com/jemalloc/jemalloc
tcmalloc:
https://github.com/google/tcmalloc
mimalloc:
https://github.com/microsoft/mimalloc
巨头对基础技术的研究还是很舍得投入。
这几个内存分配器我都尝试过,它们都很优秀。
今天的主题还是内存切片问题,所以得以jemalloc为例,其实tcmalloc也具备类似的能力。
3. 内存泄漏
关于内存泄漏问题,大家可以去读读 维基百科 和 百度百科 的描述,很有参考价值。
内存泄漏问题,表现为随着时间的前进,物理内存消耗越来越多。
严重时,以至于发展到系统无物理内存可分配,导致系统主动进行OOM,最终造成程序被操作系统异常终止。
不同的原因造成的内存泄漏表现也各不相同。
泄漏的内在含义表现在:资源和 可控性,这两者上。
资源:
资源当然是内存资源,在linux操作系统上内存资源包含2个大的部分:虚拟内存资源 和 物理内存资源。
如果泄漏仅仅发生在虚拟内存资源上,那么你可能不会那么快感受到问题,现代的CPU和操作系统往往都是64位的,他们拥有非常大的地址空间,不是一下就能消耗完。
物理内存资源往往相对比较紧缺,大量的泄漏必定造成资源匮乏。
资源都有存放的位置,内存泄漏往往是发生在进程空间中的堆空间中,堆是动态内存申请的最主要的场地,也是我们工程实践过程中内存泄漏发生的主要位置。
也有一部分内存泄漏发生在栈空间中,但是非常少见,举个例子,频繁的创建线程而不回收线程就会造成栈内存泄露,当然不回收线程带来的后果比栈内存泄漏更严重。
可控性:
只有内存资源的控制权丢失,程序内存消耗表现不符合业务需求,这种情况才是内存泄漏。
内存资源的控制权本应该掌握在编码者的预期表现所框定的范围内,但是程序可能由于各种原因出现了偏差,所以是编程者丧失了对程序行为表现的控制权。
比如是编码者忘记了释放申请的内存,或者是智能指针循环引用,或者内存句柄被破坏等等等具体的泄漏原因,这些都是由于内存资源的控制权丧失造成内存资源不可控而出现的内存泄漏问题。
内存泄漏问题可以根据其特性按照多个维度进行分类,大致有:
触发条件
时间特性
泄漏内存量
泄漏位置数量
触发条件
指泄漏出现的条件,可以分为:偶发、必现。
时间特性
指程序的生命周期内,泄漏出现的时间情况,大体可分为:持续性泄漏 和 非持续性泄漏,细一点可以分为启动时泄漏,运行过程中泄漏,退出时泄漏,长时间,短时间等等。
泄漏内存量
指单次泄漏发生时所消耗的内存量,这个维度本身可被量化,但大体可分为:少量泄漏,大量泄漏。
泄漏位置数量
指程序中引发泄漏的问题出处,大体可分为:少处泄漏,多处泄漏。
有了时间和泄漏量就能有泄露速度,可以延伸出泄漏的速度:快速泄漏和缓慢泄漏。
从功用的角度来看,并非所有的内存泄露都需要花费时间去跟踪,比如只在系统退出时才会产生的泄漏,比如非常少量的非持续性泄漏。
强迫症同事不允许有瑕疵,怎么能存在泄漏?必须消灭。
哪种内存泄露最难跟踪?
是这种:
偶发性,持续,缓慢,多处内存泄露。
如果你的项目遇到这种情况,不好意思,准备好掉头发。
但是转念一想,维护糟糕的项目能够发展你的核心技能。
跟踪内存泄漏问题往往使用动态跟踪技术,而非静态检查。
jemalloc的Profiling就是动态跟踪技术,在程序运行时分析。
4. Profiling的内在逻辑
【切片】
一词,是自己瞎翻译的,反正国内对这个问题也没有业界统一的叫法,就让我用捉急的英文勉强翻译一下。
在
jemalloc的官方文档上叫Profiling,简写成p
rof。
词典对Profiling的解释是:
描…的轮廓
切片要表达一个意思:
截取大的物体上的一点东西下来分析,以获得物体的一个概要信息。
准确的说,这里的Profiling是指Heap Profiling,堆内存切片。
既然是堆内存分配器,当然就只能管理堆上的内存。
jemalloc内部会有一个抽象意义上的allocate table(不是真的有),用于存放内存指针与内存信息的关联关系。
简化后这个表的逻辑大概是这个样子:
allocate table:
内存指针 申请的内存量
ptr1(0x000000123) 56bytes
ptr2(0x000000456) 26bytes
ptr3(0x000000789) 1024bytes
......
当开启prof时,
在应用程序调用malloc的时候,jemalloc会存储调用堆栈和申请到的内存指针,以及申请的内存大小。
free的时候将ptr指针所对应的内存申请size值从内存申请映射表中减掉。
举个例子:
ptr = malloc(size);
多次调用malloc后形成:
allocate table:
内存指针 申请的内存量
ptr1(0x000000123) 56bytes
ptr2(0x000000456) 26bytes
ptr3(0x000000789) 1024bytes
allocate table 的表达能力有限,不能以此分析输出代码问题发生的位置,需要另外使用函数调用栈来表达内存分配信息。
所以需要构造调用栈映射表(抽象意义上的,并非真有):
call stack map:
内存指针 调用栈 申请内存量(可查allocate table得到)
ptr1(0x000000123) callstack1 56bytes
ptr2(0x000000456) callstack2 26bytes
ptr3(0x000000789) callstack1 1024bytes
调用free(ptr3)后:
call stack map:
内存指针 调用栈 申请内存量(可查allocate table得到)
ptr1(0x000000123) callstack1 56bytes
ptr2(0x000000456) callstack2 26bytes
allocate table:
内存指针 申请的内存量
ptr1(0x000000001) 56bytes
ptr2(0x000000456) 26bytes
其他的类
jemalloc的工具都有类似的处理逻辑,但各个工具的内部实现细节各不相同。
概括起来,其核心就是维护了一个以 [调用栈] 为键,键值为 [复合统计值] 的 [映射表]。
当然
jemalloc中的调用栈映射表比我们这里描述的要复杂,除了总字节数之外其中还包含:
线程编号、分配对象个数、累计分配对象个数、累计分配字节数。
其中:
线程编号:
如其名字。
分配对象个数:
是
指某个调用栈尚未释放的内存申请的计数值,调用一次malloc函数该计数值增加1,调用一次fre
e函数该计数值减1。
累计分配对象个数:
是指某个调用栈总的内存申请的计数值,调用一次malloc函数该计数值增加1,调用free的时候并不会减1。这个统计值默认是关闭的,可通过prof_accum选项打开。
累计分配字节数:
是指某个调用栈总的内存申请的字节计数值,调用一次malloc函数该计数值增加申请的字节数,调用free的时候并不会减去释放的内存的字节数。这个统计值默认是关闭的,可通过prof_accum选项打开。
而且
jemalloc的heap文件中对调用栈进行了聚合,相同调用栈只有一条记录数据。
理解了本文说的这种简要模型就足够使用
jemalloc的切片能力了。
jemalloc里,在切片操作发生时,将导出内存申请映射表,生成heap文件。
最终我们可以使用jeprof工具遍历该映射表,以调用栈为中心对内存申请量进行统计,最终生成树形的内存消耗调用关系。
对于内存泄漏问题:
jemalloc直接提供了分析能力。基本原理也是在程序启动时开始进行prof记录,在程序退出时将[调用栈映射表]导出成heap文件,该heap文件就直接表明了内存泄漏的函数调用栈。
对于内存增长问题:
jemalloc提供了足够的接口,在内存增长之前激活prof记录功能,等到增长完成后,我们通过调用接口dump出[调用栈映射表]到heap文件中,该heap文件直接表明了内存增长的函数调用栈。
另外,我们也可以通过接口在内存增长之前生成一个heap1, 在内存增长后再生成一个heap2,通过jeprof工具对比heap1和heap2的差异,找出增长的函数调用栈。
内存泄漏问题我们也可以使用这种方法来跟踪。
5. Jemalloc的使用方式
我们需要安装开发版本
jemalloc库和graphviz库
jemalloc的使用方式主要有2种:
通过静态链接,链接到可执行文件中。
这需要修改代码,运行时没有任何依赖。使用LD_PRELOAD指定
jemalloc的动态库路径,预装载到进程空间中,替换ptmalloc的接口。
这种方式使用最简单,灵活度最高,因为出jemalloc之外的其他内存分配器也使用这种方式,这就能够在客户环境中非常轻松地更换内存分配器。
不过也有缺陷,动态库的方式在函数调用时会多一次符号查找,因此性能会有非常轻微的降低。
一般的程序,这点下降几乎感知不到。
6. Jemalloc的安装
我们需要安装开发版本jemalloc库和graphviz库
graphviz库用于绘图生成pdf文件
kcachegrind软件可选安装,用于交互查看调用图。
centos上安装
yum install jemalloc-devel
yum install graphviz
[可选] yum install kdesdk-kcachegrind
ubuntu上安装
apt-get install libjemalloc-dev
apt-get install graphviz
[可选] apt-get install kcachegrind
我们可以通过查看安装后的文件列表来获取jemalloc的库文件和头文件位置:
在centos上执行
rpm -ql jemalloc-devel
在ubuntu上执行
dpkg -L libjemalloc-dev
结果如下:
/.
/usr
/usr/bin
/usr/bin/jeprof
/usr/include
/usr/include/jemalloc
/usr/include/jemalloc/jemalloc.h
/usr/lib
/usr/lib/x86_64-linux-gnu
/usr/lib/x86_64-linux-gnu/libjemalloc.a
/usr/lib/x86_64-linux-gnu/libjemalloc_pic.a
/usr/lib/x86_64-linux-gnu/pkgconfig
/usr/lib/x86_64-linux-gnu/pkgconfig/jemalloc.pc
/usr/share
/usr/share/doc
/usr/share/doc/libjemalloc-dev
/usr/share/doc/libjemalloc-dev/copyright
/usr/share/doc/libjemalloc-dev/jemalloc.html
/usr/share/doc-base
/usr/share/doc-base/libjemalloc-dev
/usr/share/man
/usr/share/man/man1
/usr/share/man/man1/jeprof.1.gz
/usr/share/man/man3
/usr/share/man/man3/jemalloc.3.gz
/usr/lib/x86_64-linux-gnu/libjemalloc.so
/usr/share/doc/libjemalloc-dev/README
/usr/share/doc/libjemalloc-dev/changelog.Debian.gz
7. Jemalloc关键文件说明
一般,我们主要使用如下几个文件:
使用LD_PRELOAD时,仅需使用动态库:
/usr/lib/x86_64-linux-gnu/libjemalloc.so
使用编译时链接的方式,需要如下文件:
/usr/lib/x86_64-linux-gnu/libjemalloc.a
/usr/lib/x86_64-linux-gnu/libjemalloc_pic.a
/usr/include/jemalloc/jemalloc.h
jemalloc.h和libjemalloc.a 看名字就知道,编译时静态链接需要用的头文件和静态库。
特别的:
libjemalloc_pic.a用于链接那些编译时使用了位置无关编译策略的程序。
要想使用内存切片功能,需要保证编译jemalloc的库时,开启了prof编译选项。
对于我们安装的rpm或者deb包,可以通过执行如下命令来测试来确认jemalloc的prof功能是否被编译:
MALLOC_CONF=stats_print:true LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so sed "xxxxx" ./*
如果prof功能被编译,则输出结果中的config.prof一项值应该为true
sed: -e expression #1, char 2: extra characters after command
___ Begin jemalloc statistics ___
Version: "5.2.0-0-gb0b3e49a54ec29e32636f4577d9d5a896d67fd20"
Build-time option settings
config.cache_oblivious: true
config.debug: false
config.fill: true
config.lazy_lock: false
config.malloc_conf: ""
config.prof: true
config.prof_libgcc: true
config.prof_libunwind: false
config.stats: true
......
关注这项:
config.prof: true
还有我们的切片分析工具
/usr/bin/jeprof
jeprof是一个脚本工具,由perl语言所编写。
主要提供的能力:
反汇编指定函数;
分析和可视化jemalloc生成的heap文件,可使用top命令查看消耗热点函数排序结果;
多种格式的分析结果输出:pdf文件、文本、callgrind等多种格式;
对比2个不同的heap文件的差异;
8. 编译带prof功能的jemalloc库
如果通过yum和apt安装的libjemalloc没有prof功能,就需要自己手动编译。
步骤很简单:
wget https://github.com/jemalloc/jemalloc/archive/refs/tags/5.2.1.tar.gz
tar -zxvf 5.2.1.tar.gz
cd jemalloc-5.2.1/
./autogen.sh
./configure --prefix=/usr/local/jemalloc-5.2.1 --enable-prof
make -j
make install
9. 内存泄漏和内存增长测试例子
我们编写一个测试代码,用于验证jemalloc的Profiling能力。
该测试程序提供多个函数调用的路径去申请内存,并且随机调用free,模拟内存泄漏和内存增长。
其中包含了调用jemalloc接口的例子。
主要使用了2个接口:
int mallctl( const char *name,
void *oldp,
size_t *oldlenp,
void *newp,
size_t newlen);
void malloc_stats_print( void (*write_cb) (void *, const char *) ,
void *cbopaque,
const char *opts);
主要通过mallctl来实现对jemalloc的控制。
通过向测试程序发送signal来控制程序的行为,如下:
信号ID 动作
SIGUSR1 进行Profiling的heap文件dump操作
SIGUSR2 激活Profiling,新的malloc从此时才能被记录
SIGRTMIN+10 输出jemalloc的内部统计信息
代码如下:
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <random>
#include <signal.h>
#include <unistd.h>
#include <jemalloc/jemalloc.h>
//生成指定范围的随机数
template<typename T>
T random(T range_from, T range_to) {
std::random_device rand_dev;
std::mt19937 generator(rand_dev());
std::uniform_int_distribution<T> distr(range_from, range_to);
return distr(generator);
}
//为了触发缺页中断分配物理内存
__attribute__ ((noinline))
void access_ptr(void* ptr, size_t s){
if(nullptr == ptr) return;
for(size_t i = 0; i < s; ++i){
((uint8_t*)ptr)[i] = i;
}
}
__attribute__ ((noinline))
void fun_leak_1(){
void* p = malloc(1*1024);
access_ptr(p, 1*1024);
if(random<uint32_t>(0, 1)){
free(p);
}
}
__attribute__ ((noinline))
void fun_leak_2(){
void* p = malloc(2*1024);
access_ptr(p, 2*1024);
if(random<uint32_t>(0, 1)){
free(p);
}
}
__attribute__ ((noinline))
void fun_leak_3(){
void* p = malloc(5*1024);
access_ptr(p, 5*1024);
if(random<uint32_t>(0, 1)){
free(p);
}
}
__attribute__ ((noinline))
void fun_leak_4(){
void* p = malloc(10*1024);
access_ptr(p, 10*1024);
if(random<uint32_t>(0, 1)){
free(p);
}
}
__attribute__ ((noinline))
void fun_leak_recu(uint32_t &level){
if(level > 6){
return ;
}
if(level <= 2 || random<uint32_t>(0, 1)){
fun_leak_recu(++level);
}
uint32_t w = random<uint32_t>(1, 4);
switch (w)
{
case 1:
fun_leak_1();
break;
case 2:
fun_leak_2();
break;
case 3:
fun_leak_3();
break;
case 4:
fun_leak_4();
break;
default:
break;
}
return;
}
__attribute__ ((noinline))
void fun_big_malloc(){
void * p = malloc(10*1024);
}
__attribute__ ((noinline))
void fun_root(){
fun_big_malloc();
uint32_t level = 0;
fun_leak_recu(level);
}
static void sig_usr(int signo)
{
if (signo == SIGUSR1)
{
std::cout << "received SIGUSR1, run prof.dump!\n" << std::endl;
mallctl("prof.dump", NULL, NULL, NULL, 0);
}
else if (signo == SIGUSR2)
{
std::cout << "received SIGUSR2, run prof.active!\n" << std::endl;
bool active = true;
mallctl("prof.active", NULL, NULL, &active, sizeof(active));
}
else if (signo == (SIGRTMIN+10)){
std::cout << "received SIGRTMIN+10, run malloc_stats_print!\n" << std::endl;
malloc_stats_print(nullptr, nullptr, "");
}
}
void RegistrySignal(){
if (signal(SIGUSR1, sig_usr) == SIG_ERR)
{
std::cout << "signal1 error!" << std::endl;
}
if (signal(SIGUSR2, sig_usr) == SIG_ERR)
{
std::cout << "signal2 error!" << std::endl;
}
if (signal(SIGRTMIN+10, sig_usr) == SIG_ERR)
{
std::cout << "signal3 error!" << std::endl;
}
}
int main(int argc, char **argv)
{
std::cout << "--->Begin test" << std::endl;
RegistrySignal();
std::cout << "--->wait op" << std::endl;
//等待3秒,以方便外部操作
usleep(3*1000*1000);
std::cout << "--->Begin allocate" << std::endl;
for (int32_t i = 0; i < 100; i++){
fun_root();
usleep(200000);
}
std::cout << "--->wait op2" << std::endl;
//等待3秒,以方便外部操作
usleep(3*1000*1000);
std::cout << "--->End test" << std::endl;
return 0;
}
10. 编译测试程序
目录结构如下:
root@# ls -lh
-rw-r--r-- 1 root root 4.7K 9月 2 09:08 jemalloc_heap_profiling.cpp
drwxr-xr-x 2 root root 4.0K 9月 2 10:43 jeprof
链接静态库编译:
g++ -o jemalloc_profiling_test jemalloc_heap_profiling.cpp -std=c++11 -g -m64 -l:libjemalloc.a -lpthread -ldl
链接动态库编译:
g++ -o jemalloc_profiling_test jemalloc_heap_profiling.cpp -std=c++11 -g -m64 -ljemalloc
其他编译说明:
最好带上调试符号,否则有可能解析不出调用堆栈。
如果程序在客户现场出现问题,并且程序已经过瘦身,情况紧急,需要先抓取数据,只要被裁剪的符号还在,也有办法解析出调用栈。
基本思路:
把裁剪后的符号通过eu-unstrip命令再拼接回去,然后再使用jeprof工具,分析heap文件和带符号信息的可执行文件。
jeprof的--add_lib参数可以指定带符号的库路径,--lib_prefix参数可指定库路径前缀,--lines参数可以显示代码对应位置。
如果不想使用这些参数,也可以将出现问题的可执行文件部署在与现场相同的路径上,然再使用jeprof分析。
11. Jemalloc运行时控制
在测试之前,先介绍一下jemalloc运行时的控制方法。
11.1 通过环境变量
通过设置环境变量MALLOC_CONF,我们可以控制jemalloc在运行时的行为,具体的配置项请参考jemalloc的官方手册。例如:
MALLOC_CONF=stats_print:true test_program
多个选项使用“,”分割,例如:
MALLOC_CONF=prof:true,stats_print:true test_program
11.2 通过接口
MALLOC_CONF中可配置的选项也可以通过接口调用的方式,例如:
bool active = true;
mallctl("prof.active", NULL, NULL, &active, sizeof(active));
11.3 启用和激活prof
jemalloc提供了profiling功能的两种开启方式:启用和激活。
必须先启用才能激活,只有激活了才能进行profiling分析。
选项prof可用于开启profiling功能,如:
启用:
MALLOC_CONF=prof:true
禁用:
MALLOC_CONF=prof:false
选项prof_active可用于激活profiling功能,如:
暂停profiling:
MALLOC_CONF=prof_active:false
激活profiling:
MALLOC_CONF=prof_active:true
对于运行一段时间后才激活prof功能的情况,有可能错过内存分配的时机,造成切片不准确。
11.4 profiling的输出
prof会将内部的函数调用映射表导出生成后缀为"heap"的文件。
配置heap文件的输出路径
prof_prefix:./jeprof/test
“./jeprof/test”指示的是生成的heap文件的前缀,准确来说是路径前缀,生成文件的路径如下:
./jepprof/test.xxx.heap
11.5 profiling的基本流程:
概括说就是2个流程:
采样 -> 导出
采样的目的是为了忽略一些细节,保留一个整体概要信息。
导出是指将内存中的分配信息导出到文件中。
其中采样流程可控制采集间隔参数:
选项:lg_prof_sample:19
其中19指512KiB(2^19 B)。
该参数的含义:
内存分配采样之间的平均间隔,表示分配的字节数。
增加采样间隔降低了切片的保真度,但也降低了计算开销。默认的采样间隔为512KiB(2^19B)。
将lg_prof_sample设置为0时能够最大程度的保留内存分配的信息。
11.6 触发profiling的2类方法
- jemalloc内部自动切片;
分为:
a. 退出时dump
配置项:prof_final:true
b. 超过上一次内存消耗最大值时dump
配置项:prof_gdump:true
c. 增量达到门限时dump
配置项:lg_prof_interval:12
12表示4KB(2^12)
- 应用程序通过接口完成;
mallctl("prof.dump", NULL, NULL, NULL, 0);
12. 测试:直接使用内存泄漏检查功能
Jemalloc直接提供了内存泄漏检查的能力,它会再程序正常结束时检查内存泄漏,并输出heap文件。以如下方式运行编译好的测试程序:
root@# MALLOC_CONF=prof_leak:true,lg_prof_sample:0,prof_final:true,prof_prefix:./jeprof/test ./jemalloc_profiling_test
--->Begin test
--->wait op
--->End test
<jemalloc>: Leak approximation summary: ~2182144 bytes, ~336 objects, >= 30 contexts
<jemalloc>: Run jeprof on "./jeprof/test.71646.0.f.heap" for leak detail
关键就是“
prof_leak:true”和“
prof_final:true”发挥了作用。
“
lg_prof_sample:0”指示着不要错过任何分配,尽最大力量还原信息,当然,这是有开销的。
我们看到jemalloc的内存泄漏报告和生成的heap文件:
<jemalloc>: Leak approximation summary: ~2182144 bytes, ~336 objects, >= 30 contexts
<jemalloc>: Run jeprof on "./jeprof/test.71646.0.f.heap" for leak detail
查看下heap文件:
root@# ls ./jeprof/ -lht
total 12K
-rw-r--r-- 1 root root 12K 9月 2 10:40 test.71646.0.f.heap
我需要通过jeprof工具来对heap文件进行分析。
以交互方式查看,如下:
root@# jeprof --show_bytes ./jemalloc_profiling_test ./jeprof/test.71646.0.f.heap
Using local file ./jemalloc_profiling_test.
Using local file ./jeprof/test.71646.0.f.heap.
Welcome to jeprof! For help, type 'help'.
(jeprof) top
Total: 2182144 B
2182144 100.0% 100.0% 2182144 100.0% je_prof_backtrace
0 0.0% 100.0% 81920 3.8% GLIBC_2.2.5
0 0.0% 100.0% 1024 0.0% _IO_doallocbuf
0 0.0% 100.0% 1024 0.0% _IO_file_doallocate
0 0.0% 100.0% 1024 0.0% _IO_file_overflow
0 0.0% 100.0% 1024 0.0% _IO_file_xsputn
0 0.0% 100.0% 2100224 96.2% __libc_start_main
0 0.0% 100.0% 81920 3.8% _dl_rtld_di_serinfo
0 0.0% 100.0% 2100224 96.2% _start
0 0.0% 100.0% 1024000 46.9% fun_big_malloc
(jeprof) top33
Total: 2182144 B
2182144 100.0% 100.0% 2182144 100.0% je_prof_backtrace
0 0.0% 100.0% 81920 3.8% GLIBC_2.2.5
0 0.0% 100.0% 1024 0.0% _IO_doallocbuf
0 0.0% 100.0% 1024 0.0% _IO_file_doallocate
0 0.0% 100.0% 1024 0.0% _IO_file_overflow
0 0.0% 100.0% 1024 0.0% _IO_file_xsputn
0 0.0% 100.0% 2100224 96.2% __libc_start_main
0 0.0% 100.0% 81920 3.8% _dl_rtld_di_serinfo
0 0.0% 100.0% 2100224 96.2% _start
0 0.0% 100.0% 1024000 46.9% fun_big_malloc
0 0.0% 100.0% 58368 2.7% fun_leak_1
0 0.0% 100.0% 110592 5.1% fun_leak_2
0 0.0% 100.0% 353280 16.2% fun_leak_3
0 0.0% 100.0% 552960 25.3% fun_leak_4
0 0.0% 100.0% 1075200 49.3% fun_leak_recu
0 0.0% 100.0% 2099200 96.2% fun_root
0 0.0% 100.0% 1024 0.0% fwrite
0 0.0% 100.0% 2182144 100.0% je_malloc_default
0 0.0% 100.0% 2100224 96.2% main
0 0.0% 100.0% 1024 0.0% std::__ostream_insert
0 0.0% 100.0% 81920 3.8% std::__throw_ios_failure
0 0.0% 100.0% 1024 0.0% std::operator<<
(jeprof) disasm fun_leak_recu
Total: 2182144 B
ROUTINE ====================== fun_leak_recu
0 1915904 B (flat, cumulative) 87.8% of total
-------------------- ...sts/jemalloc_heap_profiling/jemalloc_heap_profiling.cpp
. . 92: void fun_leak_recu(uint32_t &level){
. . 7d15: endbr64
. . 7d19: push %rbp
. . 7d1a: mov %rsp,%rbp
. . 7d1d: sub $0x20,%rsp
. . 7d21: mov %rdi,-0x18(%rbp)
. . 94: if(level > 6){
. . 7d25: mov -0x18(%rbp),%rax
. . 7d29: mov (%rax),%eax
. . 7d2b: cmp $0x6,%eax
. . 7d2e: ja 7dd4 <fun_leak_recu+0xbf>
. . 98: if(level <= 2 || random<uint32_t>(0, 1)){
. . 7d34: mov -0x18(%rbp),%rax
. . 7d38: mov (%rax),%eax
. . 7d3a: cmp $0x2,%eax
. . 7d3d: jbe 7d52 <fun_leak_recu+0x3d>
. . 7d3f: mov $0x1,%esi
. . 7d44: mov $0x0,%edi
. . 7d49: callq 8272 <unsigned int random>
. . 7d4e: test %eax,%eax
. . 7d50: je 7d59 <fun_leak_recu+0x44>
. . 7d52: mov $0x1,%eax
. . 7d57: jmp 7d5e <fun_leak_recu+0x49>
. . 7d59: mov $0x0,%eax
. . 7d5e: test %al,%al
. . 7d60: je 7d7d <fun_leak_recu+0x68>
. 840704 99: fun_leak_recu(++level);
. . 7d62: mov -0x18(%rbp),%rax
. . 7d66: mov (%rax),%eax
. . 7d68: lea 0x1(%rax),%edx
. . 7d6b: mov -0x18(%rbp),%rax
. . 7d6f: mov %edx,(%rax)
. . 7d71: mov -0x18(%rbp),%rax
. . 7d75: mov %rax,%rdi
. 840704 7d78: callq 7d15 <fun_leak_recu>
. . 102: uint32_t w = random<uint32_t>(1, 4);
. . 7d7d: mov $0x4,%esi
. . 7d82: mov $0x1,%edi
. . 7d87: callq 8272 <unsigned int random>
. . 7d8c: mov %eax,-0x4(%rbp)
. . 103: switch (w)
. . 7d8f: cmpl $0x4,-0x4(%rbp)
. . 7d93: je 7dca <fun_leak_recu+0xb5>
. . 7d95: cmpl $0x4,-0x4(%rbp)
. . 7d99: ja 7dd1 <fun_leak_recu+0xbc>
. . 7d9b: cmpl $0x3,-0x4(%rbp)
. . 7d9f: je 7dc3 <fun_leak_recu+0xae>
. . 7da1: cmpl $0x3,-0x4(%rbp)
. . 7da5: ja 7dd1 <fun_leak_recu+0xbc>
. . 7da7: cmpl $0x1,-0x4(%rbp)
. . 7dab: je 7db5 <fun_leak_recu+0xa0>
. . 7dad: cmpl $0x2,-0x4(%rbp)
. . 7db1: je 7dbc <fun_leak_recu+0xa7>
. . 118: break;
. . 7db3: jmp 7dd1 <fun_leak_recu+0xbc>
. 58368 106: fun_leak_1();
. 58368 7db5: callq 7bcd <fun_leak_1>
. . 107: break;
. . 7dba: jmp 7dd2 <fun_leak_recu+0xbd>
. 110592 109: fun_leak_2();
. 110592 7dbc: callq 7c1f <fun_leak_2>
. . 110: break;
. . 7dc1: jmp 7dd2 <fun_leak_recu+0xbd>
. 353280 112: fun_leak_3();
. 353280 7dc3: callq 7c71 <fun_leak_3>
. . 113: break;
. . 7dc8: jmp 7dd2 <fun_leak_recu+0xbd>
. 552960 115: fun_leak_4();
. 552960 7dca: callq 7cc3 <fun_leak_4>
. . 116: break;
. . 7dcf: jmp 7dd2 <fun_leak_recu+0xbd>
. . 118: break;
. . 7dd1: nop
. . 121: return;
. . 7dd2: jmp 7dd5 <fun_leak_recu+0xc0>
. . 95: return ;
. . 7dd4: nop
. . 122: }
. . 7dd5: leaveq
. . 7dd6: retq
(jeprof) quit
root@#
我们看到了泄漏最高的函数名字:
0 0.0% 100.0% 2100224 96.2% _start
0 0.0% 100.0% 1024000 46.9% fun_big_malloc
0 0.0% 100.0% 58368 2.7% fun_leak_1
0 0.0% 100.0% 110592 5.1% fun_leak_2
0 0.0% 100.0% 353280 16.2% fun_leak_3
0 0.0% 100.0% 552960 25.3% fun_leak_4
0 0.0% 100.0% 1075200 49.3% fun_leak_recu
0 0.0% 100.0% 2099200 96.2% fun_root
由于没有分析累计统计,所以前几列的值都为0,这不影响我们的分析。
我们可以同pdf的方式来查看,以获得更直观的结果,如下:
root@# jeprof --show_bytes --pdf ./jemalloc_profiling_test ./jeprof/test.71646.0.f.heap > ./jeprof/je_check_leak.pdf
Using local file ./jemalloc_profiling_test.
Using local file ./jeprof/test.71646.0.f.heap.
Dropping nodes with <= 10910 B; edges with <= 2182 abs(B)
我们打开
“
./jeprof/je_check_leak.pdf”
可以看到:

内存泄漏的函数调用路径一目了然。
13. 测试:通过heap对比来分析泄漏和增长
通过heap对比来分析泄漏和增长。
在一个shell窗口中启动测试程序:
root@# mkdir jeprof
root@# MALLOC_CONF=prof:true,lg_prof_sample:0,prof_active:false,prof_prefix:./jeprof/test ./jemalloc_profiling_test
--->Begin test
--->wait op
received SIGUSR2, run prof.active!
--->Begin allocate
received SIGUSR1, run prof.dump!
received SIGUSR1, run prof.dump!
received SIGUSR1, run prof.dump!
received SIGUSR1, run prof.dump!
--->wait op2
--->End test
在另一个shell窗口中发送控制信号:
root@# kill -SIGUSR2 $(pidof jemalloc_profiling_test)
root@# kill -SIGUSR1 $(pidof jemalloc_profiling_test)
root@# kill -SIGUSR1 $(pidof jemalloc_profiling_test)
root@# kill -SIGUSR1 $(pidof jemalloc_profiling_test)
root@# kill -SIGUSR1 $(pidof jemalloc_profiling_test)
启动时,我们将prof_active设置为false,此时jemalloc不会进行prof采样。
在操作等待时间内我们,发送了prof激活信号。
然后进行了多次dump操作。
最终生成的结果:
root@# ls ./jeprof/ -lht
total 68K
-rw-r--r-- 1 root root 11K 9月 2 11:12 test.72297.3.m3.heap
-rw-r--r-- 1 root root 11K 9月 2 11:12 test.72297.2.m2.heap
-rw-r--r-- 1 root root 9.5K 9月 2 11:11 test.72297.1.m1.heap
-rw-r--r-- 1 root root 5.6K 9月 2 11:11 test.72297.0.m0.heap
分析结果:
一共dump了4次,生成了4个heap文件
test.72297.0.m0.heap是在激活prof后,第一次生成的heap,此时并未执行任何的内存申请操作,因此其中不包含映射表。
我们挑选test.72297.2.m2.heap和test.72297.1.m1.heap两次dump来做比较,可以分析内存泄漏发生的位置。
root@# jeprof --pdf --show_bytes ./jemalloc_profiling_test ./jeprof/test.72297.2.m2.heap --base=./jeprof/test.72297.1.m1.heap > ./jeprof/je_leak_grow_api_1.pdf
Using local file ./jemalloc_profiling_test.
Using local file ./jeprof/test.72297.2.m2.heap.
Dropping nodes with <= 1853 B; edges with <= 370 abs(B)
结果如下:

可以再对比最早的和最晚的2次heap:
root@# jeprof --pdf --show_bytes ./jemalloc_profiling_test ./jeprof/test.72297.3.m3.heap --base=./jeprof/test.72297.0.m0.heap > ./jeprof/je_leak_grow_api_2.pdf
Using local file ./jemalloc_profiling_test.
Using local file ./jeprof/test.72297.3.m3.heap.
Dropping nodes with <= 7808 B; edges with <= 1561 abs(B)
结果如下:

通过pdf图我们看到了所有内存增长的位置。
检查代码会发现泄漏的调用路径和内存增长的位置与图中吻合。
14. jeprof工具的用法举例
jeprof还有非常多的其它用法,本文再列举些例子。
以文本形式输出:
jeprof --show_bytes --text ./jemalloc_profiling_test ./jeprof/test.55046.0.m0.heap
当调用栈很多时,jeprof会省略许多信息,此时想查看细微的信息,可以这样:
jeprof --pdf --inuse_space --show_bytes --nodecount=1000 --nodefraction=0.000001 --edgefraction=0.000001 --maxdegree=100 ./jemalloc_profiling_test ./jeprof/test.55046.0.m0.heap > test.pdf
显示代码行数:
jeprof --lines --show_bytes ./jemalloc_profiling_test ./jeprof/test.72297.3.m3.heap --base=./jeprof/test.72297.0.m0.heap
以callgrind格式输出:
jeprof --callgrind --show_bytes ./jemalloc_profiling_test ./jeprof/test.72297.3.m3.heap > cachegrind.out
将生成的“
cachegrind.out”用kcachegrind打开:

其他用法请参考jeprof的help信息。
jeprof -h
15. Jemalloc内存切片的缺陷
如开篇所说,Jemalloc切片方法并非万能,它有自己的缺陷:
1. 系统会变慢
每个malloc和每个free的函数调用都需要做更多额外操作,而且这些操作jemalloc的优化不是很充分。
一旦开启并激活了prof后,程序的性能会有大幅度下降,一倍到数倍不等等,与应用程序的特性相关,内存申请释放越频繁性能下降越明显。
有些场景下,缓慢的系统无法复现问题,甚至会产生新的阻塞造成新的内存增长问题,此时jemalloc的prof功能就无法胜任了。
面对这个问题,我们可以通过其他手段来解决,后边有机会再说。
2. 无法分析消耗大量虚拟内存,但表现未物理内存泄露泄漏的问题
频繁使用虚拟内存预分配的编程风格,调用了malloc,实际运行的过程中业务流程却未充分使用这些内存,虚拟内存增长很快,但是物理内存却未消耗。
内存泄漏问题往往更关注物理内存的泄漏,此种场景下,jemalloc的分析结果就变得不可信了,以此为分析的依据甚至会得到错误的结论。
面对这个问题,我们同样可以通过其他手段来解决,后边有机会再说。
16. 最后
如果你的程序经常要分析内存泄漏或内存消耗问题,而且发现jemalloc很合适你的场景,建议你将其工程化,在业务上提供:导出heap文件、下载heap文件、解析heap文件的能力。
参考资料:
- [1] jemalloc的泄漏检查 https://github.com/jemalloc/jemalloc/wiki/Use-Case%3A-Leak-Checking
- [2] jemalloc的heap profiling https://www.yuanguohuo.com/2019/01/02/jemalloc-heap-profiling/
- [3] A-Heap-Profiling https://github.com/jemalloc/jemalloc/wiki/Use-Case%3A-Heap-Profiling
- [4] Memory Profiling with Mesos and Jemalloc https://mesos.apache.org/documentation/latest/memory-profiling/
- [5] jemalloc man http://jemalloc.net/jemalloc.3.html
- [6] 内存泄漏 百度百科 https://baike.baidu.com/item/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F/6181425?fr=aladdin
- [7] 内存泄漏 维基百科 https://zh.wikipedia.org/wiki/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F