使用 jemalloc profile memory

對(duì)于長(zhǎng)時(shí)間運(yùn)行的服務(wù)端程序,內(nèi)存的使用一直是一個(gè)非常重要的監(jiān)控指標(biāo),當(dāng)內(nèi)存的使用量一直在上升的時(shí)候,我們就需要警覺(jué)起來(lái),因?yàn)楹苡锌赡苷麄€(gè)系統(tǒng)出現(xiàn)了內(nèi)存泄露。那么剩下的問(wèn)題就比較簡(jiǎn)單了,如何動(dòng)態(tài)的獲知哪里有內(nèi)存泄露呢?

對(duì) Go 的程序來(lái)說(shuō),我們可以使用語(yǔ)言?xún)?nèi)置的 pprof 工具非常方便的對(duì)內(nèi)存進(jìn)行 profile,我們只需要在程序里面 import _ "net/http/pprof",這樣啟動(dòng)的 HTTP server 服務(wù)器就能夠被直接 profile 了。

但對(duì) Rust,情況就沒(méi)那么簡(jiǎn)單了。因?yàn)檎Z(yǔ)言并沒(méi)有內(nèi)置這個(gè)功能,所以我們得想其他辦法來(lái)解決。Rust 默認(rèn)使用的是 jemalloc 這個(gè)內(nèi)存分配器,jemalloc 提供了非常方便的 profile 功能。所以我們自然將目光放在了如何用 jemalloc 來(lái) profile memory 以及如何與 Rust 整合上面了。

要打開(kāi) jemalloc 的 profile 功能,在編譯的時(shí)候我們需要顯示的帶上 --enable-prof 選項(xiàng),通常在 Linux 下面我們會(huì)安裝 libunwind 庫(kù),這樣 prof 默認(rèn)就會(huì)使用 libunwind 了。另外,為了不跟系統(tǒng)的 malloc 這些函數(shù)有命名沖突,這里顯示的給 jemalloc 加上了前綴,使用 --with-jemalloc-prefix="je_",這樣我們外面就會(huì)使用 je_malloc 這種的函數(shù)名字了。

我們用官網(wǎng)非常簡(jiǎn)單的例子來(lái)說(shuō)明內(nèi)存泄露問(wèn)題,如下:

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

上面的函數(shù)有一個(gè)典型的內(nèi)存泄漏,我們調(diào)用 1000 次:

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

剩下的就是如何來(lái)定位內(nèi)存問(wèn)題了。

Mem Statistics

首先我們來(lái)看看 jemalloc 自己提供的統(tǒng)計(jì)信息,我們可以直接使用 je_malloc_stats_print(NULL, NULL, NULL) 來(lái)將 memory 的統(tǒng)計(jì)輸出到 stderr 上面,但這個(gè)函數(shù)輸出的東西比較多,并不利于實(shí)時(shí)的查看。多數(shù)時(shí)候,我們都是使用 je_mallctl 函數(shù),得到一些關(guān)鍵的統(tǒng)計(jì)數(shù)據(jù),然后發(fā)送給 Prometheus 來(lái)展示,這樣我們就能夠在 Prometheus 里面觀察到整個(gè) jemalloc 內(nèi)存變化的曲線,如果持續(xù)上升,就需要報(bào)警了。

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);

上面我們?cè)诿看?do_something 后面得到 allocated,active 以及 mapped 這些指標(biāo),然后輸出:

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 來(lái)讓統(tǒng)計(jì)的 cache 更新。

Leak Check

通過(guò)統(tǒng)計(jì),我們能看到整個(gè)內(nèi)存的變化曲線,但到底哪里有內(nèi)存問(wèn)題呢?我們可以在程序結(jié)束的時(shí)候顯示的輸出內(nèi)存泄露。仍然使用上面的程序,我們使用 JE_MALLOC_CONF="prof_leak:true,lg_prof_sample:0,prof_final:true" ./leak 來(lái)執(zhí)行,當(dāng)程序退出之后,會(huì)生成一個(gè) prof heap 的文件,我們用 jeprof 工具就可以知道內(nèi)存泄露了。

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

