使用 jemalloc profile memory

对于长时间运行的服务端程序,内存的使用一直是一个非常重要的监控指标,当内存的使用量一直在上升的时候,我们就需要警觉起来,因为很有可能整个系统出现了内存泄露。那么剩下的问题就比较简单了,如何动态的获知哪里有内存泄露呢?

对 Go 的程序来说,我们可以使用语言内置的 pprof 工具非常方便的对内存进行 profile,我们只需要在程序里面 import _ "net/http/pprof",这样启动的 HTTP server 服务器就能够被直接 profile 了。

但对 Rust,情况就没那么简单了。因为语言并没有内置这个功能,所以我们得想其他办法来解决。Rust 默认使用的是 jemalloc 这个内存分配器,jemalloc 提供了非常方便的 profile 功能。所以我们自然将目光放在了如何用 jemalloc 来 profile memory 以及如何与 Rust 整合上面了。

要打开 jemalloc 的 profile 功能,在编译的时候我们需要显示的带上 --enable-prof 选项,通常在 Linux 下面我们会安装 libunwind 库,这样 prof 默认就会使用 libunwind 了。另外,为了不跟系统的 malloc 这些函数有命名冲突,这里显示的给 jemalloc 加上了前缀,使用 --with-jemalloc-prefix="je_",这样我们外面就会使用 je_malloc 这种的函数名字了。

我们用官网非常简单的例子来说明内存泄露问题,如下:

void do_something(size_t i)
{
    // Leak some memory.
    je_malloc(i * 100);
}

上面的函数有一个典型的内存泄漏,我们调用 1000 次:

for (i = 0; i < 1000; i++) {
    do_something(i);
}

剩下的就是如何来定位内存问题了。

Mem Statistics

首先我们来看看 jemalloc 自己提供的统计信息,我们可以直接使用 je_malloc_stats_print(NULL, NULL, NULL) 来将 memory 的统计输出到 stderr 上面,但这个函数输出的东西比较多,并不利于实时的查看。多数时候,我们都是使用 je_mallctl 函数,得到一些关键的统计数据,然后发送给 Prometheus 来展示,这样我们就能够在 Prometheus 里面观察到整个 jemalloc 内存变化的曲线,如果持续上升,就需要报警了。

uint64_t epoch = 1;
size_t sz = sizeof(epoch);
je_mallctl("epoch", &epoch, &sz, &epoch, sz);

size_t allocated, active, mapped;
sz = sizeof(size_t);
je_mallctl("stats.allocated", &allocated, &sz, NULL, 0);
je_mallctl("stats.active", &active, &sz, NULL, 0);
je_mallctl("stats.mapped", &mapped, &sz, NULL, 0);

printf("allocated/active/mapped: %zu/%zu/%zu\n", allocated, active, mapped);

上面我们在每次 do_something 后面得到 allocated,active 以及 mapped 这些指标,然后输出:

allocated/active/mapped: 54919648/58540032/64831488
allocated/active/mapped: 55034336/58658816/64950272
allocated/active/mapped: 55149024/58777600/65069056
allocated/active/mapped: 55263712/58896384/65187840

上面需要注意,我们需要用 epoch 来让统计的 cache 更新。

Leak Check

通过统计,我们能看到整个内存的变化曲线,但到底哪里有内存问题呢?我们可以在程序结束的时候显示的输出内存泄露。仍然使用上面的程序,我们使用 JE_MALLOC_CONF="prof_leak:true,lg_prof_sample:0,prof_final:true" ./leak 来执行,当程序退出之后,会生成一个 prof heap 的文件,我们用 jeprof 工具就可以知道内存泄露了。

jeprof leak jeprof.9001.0.f.heap

Using local file leak.
Using local file jeprof.9001.0.f.heap.
Welcome to jeprof!  For help, type 'help'.
(jeprof) top
Total: 52.1 MB
    52.1 100.0% 100.0%     52.1 100.0% je_prof_backtrace
    0.0   0.0% 100.0%     52.1 100.0% __libc_start_main
    0.0   0.0% 100.0%     52.1 100.0% _start
    0.0   0.0% 100.0%     52.1 100.0% do_something
    0.0   0.0% 100.0%     52.1 100.0% imalloc (inline)
    0.0   0.0% 100.0%     52.1 100.0% imalloc_body (inline)
    0.0   0.0% 100.0%     52.1 100.0% je_malloc
    0.0   0.0% 100.0%     52.1 100.0% je_prof_alloc_prep (inline)
    0.0   0.0% 100.0%     52.1 100.0% main

Heap Profiling

使用上面的方式,我们只能在程序结束的时候输出内存泄露,实际并不适用于长时间运行的程序,幸运的时候,我们可以通过 jemalloc 的一些参数以及 mallctl 函数来显示的对内存进行 profile。在运行程序之前,我们需要设置 export JE_MALLOC_CONF="prof:true,prof_prefix:jeprof.out",它用来告诉 jemalloc 显示的打开 prof,同时自动的生成 profile 文件名。

