Rust 里用 jemalloc 后 RSS 涨了 40% 的一次定位

场景是一个长跑的数据摄入服务,Rust 1.76,x86_64 Linux 5.15 容器,镜像基于 debian-slim。进程常驻,每批处理约 200 万条记录,单批峰值堆上大概 3~4 GB,跑几天后 RSS 稳定在 7 GB 左右。我为了压住内存碎片,把分配器从系统 malloc 换成了 tikv-jemallocator,结果 RSS 反而涨到接近 10 GB,容器 OOM 直接打回原形。

换的方式很直接,Cargo.toml 加依赖,main.rs 顶部声明全局分配器:

// Cargo.toml
// [dependencies]
// tikv-jemallocator = "0.5"

#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;

然后我跑了两个对照:一是不带任何环境变量;二是设了 MALLOC_CONF="dirty_decay_ms:1000,muzzy_decay_ms:0" 再启动。同时用 jemalloc 自带的统计接口,在每批结束时把 allocated、active、resident 打出来,命令行加 --features stats 之类不一定需要,实际用的是 tikv-jemalloc-ctl:

use tikv_jemalloc_ctl::{epoch, stats};

fn log_mem() {
    epoch::advance().unwrap();
    let allocated = stats::allocated::read().unwrap();
    let resident = stats::resident::read().unwrap();
    println!("allocated={} resident={}", allocated, resident);
}

对照结果很清楚:allocated 每批结束后基本回落到 300 MB 以内,resident 却一直挂在高位不降,说明不是业务逻辑持有内存,是分配器把页留在自己手里了。jemalloc 默认的脏页衰减窗口比我预期长,容器里又看不到物理内存压力,它就懒得还。把 dirty_decay_ms 调到 1000 后,RSS 回落到 6.5 GB 左右,和系统 malloc 接近,碎片确实少了一点。

代价要讲清楚:缩短 decay 窗口等于让分配器更频繁地 madvise 还页,批处理之间的 CPU 有可观测上升,我这边大概多了 2%~3%;另外 muzzy_decay_ms:0 对某些工作负载会导致反复重新映射,不一定划算。如果你们的服务对延迟敏感,这个参数不该照抄。

失效的地方也踩过:一是这套统计在 musl 静态链接时用不了,tikv-jemalloc-ctl 依赖的符号找不到,我最后是靠 /proc/<pid>/status 的 VmRSS 粗看;二是容器里 cgroup 限额低于宿主物理内存时,jemalloc 感知不到真实的“内存紧张”,衰减策略不会自动变积极,必须手动配;三是线程数暴涨时,每个 arena 都会留缓存,光调 decay 参数解决不了,得看 narenas。

现在我想确认一个边界:如果进程里有明显的大小对象混合分配(大量 64B 级小对象加少量 MB 级缓冲),你们是调 decay,还是干脆按线程绑 arena、给大对象走单独路径?想听听实际线上怎么选。

0
赞同度 100% · 0 条评论 · 8 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

还没有评论,写下第一条。