在某次持續(xù)壓測過程中,我們發(fā)現(xiàn) GreptimeDB 的 Frontend 節(jié)點內(nèi)存即使在請求量平穩(wěn)的階段也在持續(xù)上漲,直至被 OOM kill。我們判斷 Frontend 應該是有內(nèi)存泄漏了,于是開啟了排查內(nèi)存泄漏之旅。
Heap Profiling
大型項目幾乎不可能只通過看代碼就能找到內(nèi)存泄漏的地方。所以我們首先要對程序的內(nèi)存用量做統(tǒng)計分析。幸運的是,GreptimeDB 使用的 jemalloc 自帶 heap profiling[1],我們也支持了導出 jemalloc 的 profile dump 文件[2]。于是我們在 GreptimeDB 的 Frontend 節(jié)點內(nèi)存達到 300MB 和 800MB 時,分別 dump 出了其內(nèi)存 profile 文件,再用 jemalloc 自帶的jeprof分析兩者內(nèi)存差異(--base參數(shù)),最后用火焰圖顯示出來:

顯然圖片中間那一大長塊就是不斷增長的 500MB 內(nèi)存占用了。仔細觀察,居然有 thread 相關(guān)的 stack trace。難道是創(chuàng)建了太多線程?簡單用ps -T -p命令看了幾次 Frontend 節(jié)點的進程,線程數(shù)穩(wěn)定在 84 個,而且都是預知的會創(chuàng)建的線程。所以“線程太多”這個原因可以排除。
再繼續(xù)往下看,我們發(fā)現(xiàn)了很多 Tokio runtime 相關(guān)的 stack trace,而 Tokio 的 task 泄漏也是常見的一種內(nèi)存泄漏。這個時候我們就要祭出另一個神器:Tokio-console[3]。
Tokio Console
Tokio Console 是 Tokio 官方的診斷工具,輸出結(jié)果如下:

我們看到居然有 5559 個正在運行的 task,且絕大多數(shù)都是 Idle 狀態(tài)!于是我們可以確定,內(nèi)存泄漏發(fā)生在 Tokio 的 task 上。現(xiàn)在問題就變成了:GreptimeDB 的代碼里,哪里 spawn 了那么多的無法結(jié)束的 Tokio task?
從上圖的 "Location" 列我們可以看到 task 被 spawn 的地方[4]:
implRuntime{
///Spawn a future and execute it in this thread pool
///
///Similar to Tokio::spawn()
pubfnspawn(&self,future:F)->JoinHandle
where
F:Future+Send+'static,
F:Send+'static,
{
self.handle.spawn(future)
}
}
接下來的任務是找到 GreptimeDB 里所有調(diào)用這個方法的代碼。
..Default::default()
經(jīng)過一番看代碼的仔細排查,我們終于定位到了 Tokio task 泄漏的地方,并在 PR #1512[5]中修復了這個泄漏。簡單地說,就是我們在某個會被經(jīng)常創(chuàng)建的 struct 的構(gòu)造方法中,spawn 了一個可以在后臺持續(xù)運行的 Tokio task,卻未能及時回收它。對于資源管理來說,在構(gòu)造方法中創(chuàng)建 task 本身并不是問題,只要在Drop中能夠順利終止這個 task 即可。而我們的內(nèi)存泄漏就壞在忽視了這個約定。
這個構(gòu)造方法同時在該 struct 的Default::default()方法當中被調(diào)用了,更增加了我們找到根因的難度。
Rust 有一個很方便的,可以用另一個 struct 來構(gòu)造自己 struct 的方法,即 "Struct Update Syntax"[6]。如果 struct 實現(xiàn)了Default,我們可以簡單地在 struct 的 field 構(gòu)造中使用..Default::default()。
如果Default::default()內(nèi)部有 “side effect”(比如我們本次內(nèi)存泄漏的原因——創(chuàng)建了一個后臺運行的 Tokio task),一定要特別注意:struct 構(gòu)造完成后,Default創(chuàng)建出來的臨時 struct 就被丟棄了,一定要做好資源回收。
例如下面這個小例子:Rust Playground[7]
structA{
i:i32,
}
implDefaultforA{
fndefault()->Self{
println!("called A::default()");
A{i:42}
}
}
#[derive(Default)]
structB{
a:A,
i:i32,
}
implB{
fnnew(a:A)->Self{
B{
a,
//A::default()is called in B::default(),even though"a"is provided here.
..Default::default()
}
}
}
fnmain(){
leta=A{i:1};
letb=B::new(a);
println!("{}",b.a.i);
}
struct A 的default方法是會被調(diào)用的,打印出called A::default()。
總結(jié)
?排查 Rust 程序的內(nèi)存泄漏,我們可以用 jemalloc 的 heap profiling 導出 dump 文件;再生成火焰圖可直觀展現(xiàn)內(nèi)存使用情況。
? Tokio-console 可以方便地顯示出 Tokio runtime 的 task 運行情況;要特別注意不斷增長的 idle tasks。
?盡量不要在常用 struct 的構(gòu)造方法中留下有副作用的代碼。
?Default只應該用于值類型 struct。
審核編輯:湯梓紅
-
內(nèi)存
+關(guān)注
關(guān)注
9文章
3214瀏覽量
76402 -
文件
+關(guān)注
關(guān)注
1文章
594瀏覽量
26073 -
線程
+關(guān)注
關(guān)注
0文章
510瀏覽量
20833 -
Rust
+關(guān)注
關(guān)注
1文章
240瀏覽量
7609
原文標題:記一次 Rust 內(nèi)存泄漏排查之旅 | 經(jīng)驗總結(jié)篇
文章出處:【微信號:Rust語言中文社區(qū),微信公眾號:Rust語言中文社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
寫了一個內(nèi)存泄漏檢查工具
分享一種內(nèi)存泄漏定位排查技巧
如何處理服務存在內(nèi)存泄漏問題?
一次性輸液器泄漏正負壓檢測儀
一次性輸液器泄漏正負壓檢測儀
什么是內(nèi)存泄漏?如何避免JavaScript內(nèi)存泄漏
記一次Rust內(nèi)存泄漏排查之旅
評論