使用上面的方式,我們只能在程序結(jié)束的時(shí)候輸出內(nèi)存泄露,實(shí)際并不適用于長(zhǎng)時(shí)間運(yùn)行的程序,幸運(yùn)的時(shí)候,我們可以通過(guò) jemalloc 的一些參數(shù)以及 mallctl 函數(shù)來(lái)顯示的對(duì)內(nèi)存進(jìn)行 profile。在運(yùn)行程序之前,我們需要設(shè)置 export JE_MALLOC_CONF="prof:true,prof_prefix:jeprof.out",它用來(lái)告訴 jemalloc 顯示的打開(kāi) prof,同時(shí)自動(dòng)的生成 profile 文件名。

在代碼里面,我們可以使用 mallctl("prof.dump", NULL, NULL, NULL, 0); 來(lái)對(duì)當(dāng)前執(zhí)行的程序生成一個(gè) mem dump,然后過(guò)一段時(shí)間之后,用相同的方法再次生成一個(gè),在用 jeprof 工具對(duì)比兩次的 dump,就大概能知道是否有內(nèi)存問(wèn)題了。

具體到上面的例子,我們?cè)诔绦虻拈_(kāi)始和結(jié)束都使用 mallctl dump 一次 memory,然后對(duì)兩次生成的 profile 文件進(jìn)行對(duì)比:

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

上面說(shuō)完了在 C 里面使用 jemalloc 來(lái)看內(nèi)存問(wèn)題,那么對(duì)于 Rust 語(yǔ)言來(lái)說(shuō),我們?cè)趺刺幚砟兀縍ust 默認(rèn)使用的就是 jemalloc,但發(fā)布的版本里面 jemalloc 并沒(méi)有帶上 profile 的功能,所以需要重新編譯 Rust,對(duì)于我們來(lái)說(shuō),因?yàn)橐獙?shí)時(shí)的跟進(jìn) Rust 的版本,這并不是一個(gè)好辦法。

幸運(yùn)的是,Rust 提供了 custom allocator 的功能,也就是能使用自定義的 allocator,這樣對(duì)我們來(lái)說(shuō)就簡(jiǎn)單很多了,使用一個(gè)打開(kāi)了 profile 功能的 jemalloc 用作自定義的 allocator,這樣就能通過(guò) mallctl 來(lái) profile memory 了。更幸運(yùn)的是,Rust 的一個(gè)開(kāi)發(fā)者已經(jīng)提供了相關(guān)的 allocator,我們可以直接使用。

我們可以構(gòu)造一個(gè)非常簡(jiǎn)單的 case,使用 mem::forget

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

在這個(gè)函數(shù)前后,我們都使用 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);

執(zhí)行之后,就會(huì)生成兩個(gè) 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

小結(jié)

內(nèi)存問(wèn)題一直是長(zhǎng)時(shí)間運(yùn)行程序需要處理的一個(gè)棘手問(wèn)題,雖然 Rust 相比 C 以及 CPP,在內(nèi)存處理上面有了很大的改善,但我們?nèi)匀豢赡軙?huì)有引用泄露等問(wèn)題出現(xiàn),這些問(wèn)題很難通過(guò)直接瀏覽代碼,看 log 和 metrics 來(lái)看出來(lái)的,而 profile 恰恰能很好的解決,所以這也是我們一直想在 TiKV 上面加入 profile memory 的原因。

需要注意,加入 profile 之后,會(huì)影響系統(tǒng)的性能,所以通常,我們都會(huì)采用 sample 的方式或者動(dòng)態(tài)的打開(kāi)或者關(guān)閉 profile 功能。譬如,假設(shè)我們要 profile memory,就使用 mallctlprof.active 打開(kāi),一段時(shí)間,在使用 mallctl dump 出 memory,然后在關(guān)閉 profile。

另外,除了 jemalloc,其實(shí) tcmalloc 也照樣能支持 profile memory,只是因?yàn)?Rust 默認(rèn)使用的是 jemalloc,我們最終我們還是決定基于 jemalloc 來(lái)使用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容