关于rust:如何在生产环境排查-Rust-内存占用过高问题

145次阅读

共计 6431 个字符,预计需要花费 17 分钟才能阅读完成。


📄

文|魏熙凯(蚂蚁团体技术专家)

本文 6320 字 浏览 10 分钟

内存平安的 Rust,尽管根本不会呈现内存透露,但如何正当分配内存,是每个简单利用都要面临的问题。往往随着业务的不同,雷同的代码可能会产生不同的内存占用。因而,有不小的概率会呈现内存应用过多、内存逐步增长不开释的问题。

本文我想分享一下,咱们在实际过程中遇到的对于内存占用过高的问题。对于这些内存问题,在本文中会做出简略的分类,以及提供咱们在生产环境下进行排查定位的办法给大家参考。

本文最先发表于 RustMagazine 中文月刊

(https://rustmagazine.github.io/rust_magazine_2021/chapter_5/rust-memory-troubleshootting.html)

内存分配器

首先在生产环境中,咱们往往不会抉择默认的内存分配器(malloc),而是会抉择 jemalloc,能够提供更好的多核性能以及更好的防止内存碎片(具体起因能够参考[1])。Rust 的生态中,对于 jemalloc 的封装有很多优良的库,这里咱们就不纠结于哪一个库更好,咱们更关怀如何应用 jemalloc 提供的剖析能力,帮忙咱们诊断内存问题。

浏览 jemalloc 的应用文档,能够晓得其提供了基于采样形式的内存 profile 能力,而且能够通过 mallctl 能够设置 prof.active 和 prof.dump 这两个选项,来达到动态控制内存 profile 的开关和输入内存 profile 信息的成果。

内存快速增长直至 oom

这样的状况个别是雷同的代码在面对不同的业务场景时会呈现,因为某种特定的输出(往往是大量的数据)引起程序的内存快速增长。

不过有了下面提到的 memory profiling 性能,疾速的内存增长其实一个非常容易解决的状况,为咱们能够在快速增长的过程中关上 profile 开关,一段时间后,输入 profile 后果,通过相应的工具进行可视化,就能够分明地理解到哪些函数被调用,进行了哪些构造的内存调配。

不过这里分为两种状况:能够复现以及难以复现,对于两种状况的解决伎俩是不一样的,上面对于这两种状况别离给出可操作的计划。

能够复现

能够复现的场景其实是最容易的解决的问题,因为咱们能够在复现期间采纳动静关上 profile,在短时间内的取得大量的内存调配信息即可。

上面给出一个残缺的 demo,展现一下在 Rust 利用中如何进行动静的内存 profile。

本文章,我会采纳 jemalloc-sys jemallocator jemalloc-ctl 这三个 Rust 库来进行内存的 profile,这三个库的性能次要是:

jemalloc-sys: 封装 jemalloc。

jemallocator: 实现了 Rust 的 GlobalAlloc,用来替换默认的内存分配器。

jemalloc-ctl: 提供了对于 mallctl 的封装,能够用来进行 tuning、动静配置分配器的配置、以及获取分配器的统计信息等。

上面是 demo 工程的依赖:

[dependencies]
jemallocator = "0.3.2"
jemalloc-ctl = "0.3.2"
[dependencies.jemalloc-sys]
version = "0.3.2"
features = ["stats", "profiling", "unprefixed_malloc_on_supported_platforms"]
[profile.release]
debug = true

其中比拟要害的是 jemalloc-sys 的几个 features 须要关上,否则后续的 profile 会遇到失败的状况,另外须要强调的是 demo 的运行环境是在 Linux 环境下运行的。

而后 demo 的 src/main.rs 的代码如下:

use jemallocator;
use jemalloc_ctl::{AsName, Access};
use std::collections::HashMap;
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
const PROF_ACTIVE: &'static [u8] = b"prof.active\0";
const PROF_DUMP: &'static [u8] = b"prof.dump\0";
const PROFILE_OUTPUT: &'static [u8] = b"profile.out\0";
fn set_prof_active(active: bool) {let name = PROF_ACTIVE.name();
    name.write(active).expect("Should succeed to set prof");
}
fn dump_profile() {let name = PROF_DUMP.name();
    name.write(PROFILE_OUTPUT).expect("Should succeed to dump profile")
}
fn main() {set_prof_active(true);
    let mut buffers: Vec<HashMap<i32, i32>> = Vec::new();
    for _ in 0..100 {buffers.push(HashMap::with_capacity(1024));
    }
    set_prof_active(false);
    dump_profile();}

demo 曾经是十分简化的测试用例了,次要做如下的阐明:

set_prof_activedump_profile 都是通过 jemalloc-ctl 来调用 jemalloc 提供的 mallctl 函数,通过给相应的 key 设置 value 即可,比方这里就是给 prof.active 设置布尔值,给 profile.dump 设置 dump 的文件门路。

编译实现之后,间接运行程序是不行的,须要设置好环境变量(开启内存 profile 性能):

export MALLOC_CONF=prof:true

而后再运行程序,就会输入一份 memory profile 文件,demo 中文件名字曾经写死 —— profile.out,这个是一份文本文件,不利于间接察看(没有直观的 symbol)。

通过 jeprof 等工具,能够间接将其转化成可视化的图形:

jeprof --show_bytes --pdf <path_to_binary> ./profile.out > ./profile.pdf

这样就能够将其可视化,从下图中,咱们能够清晰地看到所有的内存起源:

这个 demo 的整体流程就实现了,间隔利用到生产的话,只差一些 trivial 的工作,上面是咱们在生产的实际:

  • 将其封装成 HTTP 服务,能够通过 curl 命令间接触发,将后果通过 HTTP response 返回。
  • 反对设置 profile 时长。
  • 解决并发触发 profile 的状况。

说到这里,这个计划其实有一个益处始终没有提到,就是它的动态性。因为开启内存 profile 性能,势必是会对性能产生肯定的影响(尽管这里开启的影响并不是特地大),咱们天然是心愿在没有问题的时候,防止开启这个 profile 性能,因而这个动静开关还是十分实用的。

难以复现

事实上,能够稳固复现的问题都不是问题。生产上,最麻烦的问题是难以复现的问题,难以复现的问题就像是一个定时炸弹,复现条件很刻薄导致难以精准定位问题,然而问题又会冷不丁地呈现,很是让人头疼。

个别对于难以复现的问题,次要的思路是提前准备好保留现场,在问题产生的时候,尽管服务出了问题,然而咱们保留了出问题的现场。比方这里的内存占用过多的问题,有一个很不错的思路就是:在 oom 的时候,产生 coredump。

不过咱们在生产的实际并没有采纳 coredump 这个办法,次要起因是生产环境的服务器节点内存往往较大,产生的 coredump 也十分大,光是产生 coredump 就须要破费不少工夫,会影响立即重启的速度,此外剖析、传输、存储都不太不便。

这里介绍一下咱们在生产环境下采纳的计划,实际上也是非常简单的办法,通过 jemalloc 提供的性能,能够很简略的进行间接性地输入内存 profile 后果。

在启动应用了 jemalloc 的、筹备长期运行的程序,应用环境变量设置 jemalloc 参数:

export MALLOC_CONF=prof:true,lg_prof_interval:30

这里的参数减少了一个 lg_prof_interval:30,其含意是内存每减少 1GB(2^30,能够依据须要批改,这里只是一个例子),就输入一份内存 profile。这样随着工夫的推移,如果产生了内存的忽然增长(超过设置的阈值),那么相应的 profile 肯定会产生,那么咱们就能够在产生问题的时候,依据文件的创立日期,定位到出问题的时刻,内存到底产生了什么样的调配。

内存迟缓增长不开释

不同于内存的急速增长,内存整体的应用处于一个稳固的状态,然而随着工夫的推移,内存又在稳固地、迟缓地增长。通过下面所说的办法,很难发现内存到底在哪里应用了。

这个问题也是咱们在生产碰到的十分辣手的问题之一,相较于此前的激烈变动,咱们不再关怀产生了那些调配事件,咱们更关怀的是以后的内存散布状况,然而在没有 GC 的 Rust 中,察看以后程序的内存散布状况,并不是一件很简略的事件(尤其是在不影响生产运行的状况下)。

针对这个状况,咱们在生产环境中的实际是这样的:

手动开释局部构造(往往是缓存)内存
而后察看前后的内存变动(开释了多少内存),确定各个模块的内存大小

而借助 jemalloc 的统计性能,能够获取到以后的内存使用量,咱们齐全能够反复进行 开释制订模块的内存 + 计算开释大小,来确定内存的散布状况。

这个计划的缺点也是很显著的,就是参加内存占用检测的模块是先验的(你无奈发现你认知以外的内存占用模块),不过这个缺点还是能够承受的,因为一个程序中可能占用内存过大的中央,咱们往往都是晓得的。

上面给出一个 demo 工程,能够依据这个 demo 工程,利用到生产。

上面是 demo 工程的依赖:

[dependencies]
jemallocator = "0.3.2"
jemalloc-ctl = "0.3.2"
[dependencies.jemalloc-sys]
version = "0.3.2"
features = ["stats", "profiling", "unprefixed_malloc_on_supported_platforms"]
[profile.release]
debug = true

demo 的 src/main.rs 的代码:

use jemallocator;
use jemalloc_ctl::{epoch, stats};
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
fn alloc_cache() -> Vec<i8> {let mut v = Vec::with_capacity(1024 * 1024);
    v.push(0i8);
    v
}
fn main() {let cache_0 = alloc_cache();
    let cache_1 = alloc_cache();
    let e = epoch::mib().unwrap();
    let allocated_stats = stats::allocated::mib().unwrap();
    let mut heap_size = allocated_stats.read().unwrap();
    drop(cache_0);
    e.advance().unwrap();
    let new_heap_size = allocated_stats.read().unwrap();
    println!("cache_0 size:{}B", heap_size - new_heap_size);
    heap_size = new_heap_size;
    drop(cache_1);
    e.advance().unwrap();
    let new_heap_size = allocated_stats.read().unwrap();
    println!("cache_1 size:{}B", heap_size - new_heap_size);
    heap_size = new_heap_size;
    println!("current heap size:{}B", heap_size);
}

比起上一个 demo 长了一点,然而思路非常简单,只有简略阐明一下 jemalloc-ctl 的一个应用留神点即可,在获取新的统计信息之前,必须先调用一下 epoch.advance()

上面是我的编译后运行的输入信息:

cache_0 size:1048576B
cache_1 size:1038336B
current heap size:80488B

这里能够发现,cache_1 的 size 并不是严格的 1MB,这个能够说是失常的,一般来说(不针对这个 demo)次要有两个起因:

在进行内存统计的时候,还有其余的内存变动在产生。
jemalloc 提供的 stats 数据不肯定是齐全精确的,因为他为了更好的多核性能,不可能应用全局的统计,因而实际上是为了性能,放弃了统计信息的一致性。

不过这个信息的不准确,并不会给定位内存占用过高的问题带来妨碍,因为开释的内存往往是微小的,渺小的扰动并不会影响到最终的后果。

另外,其实还有更简略的计划,就是通过开释缓存,间接察看机器的内存变动,不过须要晓得的是内存不肯定是立刻还给 OS 的,而且靠眼睛察看也比拟累,更好的计划还是将这样的内存散布查看性能集成到本人的 Rust 利用之中。

其余通用计划

metrics

另外还有一个十分无效、咱们始终都在应用的计划,就是在产生大量内存调配的时候,将调配的内存大小记录成指标,供后续采集、察看。

整体的计划如下:

  • 应用 Prometheus Client 记录调配的内存(应用层统计)
  • 暴露出 metrics 接口
  • 配置 Promethues server,进行 metrics 拉取
  • 配置 Grafana,连贯 Prometheus server,进行可视化展现

内存排查工具

在内存占用过高的排查过程中,也尝试过其余的弱小工具,比方 heaptrack、valgrind 等工具,然而这些工具有一个微小的弊病,就是会带来十分大的 overhead。

一般来说,应用这类工具的话,基本上应用程序是不可能在生产运行的。

也正因如此,在生产的环境下,咱们很少应用这类工具排查内存的问题。

总结

尽管 Rust 曾经帮咱们防止掉了内存透露的问题,然而内存占用过高的问题,我想不少在生产长期运行的程序还是会有十分大的概率呈现的。本文次要分享了咱们在生产环境中遇到的几种内存占用过高的问题场景,以及目前咱们在不影响生产失常服务的状况下,一些罕用的、疾速定位问题的排查计划,心愿能给大家带来一些启发和帮忙。

当然能够必定的是,还有其余咱们没有遇到过的内存问题,也还有更好的、更不便的计划去做内存问题的定位和排查,心愿晓得的同学能够一起多多交换。

参考

[1] Experimental Study of Memory Allocation forHigh-Performance Query Processing
[2] jemalloc 应用文档
[3] jemallocator

对于咱们

咱们是蚂蚁智能监控技术中台的时序存储团队,咱们正在应用 Rust 构建高性能、低成本并具备实时剖析能力的新一代时序数据库。

欢送退出或者举荐

请分割:jiachun.fjc@antgroup.com

* 本周举荐浏览 *

新一代日志型零碎在 SOFAJRaft 中的利用

下一个 Kubernetes 前沿:多集群治理

基于 RAFT 的生产级高性能 Java 实现 – SOFAJRaft 系列内容合辑

终于!SOFATracer 实现了它的链路可视化之旅

正文完
 0