Rust 内存 profling 之旅
首先如果你使用的 macos,那么要做的第一件事情是找个 linux 的机器,要么服务器,要么搞个 docker 用。不要用 macos 进行下面的操作,因为会有各种各样难以 debug 的问题。
1. 下面先说下代码部分的一些改动。首先我们需要确保 Rust 程序设置一些 Debug 模式,可以让我们追踪到内存里面的实际情况,这里 Cargo.toml 需要一些修改,增加一些我们需要的内存 profiling 的工具,例如 jemallocator ctl sys 之类的。
[profile.release]
debug = "full"
split-debuginfo = "off"
strip = "none"
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemallocator = { version = "0.6", optional = true }
tikv-jemalloc-ctl = { version = "0.6", optional = true }
tikv-jemalloc-sys = { version = "0.6", optional = true }
[features]
default = ["process", "jemalloc-stats", "jemalloc-prof"]
# Only change global allocator to jemalloc, with no extra features.
jemalloc = ["dep:tikv-jemallocator"]
# enable jemalloc with profiling enabled. This is disabled by default.
# Will also enable "jemalloc"
jemalloc-prof = [
"jemalloc",
"dep:tikv-jemalloc-ctl",
"tikv-jemallocator/profiling",
"tikv-jemalloc-sys/profiling",
]
# enable jemalloc with stats enabled. This is enalbed by default.
# Will also enable "jemalloc"
jemalloc-stats = [
"jemalloc",
"dep:tikv-jemalloc-ctl",
"dep:tikv-jemalloc-sys",
"tikv-jemalloc-sys/stats",
"tikv-jemallocator/stats",
]
2. 然后我们在自己程序的 main 函数里面申明要使用 jemalloc 并且添加一些参数配置。
/// jemalloc will lookup for global (char* _rjem_malloc_conf) for config at start up. Some config
/// cannot be changed during runtime needs to be set here, or via envrion [`MALLOC_CONF`]
///
/// Default configs:
/// - `prof:true`: enable prof when `jemalloc-prof` is configured, but not active it yet
/// - `lg_prof_sample:16`: sample when every 2^16 bytes is allocated
/// - `prof_accum:false`: generate an incremental profiling result
#[cfg(feature = "jemalloc-prof")]
#[export_name = "_rjem_malloc_conf"]
static JEMALLOC_CONF: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:16,prof_accum:true\0";
#[cfg(feature = "jemalloc")]
#[global_allocator]
static JEMALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
3. 由于我们有很多参数的申明我们无法确定是否生效。我们可以尝试在 .cargo 下面添加一个 config.toml 配置
[env] JEMALLOC_SYS_WITH_MALLOC_CONF="confirm_conf:true"
这样当我们程序重新 build 之后我们可以看到一些这样的提示。
<jemalloc>: malloc_conf #1 (string specified via --with-malloc-conf): "confirm_conf:true" <jemalloc>: -- Set conf value: confirm_conf:true <jemalloc>: malloc_conf #2 (string pointed to by the global variable malloc_conf): "prof:true,prof_active:true,lg_prof_sample:16,prof_accum:true" <jemalloc>: -- Set conf value: prof:true <jemalloc>: -- Set conf value: prof_active:true <jemalloc>: -- Set conf value: lg_prof_sample:16 <jemalloc>: -- Set conf value: prof_accum:true <jemalloc>: malloc_conf #3 ("name" of the file referenced by the symbolic link named /etc/malloc.conf): "" <jemalloc>: malloc_conf #4 (value of the environment variable MALLOC_CONF): "" <jemalloc>: malloc_conf #5 (string pointed to by the global variable malloc_conf_2_conf_harder): ""
这可以帮助我们识别我们在 main 函数里面添加的 jemalloc config 是否正确的应用上了。
4. 因为我本身是测试 actix-web 性能,自带 web 框架,所以我 new 了一个 handler 来放一下面代码,并通过调用 xxxx/mem 来开启性能 profile 收集并得到对应 heap 文件
.route("/mem", to(memory_heap))
use std::os::raw::{c_char, c_int};
use std::ptr;
use std::thread::sleep;
use std::time::Duration;
use actix_web::http::header::ContentType;
use actix_web::{web, HttpResponse, Responder};
use log::error;
use tikv_jemalloc_ctl::raw;
use tikv_jemalloc_sys::mallctl;
pub async fn memory_heap(path: web::Path<String>) -> impl Responder {
match path.as_str() {
"dump" => {
let thread = std::thread::Builder::new().name("spawn_dump".to_owned());
thread
.spawn(|| loop {
let r = dump();
println!("---------------dump ret: {} -------------------", r);
sleep(Duration::from_secs(60));
})
.unwrap();
HttpResponse::Ok()
.content_type(ContentType::plaintext())
.body("memory monitor start ok 👋!")
}
"stop" => {
stop_prof();
HttpResponse::Ok().body("Stop dump files")
}
"start" => {
start_prof();
HttpResponse::Ok().body("Start dump files")
}
_ => HttpResponse::BadRequest().body("bad request"),
}
}
fn dump() -> c_int {
unsafe {
mallctl(
"prof.dump\0".as_bytes() as *const _ as *const c_char,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
0,
)
}
}
fn stop_prof() {
match unsafe { raw::write(b"prof.active\0", false) } {
Ok(_) => println!("Profiler stopped successfully"),
Err(e) => error!("unable to stop jemalloc profiler: {}", e),
}
}
fn start_prof() {
match unsafe { raw::write(b"prof.active\0", true) } {
Ok(_) => println!("Profiler started successfully"),
Err(e) => error!("unable to start jemalloc profiler: {}", e),
}
}
这里除了 dump 函数外,我还支持了一个 start 和 end prof 的操作。这里的 raw::write(b"prof.active\0", false) 可以像一个 flag 一样控制 jemalloc 是否还继续采样。采样和 dump 数据文件是两件事情,如果没有开启采样,即使我们 dump 数据文件也是没有内容的。
另外简单介绍一下这一部分是在做什么,我们要获得内存使用情况首先我们就得获得一段时间内内存使用的情况。这一部分代码的 dump() 部分,是在实现文档中所说的这一部分
5. 一切准备就绪让我们回到我们的 linux 机器
我们可以选择在 linux 的 pod 里进行操作首先我们需要安装几个包。这些包有的是对最后关系图的支持,有的是对 dump 相关的支持。
apt update
apt install google-perftools -y
apt install binutils -y
apt install graphviz -y
apt install ghostscript -y
6. 然后我们将最新的 jeprof perl 弄下来用 https://github.com/jemalloc/jemalloc/blob/2a693b83d2d1631b6a856d178125e1c47c12add9/bin/jeprof.in#L43
这个是最新版 jeprof 工具,之前我尝试直接使用 apt update 直接下载这个工具,但是我发现似乎不能总是下载到最新的,所以我们可以直接把这个下下来用,直接保存为原始格式给个 777 权限,让其可运行就行。
7. 这个时候在机器上启动 dump 。
8. 你会得到很多 heap 文件现在,创建一个文件夹将他们都挪进去方便操作。
mkdir jeprof
mv jeprof* jeprof
这里的 jeproff 其实是我刚才下载的 jeprof 工具,因为不重名多加了一个 f
9. 使用 ./jeprof 的工具生成关系图导成 pdf
--nodecount=n Show at most so many nodes. [default=80] --nodefraction=f Hide nodes below f*total. [default=.005] --edgefraction=f Hide edges below f*total. [default=.001] --maxdegree=n Max incoming/outgoing edges per node. [default=8] ./jeproff -pdf --nodecount 800 --nodefraction .0002 --edgefraction .0001 --maxdegree 80 ../gateway jeprof.* > out.pdf
这里详细解释一下这部分,这一部分控制了生成报告的粒度。当我们尝试生成报告的时候,报告的左上角会有类似描述
Dropped nodes with ≤ 1.9 abs(MB) 这个代表 jeprof 会过滤掉内存非配较小的节点,任何占用内存小于等于 0.6 MB 的节点都会被忽略。
这个受参数 —nodefraction 影响 9749 * 0.0002 = 1.9MB。
同理 —edgefraction 用于控制 Dropped edges 参数。—edgefraction 影响 9749 * 0.0001 = 1.0MB。
如果这两个值使用默认值就会有点过大,这样你的内存分配关系图或者你的火焰图就会缺少细节。你只能看到最大的影响是哪一部分,对于 pdf 生成就是你完全看不到一些细节分配,不知道发生了什么。
当我们需要足够的细节,我们需要将这两个参数调得足够小。实践中建议多试试,调几个值看看。
10. 生成 pdf 格式结果文件,我们最后将其下载到本地查看
kubectl cp k8snamespace/pod_name:remote_path/out.pdf ~local_path/out.pdf
6. 获得性能分析的图
看到这个分析图真是亲切。
没想到的是 golang pprof 能在 macos 上轻松获得的东西,rust 搞起来这么费劲。 linux 生态下的东西,尽量还是在 linux 下用工具搞吧。
Reference:
https://blog.csdn.net/weixin_37902491/article/details/128103738 如何使用jeprof分析rust的内存
https://tidb.net/blog/f1875f94#%E4%BB%80%E4%B9%88%E6%98%AF%20Heap%20Profiling 内存泄漏的定位与排查:Heap Profiling 原理解析
https://github.com/jemalloc/jemalloc/wiki/Use-Case%3A-Heap-Profiling Use Case: Heap Profiling
https://github.com/tikv/jemallocator/issues/33 Use jeprof to see only the address, not the function name