在代码里面,我们可以使用 mallctl("prof.dump", NULL, NULL, NULL, 0); 来对当前执行的程序生成一个 mem dump,然后过一段时间之后,用相同的方法再次生成一个,在用 jeprof 工具对比两次的 dump,就大概能知道是否有内存问题了。

具体到上面的例子,我们在程序的开始和结束都使用 mallctl dump 一次 memory,然后对两次生成的 profile 文件进行对比:

jeprof --base=jeprof.out.19792.0.m0.heap profile jeprof.out.19792.1.m1.heap

Using local file profile.
Using local file jeprof.out.19792.1.m1.heap.
Welcome to jeprof!  For help, type 'help'.
(jeprof) top
Total: 53.1 MB
    53.1 100.0% 100.0%     53.1 100.0% je_prof_backtrace
    0.0   0.0% 100.0%     53.1 100.0% __libc_start_main
    0.0   0.0% 100.0%     53.1 100.0% _start
    0.0   0.0% 100.0%     53.1 100.0% do_something
    0.0   0.0% 100.0%     53.1 100.0% imalloc (inline)
    0.0   0.0% 100.0%     53.1 100.0% imalloc_body (inline)
    0.0   0.0% 100.0%     53.1 100.0% je_malloc
    0.0   0.0% 100.0%     53.1 100.0% je_prof_alloc_prep (inline)
    0.0   0.0% 100.0%     53.1 100.0% main

Rust Customized Allocator

上面说完了在 C 里面使用 jemalloc 来看内存问题,那么对于 Rust 语言来说,我们怎么处理呢?Rust 默认使用的就是 jemalloc,但发布的版本里面 jemalloc 并没有带上 profile 的功能,所以需要重新编译 Rust,对于我们来说,因为要实时的跟进 Rust 的版本,这并不是一个好办法。

幸运的是,Rust 提供了 custom allocator 的功能,也就是能使用自定义的 allocator,这样对我们来说就简单很多了,使用一个打开了 profile 功能的 jemalloc 用作自定义的 allocator,这样就能通过 mallctl 来 profile memory 了。更幸运的是,Rust 的一个开发者已经提供了相关的 allocator,我们可以直接使用。

我们可以构造一个非常简单的 case,使用 mem::forget

fn do_something()
{
   let mut bad_vec = Vec::new();
   for _ in 0..1024 {
       bad_vec.push('0');
   }
   mem::forget(bad_vec);
}

在这个函数前后,我们都使用 mallctl ,如下:

let epoch_name = "prof.dump";
let epoch_c_name = CString::new(epoch_name).unwrap();
mallctl(epoch_c_name.as_ptr(), null_mut(), null_mut(), null_mut(), 0);

执行之后,就会生成两个 profile 文件,使用 jeprof 之后,得到:

(jeprof) top
Total: 0.5 MB
     0.5 100.0% 100.0%      0.5 100.0% jemallocator::__rust_reallocate
     0.0   0.0% 100.0%      0.5 100.0% __libc_start_main
     0.0   0.0% 100.0%      0.5 100.0% _start
     0.0   0.0% 100.0%      0.5 100.0% alloc::heap::reallocate::h1264a9399460da6c
     0.0   0.0% 100.0%      0.5 100.0% alloc::raw_vec::{{impl}}::double
     0.0   0.0% 100.0%      0.5 100.0% collections::vec::{{impl}}::push
     0.0   0.0% 100.0%      0.5 100.0% core::ops::FnOnce::call_once (inline)
     0.0   0.0% 100.0%      0.5 100.0% main
     0.0   0.0% 100.0%      0.5 100.0% my_allocator::do_something::h4ffe20b1f68a3f80
     0.0   0.0% 100.0%      0.5 100.0% my_allocator::main::hffe46171bdd5ea12

小结

内存问题一直是长时间运行程序需要处理的一个棘手问题,虽然 Rust 相比 C 以及 CPP,在内存处理上面有了很大的改善,但我们仍然可能会有引用泄露等问题出现,这些问题很难通过直接浏览代码,看 log 和 metrics 来看出来的,而 profile 恰恰能很好的解决,所以这也是我们一直想在 TiKV 上面加入 profile memory 的原因。

需要注意,加入 profile 之后,会影响系统的性能,所以通常,我们都会采用 sample 的方式或者动态的打开或者关闭 profile 功能。譬如,假设我们要 profile memory,就使用 mallctlprof.active 打开,一段时间,在使用 mallctl dump 出 memory,然后在关闭 profile。

另外,除了 jemalloc,其实 tcmalloc 也照样能支持 profile memory,只是因为 Rust 默认使用的是 jemalloc,我们最终我们还是决定基于 jemalloc 来使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容