Rust Web 项目内存持续上升的真相:不是泄漏,是碎片!

485 阅读4分钟

本文首发在公众号 猩猩程序员 欢迎关注

最近,有开发者在使用 Axum 框架构建 Web 服务时遇到这样一个问题:服务运行一段时间后,内存使用不断上升,最后 OOM 被系统杀死,重启后依旧如此,形成了循环。

起初怀疑是代码中出现了内存泄漏,经过多次排查和 profiling 分析,未发现任何明显问题。使用的都是 Rust 安全代码,没有 Rc 循环,也未使用 mem::forget() 等容易导致泄漏的操作。

进一步调查后发现,问题可能并非出在业务逻辑,而是在于:默认的内存分配器可能不适合这个场景


Rust 的默认分配器适合哪些程序?

Rust 默认使用的是“系统自带”的内存分配器,比如在 Linux 上就是 glibc 的 malloc。这种分配器具有以下特点:

  • 体积小、依赖少
  • 适用于生命周期短、结构简单的程序
  • 不适合长时间运行、频繁分配释放、多线程的场景

而现代 Web 服务通常具备以下特征:

  • 使用 Tokio 实现异步 + 多线程
  • 长时间运行,7x24 不间断
  • 每次请求涉及大量堆内存分配与释放
  • 运行在容器或 Kubernetes 中,有内存上限

此时继续使用系统分配器,容易导致性能下降或内存异常增长。


是内存泄漏吗?可能是“碎片”

这类问题很可能不是程序真的“泄漏”内存,而是出现了内存碎片(fragmentation) 。表现是内存没有被释放,但实际上程序仍然能访问,只是系统无法有效重用它。

比如:

  • 多次分配释放不同大小的内存块
  • 导致大块空闲内存被分散,无法再次分配
  • 分配器无法整理这些碎片,久而久之内存持续上升

这就像抽屉里塞满了形状各异的物品,虽然有空隙,但再也塞不下新东西。


换个分配器能解决问题吗?

许多开发者建议,在这种场景下换用专为高性能设计的内存分配器,比如 mimallocjemalloc,能带来明显改善。

实际案例显示:

  • 一台拥有 192GB RAM 的服务器在使用默认分配器时依然 OOM
  • 切换到 mimalloc 后,内存使用下降至 18GB
  • 在某些应用中,mimalloc 不仅降低内存占用,还能 成倍提升性能

如何切换分配器?一行代码搞定

以 mimalloc 为例:

# Cargo.toml
[dependencies]
mimalloc = "0.1"

# main.rs
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;

jemalloc 则类似:

# Cargo.toml
jemallocator = "0.3"

# main.rs
#[global_allocator]
static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;

切换后,整个程序所有的堆内存分配操作都会使用新的分配器。


mimalloc 和 jemalloc 选哪个?

分配器特点
mimalloc微软出品,轻量级,低碎片率,适合高并发服务
jemalloc老牌稳定,性能好,最坏情况也能控制内存增长,体积稍大

如果没有特别要求,建议优先选择 mimalloc,更容易上手,表现稳定。


一些建议

除了更换分配器,还可以考虑以下方式优化内存使用:

  • 启用 leak sanitizer 检查真正的内存泄漏

    RUSTFLAGS="-Z sanitizer=leak -Zexport-executable-symbols" cargo test --target x86_64-unknown-linux-gnu
    
  • 使用 对象池(object pool)重用内存,避免重复分配大对象,例如复用 Vec<u8>

  • 如果仍使用系统分配器,可尝试 malloc_trim(0) 主动释放空闲内存(仅适用于 glibc)

  • 避免 tracing-subscriber 等日志库中缓存无限增长


粽结一下吧

在如下场景中,默认分配器可能会成为瓶颈:

  • Tokio 驱动的多线程 Web 服务
  • 长时间运行的高并发程序
  • 内存分配/释放频繁,负载不稳定
  • 运行在限制内存的容器中(如 Kubernetes)

此时,切换到 mimalloc 或 jemalloc 是一个值得尝试的优化手段。Rust 本身提供了极高的控制力,一行代码就可以改变分配行为,让程序更快、更稳、内存更可控。

不要等到 OOM 才开始排查,“选对分配器”从一开始就能省去许多麻烦。