對于長時間運行的服務(wù)端程序,內(nèi)存的使用一直是一個非常重要的監(jiān)控指標(biāo),當(dāng)內(nèi)存的使用量一直在上升的時候词裤,我們就需要警覺起來,因為很有可能整個系統(tǒng)出現(xiàn)了內(nèi)存泄露鳖宾。那么剩下的問題就比較簡單了吼砂,如何動態(tài)的獲知哪里有內(nèi)存泄露呢?
對 Go 的程序來說鼎文,我們可以使用語言內(nèi)置的 pprof 工具非常方便的對內(nèi)存進(jìn)行 profile渔肩,我們只需要在程序里面 import _ "net/http/pprof"
,這樣啟動的 HTTP server 服務(wù)器就能夠被直接 profile 了拇惋。
但對 Rust周偎,情況就沒那么簡單了抹剩。因為語言并沒有內(nèi)置這個功能,所以我們得想其他辦法來解決蓉坎。Rust 默認(rèn)使用的是 jemalloc 這個內(nèi)存分配器澳眷,jemalloc 提供了非常方便的 profile 功能。所以我們自然將目光放在了如何用 jemalloc 來 profile memory 以及如何與 Rust 整合上面了蛉艾。
要打開 jemalloc 的 profile 功能钳踊,在編譯的時候我們需要顯示的帶上 --enable-prof
選項,通常在 Linux 下面我們會安裝 libunwind
庫勿侯,這樣 prof 默認(rèn)就會使用 libunwind
了拓瞪。另外,為了不跟系統(tǒng)的 malloc 這些函數(shù)有命名沖突助琐,這里顯示的給 jemalloc 加上了前綴祭埂,使用 --with-jemalloc-prefix="je_"
,這樣我們外面就會使用 je_malloc
這種的函數(shù)名字了弓柱。
我們用官網(wǎng)非常簡單的例子來說明內(nèi)存泄露問題沟堡,如下:
void do_something(size_t i)
{
// Leak some memory.
je_malloc(i * 100);
}
上面的函數(shù)有一個典型的內(nèi)存泄漏,我們調(diào)用 1000 次:
for (i = 0; i < 1000; i++) {
do_something(i);
}
剩下的就是如何來定位內(nèi)存問題了矢空。
Mem Statistics
首先我們來看看 jemalloc 自己提供的統(tǒng)計信息航罗,我們可以直接使用 je_malloc_stats_print(NULL, NULL, NULL)
來將 memory 的統(tǒng)計輸出到 stderr 上面,但這個函數(shù)輸出的東西比較多屁药,并不利于實時的查看粥血。多數(shù)時候,我們都是使用 je_mallctl
函數(shù)酿箭,得到一些關(guān)鍵的統(tǒng)計數(shù)據(jù)复亏,然后發(fā)送給 Prometheus 來展示,這樣我們就能夠在 Prometheus 里面觀察到整個 jemalloc 內(nèi)存變化的曲線缭嫡,如果持續(xù)上升缔御,就需要報警了。
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 這些指標(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 來讓統(tǒng)計的 cache 更新评架。
Leak Check
通過統(tǒng)計眷茁,我們能看到整個內(nèi)存的變化曲線,但到底哪里有內(nèi)存問題呢纵诞?我們可以在程序結(jié)束的時候顯示的輸出內(nèi)存泄露上祈。仍然使用上面的程序,我們使用 JE_MALLOC_CONF="prof_leak:true,lg_prof_sample:0,prof_final:true" ./leak
來執(zhí)行,當(dāng)程序退出之后登刺,會生成一個 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é)束的時候輸出內(nèi)存泄露节仿,實際并不適用于長時間運行的程序晤锥,幸運的時候掉蔬,我們可以通過 jemalloc 的一些參數(shù)以及 mallctl
函數(shù)來顯示的對內(nèi)存進(jìn)行 profile。在運行程序之前矾瘾,我們需要設(shè)置 export JE_MALLOC_CONF="prof:true,prof_prefix:jeprof.out"
女轿,它用來告訴 jemalloc 顯示的打開 prof,同時自動的生成 profile 文件名壕翩。
在代碼里面蛉迹,我們可以使用 mallctl("prof.dump", NULL, NULL, NULL, 0);
來對當(dāng)前執(zhí)行的程序生成一個 mem dump,然后過一段時間之后放妈,用相同的方法再次生成一個北救,在用 jeprof 工具對比兩次的 dump,就大概能知道是否有內(nèi)存問題了芜抒。
具體到上面的例子珍策,我們在程序的開始和結(jié)束都使用 mallctl
dump 一次 memory,然后對兩次生成的 profile 文件進(jìn)行對比:
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 來看內(nèi)存問題宅倒,那么對于 Rust 語言來說攘宙,我們怎么處理呢?Rust 默認(rèn)使用的就是 jemalloc拐迁,但發(fā)布的版本里面 jemalloc 并沒有帶上 profile 的功能蹭劈,所以需要重新編譯 Rust,對于我們來說线召,因為要實時的跟進(jìn) Rust 的版本铺韧,這并不是一個好辦法。
幸運的是缓淹,Rust 提供了 custom allocator 的功能哈打,也就是能使用自定義的 allocator,這樣對我們來說就簡單很多了割卖,使用一個打開了 profile 功能的 jemalloc 用作自定義的 allocator前酿,這樣就能通過 mallctl
來 profile memory 了。更幸運的是鹏溯,Rust 的一個開發(fā)者已經(jīng)提供了相關(guān)的 allocator罢维,我們可以直接使用。
我們可以構(gòu)造一個非常簡單的 case,使用 mem::forget
:
fn do_something()
{
let mut bad_vec = Vec::new();
for _ in 0..1024 {
bad_vec.push('0');
}
mem::forget(bad_vec);
}
在這個函數(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í)行之后,就會生成兩個 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)存問題一直是長時間運行程序需要處理的一個棘手問題,雖然 Rust 相比 C 以及 CPP瑰艘,在內(nèi)存處理上面有了很大的改善是鬼,但我們?nèi)匀豢赡軙幸眯孤兜葐栴}出現(xiàn),這些問題很難通過直接瀏覽代碼紫新,看 log 和 metrics 來看出來的均蜜,而 profile 恰恰能很好的解決,所以這也是我們一直想在 TiKV 上面加入 profile memory 的原因芒率。
需要注意囤耳,加入 profile 之后,會影響系統(tǒng)的性能偶芍,所以通常充择,我們都會采用 sample 的方式或者動態(tài)的打開或者關(guān)閉 profile 功能。譬如匪蟀,假設(shè)我們要 profile memory椎麦,就使用 mallctl
將 prof.active
打開,一段時間萄窜,在使用 mallctl
dump 出 memory铃剔,然后在關(guān)閉 profile。
另外查刻,除了 jemalloc键兜,其實 tcmalloc 也照樣能支持 profile memory,只是因為 Rust 默認(rèn)使用的是 jemalloc穗泵,我們最終我們還是決定基于 jemalloc 來使用普气。