乐趣区

关于后端:记一次-Rust-内存泄漏排查之旅-经验总结篇

在某次继续压测过程中,咱们发现 GreptimeDB 的 Frontend 节点内存即便在申请量安稳的阶段也在继续上涨,直至被 OOM kill。咱们判断 Frontend 应该是有内存透露了,于是开启了排查内存透露之旅。

Heap Profiling

大型项目简直不可能只通过看代码就能找到内存透露的中央。所以咱们首先要对程序的内存用量做统计分析。侥幸的是,GreptimeDB 应用的 jemalloc 自带 heap profiling,咱们也反对了导出 jemalloc 的 profile dump 文件。于是咱们在 GreptimeDB 的 Frontend 节点内存达到 300MB 和 800MB 时,别离 dump 出了其内存 profile 文件,再用 jemalloc 自带的 jeprof 剖析两者内存差别(--base 参数),最初用火焰图显示进去:

显然图片两头那一大长块就是一直增长的 500MB 内存占用了。仔细观察,竟然有 thread 相干的 stack trace。难道是创立了太多线程?简略用 ps -T -p 命令看了几次 Frontend 节点的过程,线程数稳固在 84 个,而且都是预知的会创立的线程。所以“线程太多”这个起因能够排除。

再持续往下看,咱们发现了很多 Tokio runtime 相干的 stack trace,而 Tokio 的 task 透露也是常见的一种内存透露。这个时候咱们就要祭出另一个神器:Tokio-console。

Tokio Console

Tokio Console 是 Tokio 官网的诊断工具,输入后果如下:

咱们看到竟然有 5559 个正在运行的 task,且绝大多数都是 Idle 状态!于是咱们能够确定,内存透露产生在 Tokio 的 task 上。
当初问题就变成了:GreptimeDB 的代码里,哪里 spawn 了那么多的无奈完结的 Tokio task?

从上图的 “Location” 列咱们能够看到 task 被 spawn 的中央:

impl Runtime {
    /// Spawn a future and execute it in this thread pool
    ///
    /// Similar to Tokio::runtime::Runtime::spawn()
    pub fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
    where
        F: Future + Send + 'static,
        F::Output: Send + 'static,
    {self.handle.spawn(future)
    }
}

接下来的工作是找到 GreptimeDB 里所有调用这个办法的代码。

..Default::default()

通过一番看代码的认真排查,咱们终于定位到了 Tokio task 透露的中央,并在 PR #1512 中修复了这个透露。简略地说,就是咱们在某个会被常常创立的 struct 的构造方法中,spawn 了一个能够在后盾继续运行的 Tokio task,却未能及时回收它。对于资源管理来说,在构造方法中创立 task 自身并不是问题,只有在 Drop 中可能顺利终止这个 task 即可。而咱们的内存透露就坏在漠视了这个约定。

这个构造方法同时在该 struct 的 Default::default() 办法当中被调用了,更减少了咱们找到根因的难度。

Rust 有一个很不便的,能够用另一个 struct 来结构本人 struct 的办法,即 “Struct Update Syntax”。如果 struct 实现了 Default,咱们能够简略的在 struct 的 field 结构中应用 ..Default::default()。如果 Default::default() 外部有“side effect”(比方咱们本次内存透露的起因——创立了一个后盾运行的 Tokio task),肯定要特地留神:struct 结构实现后,Default 创立进去的长期 struct 就被抛弃了,肯定要做好资源回收。

例如上面这个小例子:(Rust Playground)

struct A {i: i32,}

impl Default for A {fn default() -> Self {println!("called A::default()");
        A {i: 42}
    }
}

#[derive(Default)]
struct B {
    a: A,
    i: i32,
}

impl B {fn new(a: A) -> Self {
        B {
            a,
            // A::default() is called in B::default(), even though "a" is provided here.
            ..Default::default()}
    }
}

fn main() {let a = A { i: 1};
    let b = B::new(a);
    println!("{}", b.a.i);
}

struct A 的 default 办法是会被调用的,打印出 called A::default()

总结

  • 排查 Rust 程序的内存透露,咱们能够用 jemalloc 的 heap profiling 导出 dump 文件;再生成火焰图可直观展示内存应用状况。
  • Tokio-console 能够不便地显示出 Tokio runtime 的 task 运行状况;要特地留神一直增长的 idle tasks。
  • 尽量不要在罕用 struct 的构造方法中留下有副作用的代码。
  • Default 只应该用于值类型 struct。

对于 Greptime

Greptime 格睿科技于 2022 年创建,目前正在欠缺和打造时序数据库 GreptimeDB 和格睿云 GreptimeCloud 这两款产品。

GreptimeDB 是一款用 Rust 语言编写的时序数据库,具备分布式、开源、云原生、兼容性强等特点,帮忙企业实时读写、解决和剖析时序数据的同时,升高长期存储的老本。

GreptimeCloud 基于开源的 GreptimeDB,为用户提供全托管的 DBaaS,以及与可观测性、物联网等畛域联合的利用产品。利用云提供软件和服务,能够达到疾速的自助开明和交付,标准化的运维反对,和更好的资源弹性。GreptimeCloud 已正式凋谢内测,欢送关注公众号或官网理解最新动静!

官网:https://greptime.com/

公众号:GreptimeDB

GitHub: https://github.com/GreptimeTeam/greptimedb

文档:https://docs.greptime.com/

Twitter: https://twitter.com/Greptime

Slack: https://greptime.com/slack

LinkedIn: https://www.linkedin.com/company/greptime/

退出移动版