用户态 OOM 数据采集库设计文档
1. 概述
1.1. 设计目标
本库旨在提供一个高效、易用且可扩展的 Rust 库,用于从 Linux 系统中采集内存压力和 I/O 相关指标。这些指标将用于用户态 OOM (Out-Of-Memory) 决策,以应对内核 OOM 策略在业务延时敏感场景下过于保守的问题,避免系统进入 Near-OOM 活锁状态,优先杀死业务进程以保证系统稳定性和其他业务的正常运行。
1.2. 核心功能
- 多源指标采集: 从
/proc
、/sys
等多个系统文件采集内存和 I/O 相关指标。 - cgroup v1/v2 兼容: 自动检测并支持 cgroup v1 和 cgroup v2 两种不同的 cgroup 接口,以获取 cgroup 相关的内存和 I/O 压力指标。
- 统一的指标数据结构: 提供一个统一的结构体来封装所有采集到的指标数据,方便用户消费。
- 模块化设计: 采用 Trait 和模块化结构,方便未来扩展新的指标源。
1.3. 非功能性需求
- 简单易用: 提供简洁明了的 API,降低用户集成和使用的门槛。
- 高性能: 确保指标采集过程对系统资源的占用最小化,避免引入额外的性能开销,影响系统正常运行。
- 可扩展性: 模块化设计,方便未来添加新的指标源或调整现有指标的采集方式。
- 健壮性: 能够优雅地处理文件读取失败、解析错误等异常情况,提供清晰的错误报告。
- 兼容性: 兼容主流 Linux 发行版,并支持 cgroup v1 和 cgroup v2。
2. 关键指标和数据源
为了判断 Near-OOM 状态,我们将关注以下指标及其对应的 Linux 系统文件:
2.1. 内存压力指标
- 可用内存 (MemAvailable):
- 数据源:
/proc/meminfo
- 预期解析内容:
MemAvailable
字段的值 (单位: KB)。 - 重要性: 直接反映系统当前可用于新分配的内存量。
- 数据源:
- 页面交换 (SwapIn/SwapOut):
- 数据源:
/proc/vmstat
- 预期解析内容:
pswpin
(页面换入) 和pswpout
(页面换出) 字段的值。 - 重要性: 大量的页面交换通常是内存压力的强烈信号,表明系统正在将内存内容写入磁盘以释放物理内存。
- 数据源:
- cgroup 内存使用和压力:
- cgroup v1:
- 数据源:
/sys/fs/cgroup/memory/memory.usage_in_bytes
(当前 cgroup 内存使用量),/sys/fs/cgroup/memory/memory.stat
(cgroup 内存统计,如total_inactive_file
,total_active_file
等)。 - 预期解析内容: 内存使用量、文件缓存统计等。
- 重要性: 提供特定 cgroup 的内存使用详情和文件缓存情况。
- 数据源:
- cgroup v2:
- 数据源:
/sys/fs/cgroup/memory.current
(当前 cgroup 内存使用量),/sys/fs/cgroup/memory.pressure
(cgroup 内存压力等级,如some
和full
)。 - 预期解析内容: 内存使用量、内存压力等级。
- 重要性: 提供特定 cgroup 的内存使用详情和更直接的内存压力信号。
- 数据源:
- cgroup v1:
2.2. I/O 相关指标
- I/O 等待时间 (iowait):
- 数据源:
/proc/stat
- 预期解析内容: CPU 统计行中
iowait
字段的值 (单位: jiffies)。 - 重要性: CPU 花在等待 I/O 完成上的时间百分比,高值表示系统存在 I/O 瓶颈,可能导致应用卡顿。
- 数据源:
- 磁盘读写操作 (Read/Write I/O):
- 数据源:
/proc/diskstats
- 预期解析内容: 各个磁盘设备的读写操作次数、读写扇区数、I/O 完成时间等。
- 重要性: 反映磁盘设备的活动强度,高读写负载可能导致系统响应变慢。
- 数据源:
3. 错误处理 (Error Handling)
3.1. Error
枚举设计
我们将定义一个自定义的 Error
枚举,用于统一处理数据采集过程中可能出现的各种错误。这将提高错误报告的清晰度和可维护性。
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
/// 文件I/O错误,例如文件不存在、无权限读取等。
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
/// 数据解析错误,例如文件内容格式不符合预期。
#[error("Data parsing error: {0}")]
Parse(String),
/// cgroup版本检测失败。
#[error("Cgroup version detection failed")]
CgroupVersionDetectionFailed,
/// cgroup v1特有的错误,例如特定文件不存在。
#[error("Cgroup v1 specific error: {0}")]
CgroupV1Specific(String),
/// cgroup v2特有的错误,例如特定文件不存在。
#[error("Cgroup v2 specific error: {0}")]
CgroupV2Specific(String),
/// 其他未知错误。
#[error("Other error: {0}")]
Other(String),
}
3.2. 错误报告
- 错误信息应尽可能详细,包含导致错误的具体原因(例如,哪个文件读取失败,哪一行解析出错)。
- 对于解析错误,应包含导致解析失败的原始字符串片段,以便于调试。
4. 指标数据结构 (Metrics Struct)
我们将定义一个 Metrics
结构体,用于存储所有采集到的内存和 I/O 指标。这个结构体将是用户态 OOM killer 进行决策的主要数据来源。所有字段都将使用无符号整数类型 (如 u64
) 来表示,以避免负值并支持较大的数值。
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Metrics {
// 内存指标 (来自 /proc/meminfo)
pub mem_total_kb: u64, // 总内存 (KB)
pub mem_free_kb: u64, // 空闲内存 (KB)
pub mem_available_kb: u64, // 可用内存 (KB)
pub buffers_kb: u64, // 缓冲区缓存 (KB)
pub cached_kb: u64, // 页面缓存 (KB)
pub swap_total_kb: u64, // 总交换空间 (KB)
pub swap_free_kb: u64, // 空闲交换空间 (KB)
// 虚拟内存统计 (来自 /proc/vmstat)
pub pswpin: u64, // 页面换入次数
pub pswpout: u64, // 页面换出次数
pub pgmajfault: u64, // 大页面错误次数 (通常表示从磁盘加载)
// CPU 统计 (来自 /proc/stat)
pub cpu_user: u64, // 用户模式下的 CPU 时间
pub cpu_nice: u64, // 低优先级用户模式下的 CPU 时间
pub cpu_system: u64, // 系统模式下的 CPU 时间
pub cpu_idle: u64, // 空闲 CPU 时间
pub cpu_iowait: u64, // I/O 等待时间
pub cpu_irq: u64, // 硬中断时间
pub cpu_softirq: u64, // 软中断时间
pub cpu_steal: u64, // 虚拟机偷取时间
pub cpu_guest: u64, // 运行虚拟机的 CPU 时间
pub cpu_guest_nice: u64, // 运行低优先级虚拟机的 CPU 时间
// 磁盘 I/O 统计 (来自 /proc/diskstats) - 简化为总和,或按设备存储
// 为了简化设计,初期可以考虑只采集总的读写操作和字节数
pub disk_read_sectors: u64, // 总读取扇区数
pub disk_write_sectors: u64,// 总写入扇区数
pub disk_read_ios: u64, // 总读取I/O操作数
pub disk_write_ios: u64, // 总写入I/O操作数
// cgroup 内存指标 (根据 cgroup 版本动态填充)
pub cgroup_memory_current_bytes: Option<u64>, // cgroup 当前内存使用量 (字节)
pub cgroup_memory_usage_bytes: Option<u64>, // cgroup 内存使用量 (v1) (字节)
pub cgroup_memory_pressure_level: Option<String>, // cgroup 内存压力等级 (v2, "some", "full")
pub cgroup_memory_stat_inactive_file_bytes: Option<u64>, // cgroup v1 非活跃文件缓存 (字节)
pub cgroup_memory_stat_active_file_bytes: Option<u64>, // cgroup v1 活跃文件缓存 (字节)
}
4.1. 字段说明和单位
- 内存指标:
mem_total_kb
,mem_free_kb
,mem_available_kb
,buffers_kb
,cached_kb
,swap_total_kb
,swap_free_kb
: 单位均为 KB。
- 虚拟内存统计:
pswpin
,pswpout
,pgmajfault
: 均为计数器,表示事件发生的次数。
- CPU 统计:
cpu_user
,cpu_nice
,cpu_system
,cpu_idle
,cpu_iowait
,cpu_irq
,cpu_softirq
,cpu_steal
,cpu_guest
,cpu_guest_nice
: 单位为 jiffies (通常是 1/100 秒)。这些是累积值,需要计算两次采集之间的差值来获取瞬时速率。
- 磁盘 I/O 统计:
disk_read_sectors
,disk_write_sectors
: 单位为扇区数 (通常一个扇区 512 字节)。disk_read_ios
,disk_write_ios
: 单位为操作次数。
- cgroup 内存指标:
cgroup_memory_current_bytes
,cgroup_memory_usage_bytes
,cgroup_memory_stat_inactive_file_bytes
,cgroup_memory_stat_active_file_bytes
: 单位为字节。使用Option<u64>
是因为这些指标可能不适用于所有 cgroup 版本或路径不存在。cgroup_memory_pressure_level
:Option<String>
,存储 cgroup v2 的内存压力等级字符串(“some” 或 “full”)。
4.2. 性能考虑
Metrics
结构体应尽可能扁平化,避免深层嵌套,以减少内存访问开销。
5. 指标源 Trait (MetricSource Trait)
为了实现库的可扩展性和模块化,我们将定义一个 MetricSource
Trait。所有具体的指标数据源(如 MeminfoSource
, VmstatSource
等)都将实现此 Trait。这将允许 Collector
以统一的方式处理不同的数据源。
5.1. Trait 定义
use crate::Metrics; // 假设 Metrics 结构体在 crate 根目录
use crate::Error; // 假设 Error 枚举在 crate 根目录
pub trait MetricSource {
/// 采集并解析指标数据,更新到 Metrics 结构体中。
///
/// # Arguments
///
/// * `metrics` - 一个可变的 `Metrics` 结构体引用,用于存储采集到的数据。
///
/// # Returns
///
/// 返回 `Result<(), Error>`,表示操作是否成功。
/// 成功时返回 `Ok(())`,失败时返回 `Err(Error)`。
fn collect(&self, metrics: &mut Metrics) -> Result<(), Error>;
}
5.2. 预期行为
- 单一职责: 每个
MetricSource
实现应只负责采集和解析其特定的数据源。 - 高效性:
collect
方法的实现应尽可能高效,避免不必要的内存分配和计算。例如,在读取文件时使用BufReader
,并使用字符串查找而非正则表达式进行解析,以减少开销。 - 错误处理:
collect
方法应处理其数据源特有的错误(如文件不存在、权限问题、解析格式不匹配),并将其转换为统一的Error
枚举返回。 - 幂等性: 多次调用
collect
方法应始终返回当前最新的指标数据。 - 无状态或最小状态: 理想情况下,
MetricSource
实现应是无状态的,或者只维护少量必要的配置状态(例如文件路径)。如果需要计算速率(如 CPU 使用率),则状态管理应由Collector
或专门的速率计算模块负责。 - 所有字段都应是基本类型,避免使用昂贵的字符串操作或动态分配。
- 对于 cgroup 相关的可选字段,使用
Option
类型可以避免在不适用的 cgroup 版本中存储无意义的数据。
6. MeminfoSource
模块设计
MeminfoSource
模块负责从 /proc/meminfo
文件中读取和解析内存相关指标。
6.1. 结构体定义
MeminfoSource
结构体将是无状态的,因为它只需要一个文件路径来读取数据。
#[derive(Debug, Default, Clone)]
pub struct MeminfoSource {
// 可以选择存储文件路径,如果需要支持非默认路径
// pub path: PathBuf,
}
impl MeminfoSource {
pub fn new() -> Self {
MeminfoSource {}
}
}
6.2. 数据解析逻辑
MeminfoSource
将实现 MetricSource
Trait 的 collect
方法。
- 文件读取: 使用
std::fs::File::open
打开/proc/meminfo
,并使用std::io::BufReader
进行高效的行读取。 - 逐行解析: 遍历文件中的每一行,查找感兴趣的内存指标(如
MemTotal
,MemFree
,MemAvailable
,Buffers
,Cached
,SwapTotal
,SwapFree
)。 - 提取数值: 对于匹配的行,提取数值部分并将其解析为
u64
。 - 更新
Metrics
: 将解析到的数值更新到传入的Metrics
结构体中对应的字段。
示例解析逻辑 (伪代码):
// 在 MeminfoSource::collect 方法中
let file = File::open("/proc/meminfo")?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
if line.starts_with("MemTotal:") {
metrics.mem_total_kb = parse_value(&line)?;
} else if line.starts_with("MemFree:") {
metrics.mem_free_kb = parse_value(&line)?;
}
// ... 其他内存指标
}
fn parse_value(line: &str) -> Result<u64, Error> {
// 查找数字部分,解析并返回
// 错误处理:如果找不到数字或解析失败,返回 Error::Parse
}
6.3. 错误处理
MeminfoSource
的 collect
方法将返回 Result<(), Error>
。
- I/O 错误: 文件打开或读取失败时,返回
Error::Io
。 - 解析错误: 如果文件内容格式不符合预期,导致无法提取或解析数值,返回
Error::Parse
,并包含详细的错误信息。
7. VmstatSource
模块设计
VmstatSource
模块负责从 /proc/vmstat
文件中读取和解析虚拟内存统计指标。
7.1. 结构体定义
VmstatSource
结构体将是无状态的,因为它只需要一个文件路径来读取数据。
#[derive(Debug, Default, Clone)]
pub struct VmstatSource {
// 可以选择存储文件路径,如果需要支持非默认路径
// pub path: PathBuf,
}
impl VmstatSource {
pub fn new() -> Self {
VmstatSource {}
}
}
7.2. 数据解析逻辑
VmstatSource
将实现 MetricSource
Trait 的 collect
方法。
- 文件读取: 使用
std::fs::File::open
打开/proc/vmstat
,并使用std::io::BufReader
进行高效的行读取。 - 逐行解析: 遍历文件中的每一行,查找感兴趣的虚拟内存指标(如
pswpin
,pswpout
,pgmajfault
)。 - 提取数值: 对于匹配的行,提取数值部分并将其解析为
u64
。 - 更新
Metrics
: 将解析到的数值更新到传入的Metrics
结构体中对应的字段。
示例解析逻辑 (伪代码):
// 在 VmstatSource::collect 方法中
let file = File::open("/proc/vmstat")?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
if line.starts_with("pswpin ") {
metrics.pswpin = parse_value(&line)?;
} else if line.starts_with("pswpout ") {
metrics.pswpout = parse_value(&line)?;
} else if line.starts_with("pgmajfault ") {
metrics.pgmajfault = parse_value(&line)?;
}
// ... 其他 vmstat 指标
}
fn parse_value(line: &str) -> Result<u64, Error> {
// 查找数字部分,解析并返回
// 错误处理:如果找不到数字或解析失败,返回 Error::Parse
}
7.3. 错误处理
VmstatSource
的 collect
方法将返回 Result<(), Error>
。
- I/O 错误: 文件打开或读取失败时,返回
Error::Io
。 - 解析错误: 如果文件内容格式不符合预期,导致无法提取或解析数值,返回
Error::Parse
,并包含详细的错误信息。
8. CpuStatSource
模块设计
CpuStatSource
模块负责从 /proc/stat
文件中读取和解析 CPU 统计指标,特别是 iowait
。
8.1. 结构体定义
CpuStatSource
结构体将是无状态的,因为它只需要一个文件路径来读取数据。
#[derive(Debug, Default, Clone)]
pub struct CpuStatSource {
// 可以选择存储文件路径,如果需要支持非默认路径
// pub path: PathBuf,
}
impl CpuStatSource {
pub fn new() -> Self {
CpuStatSource {}
}
}
8.2. 数据解析逻辑
CpuStatSource
将实现 MetricSource
Trait 的 collect
方法。
- 文件读取: 使用
std::fs::File::open
打开/proc/stat
,并使用std::io::BufReader
进行高效的行读取。 - 查找 CPU 行: 查找以
cpu
开头的行,这是所有 CPU 的总统计信息。 - 提取数值: 从 CPU 行中提取
user
,nice
,system
,idle
,iowait
,irq
,softirq
,steal
,guest
,guest_nice
等字段的数值,并将其解析为u64
。 - 更新
Metrics
: 将解析到的数值更新到传入的Metrics
结构体中对应的字段。
示例解析逻辑 (伪代码):
// 在 CpuStatSource::collect 方法中
let file = File::open("/proc/stat")?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
if line.starts_with("cpu ") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 11 { // 确保有足够的字段
metrics.cpu_user = parse_value(parts[1])?;
metrics.cpu_nice = parse_value(parts[2])?;
metrics.cpu_system = parse_value(parts[3])?;
metrics.cpu_idle = parse_value(parts[4])?;
metrics.cpu_iowait = parse_value(parts[5])?;
metrics.cpu_irq = parse_value(parts[6])?;
metrics.cpu_softirq = parse_value(parts[7])?;
metrics.cpu_steal = parse_value(parts[8])?;
metrics.cpu_guest = parse_value(parts[9])?;
metrics.cpu_guest_nice = parse_value(parts[10])?;
break; // 只需处理总的 cpu 行
} else {
return Err(Error::Parse(format!("Invalid cpu line format in /proc/stat: {}", line)));
}
}
}
fn parse_value(s: &str) -> Result<u64, Error> {
s.parse::<u64>().map_err(|e| Error::Parse(format!("Failed to parse u64 from '{}': {}", s, e)))
}
8.3. 错误处理
CpuStatSource
的 collect
方法将返回 Result<(), Error>
。
- I/O 错误: 文件打开或读取失败时,返回
Error::Io
。 - 解析错误: 如果文件内容格式不符合预期(例如,
cpu
行不存在或字段数量不足,或者数值无法解析),返回Error::Parse
,并包含详细的错误信息。
9. DiskstatsSource
模块设计
DiskstatsSource
模块负责从 /proc/diskstats
文件中读取和解析磁盘 I/O 统计指标。
9.1. 结构体定义
DiskstatsSource
结构体将是无状态的,因为它只需要一个文件路径来读取数据。
#[derive(Debug, Default, Clone)]
pub struct DiskstatsSource {
// 可以选择存储文件路径,如果需要支持非默认路径
// pub path: PathBuf,
}
impl DiskstatsSource {
pub fn new() -> Self {
DiskstatsSource {}
}
}
9.2. 数据解析逻辑
DiskstatsSource
将实现 MetricSource
Trait 的 collect
方法。
- 文件读取: 使用
std::fs::File::open
打开/proc/diskstats
,并使用std::io::BufReader
进行高效的行读取。 - 逐行解析:
/proc/diskstats
的每一行代表一个磁盘设备或分区。我们将遍历所有行,解析每个设备的统计信息。 - 提取数值: 对于每一行,提取以下关键字段:
read_ios
(第 4 字段): 完成的读操作次数read_sectors
(第 6 字段): 读取的扇区数write_ios
(第 8 字段): 完成的写操作次数write_sectors
(第 10 字段): 写入的扇区数
- 聚合到
Metrics
: 将所有设备的读写扇区数和 I/O 操作数累加,更新到Metrics
结构体中的disk_read_sectors
,disk_write_sectors
,disk_read_ios
,disk_write_ios
字段。
示例解析逻辑 (伪代码):
// 在 DiskstatsSource::collect 方法中
let file = File::open("/proc/diskstats")?;
let reader = BufReader::new(file);
// 初始化累加器
let mut total_read_sectors = 0;
let mut total_write_sectors = 0;
let mut total_read_ios = 0;
let mut total_write_ios = 0;
for line in reader.lines() {
let line = line?;
let parts: Vec<&str> = line.split_whitespace().collect();
// /proc/diskstats 至少有 11 个字段
if parts.len() >= 11 {
// 字段索引从 0 开始
// major, minor, device_name, reads_completed, reads_merged, sectors_read,
// read_time_ms, writes_completed, writes_merged, sectors_written, write_time_ms, ...
let read_ios = parse_value(parts[3])?;
let read_sectors = parse_value(parts[5])?;
let write_ios = parse_value(parts[7])?;
let write_sectors = parse_value(parts[9])?;
total_read_ios += read_ios;
total_read_sectors += read_sectors;
total_write_ios += write_ios;
total_write_sectors += write_sectors;
} else {
// 记录警告或错误,但继续处理其他行
eprintln!("Warning: Malformed line in /proc/diskstats: {}", line);
}
}
metrics.disk_read_ios = total_read_ios;
metrics.disk_read_sectors = total_read_sectors;
metrics.disk_write_ios = total_write_ios;
metrics.disk_write_sectors = total_write_sectors;
fn parse_value(s: &str) -> Result<u64, Error> {
s.parse::<u64>().map_err(|e| Error::Parse(format!("Failed to parse u64 from '{}': {}", s, e)))
}
9.3. 错误处理
DiskstatsSource
的 collect
方法将返回 Result<(), Error>
。
- I/O 错误: 文件打开或读取失败时,返回
Error::Io
。 - 解析错误: 如果文件内容格式不符合预期(例如,某一行字段数量不足,或者数值无法解析),返回
Error::Parse
,并包含详细的错误信息。对于部分解析失败但其他行仍然有效的情况,可以考虑记录警告并继续处理,而不是立即返回错误,以提高健壮性。
10. cgroup
模块设计
cgroup
模块负责处理与 Linux cgroup 相关的指标采集。由于 cgroup v1 和 cgroup v2 的接口存在显著差异,本模块将提供一个机制来检测当前系统使用的 cgroup 版本,并根据版本动态地选择相应的指标采集逻辑。
10.1. cgroup::detect_version()
函数的逻辑
detect_version()
函数将用于确定当前系统正在使用 cgroup v1 还是 cgroup v2。
10.1.1. 检测策略
- 检查
/sys/fs/cgroup/cgroup.controllers
: 这是 cgroup v2 的一个标志性文件。如果该文件存在且可读,则系统很可能正在使用 cgroup v2。 - 检查
/proc/cgroups
: 如果 cgroup v2 的标志文件不存在,则检查/proc/cgroups
。该文件列出了所有可用的 cgroup 子系统。如果其中包含memory
子系统,并且其hierarchy
字段不为 0,则可能正在使用 cgroup v1。 - 默认回退: 如果以上检测都无法明确判断,可以考虑一个默认回退策略(例如,假设为 cgroup v1 或返回未知错误)。
10.1.2. 函数签名
pub enum CgroupVersion {
V1,
V2,
Unknown,
}
pub fn detect_version() -> CgroupVersion {
// 实现上述检测逻辑
// 返回检测到的 cgroup 版本
}
10.1.3. 错误处理
- 文件 I/O 错误(如文件不存在、无权限)应被捕获,并可能导致返回
CgroupVersion::Unknown
或记录警告。 - 解析错误(如果需要解析文件内容)也应妥善处理。
10.2. cgroup::v1
子模块设计
cgroup::v1
子模块负责从 cgroup v1 接口采集内存相关的指标。
10.2.1. CgroupV1Source
结构体定义
CgroupV1Source
结构体将存储 cgroup v1 的根路径,以便于访问其下的各个指标文件。
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct CgroupV1Source {
pub cgroup_root: PathBuf, // 例如:/sys/fs/cgroup/memory/
}
impl CgroupV1Source {
pub fn new(cgroup_root: PathBuf) -> Self {
CgroupV1Source { cgroup_root }
}
}
10.2.2. 数据解析逻辑
CgroupV1Source
将实现 MetricSource
Trait 的 collect
方法。
- 文件路径: 根据
cgroup_root
构建各个指标文件的完整路径,例如:memory.usage_in_bytes
:cgroup_root.join("memory.usage_in_bytes")
memory.stat
:cgroup_root.join("memory.stat")
- 文件读取和解析:
memory.usage_in_bytes
: 读取文件内容,直接解析为u64
。memory.stat
: 读取文件内容,逐行解析,查找total_inactive_file
和total_active_file
等字段,提取数值并解析为u64
。
- 更新
Metrics
: 将解析到的数值更新到传入的Metrics
结构体中对应的字段(cgroup_memory_usage_bytes
,cgroup_memory_stat_inactive_file_bytes
,cgroup_memory_stat_active_file_bytes
)。
示例解析逻辑 (伪代码):
// 在 CgroupV1Source::collect 方法中
let usage_path = &self.cgroup_root.join("memory.usage_in_bytes");
let usage_bytes = read_and_parse_u64(usage_path)?;
metrics.cgroup_memory_usage_bytes = Some(usage_bytes);
let stat_path = &self.cgroup_root.join("memory.stat");
let stat_content = read_file_to_string(stat_path)?; // 辅助函数读取文件内容
for line in stat_content.lines() {
if line.starts_with("total_inactive_file ") {
metrics.cgroup_memory_stat_inactive_file_bytes = Some(parse_value(&line)?);
} else if line.starts_with("total_active_file ") {
metrics.cgroup_memory_stat_active_file_bytes = Some(parse_value(&line)?);
}
}
fn read_and_parse_u64(path: &Path) -> Result<u64, Error> {
// 读取文件内容,trim,然后解析为 u64
// 错误处理:Io 错误或 Parse 错误
}
fn read_file_to_string(path: &Path) -> Result<String, Error> {
// 读取文件内容为字符串
// 错误处理:Io 错误
}
fn parse_value(line: &str) -> Result<u64, Error> {
// 查找数字部分,解析并返回
// 错误处理:如果找不到数字或解析失败,返回 Error::Parse
}
10.2.3. 错误处理
CgroupV1Source
的 collect
方法将返回 Result<(), Error>
。
- I/O 错误: 文件打开或读取失败时,返回
Error::Io
。 - 解析错误: 如果文件内容格式不符合预期,导致无法提取或解析数值,返回
Error::Parse
或Error::CgroupV1Specific
,并包含详细的错误信息。
10.3. cgroup::v2
子模块设计
cgroup::v2
子模块负责从 cgroup v2 接口采集内存相关的指标。
10.3.1. CgroupV2Source
结构体定义
CgroupV2Source
结构体将存储 cgroup v2 的根路径,以便于访问其下的各个指标文件。
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct CgroupV2Source {
pub cgroup_root: PathBuf, // 例如:/sys/fs/cgroup/
}
impl CgroupV2Source {
pub fn new(cgroup_root: PathBuf) -> Self {
CgroupV2Source { cgroup_root }
}
}
10.3.2. 数据解析逻辑
CgroupV2Source
将实现 MetricSource
Trait 的 collect
方法。
- 文件路径: 根据
cgroup_root
构建各个指标文件的完整路径,例如:memory.current
:cgroup_root.join("memory.current")
memory.pressure
:cgroup_root.join("memory.pressure")
- 文件读取和解析:
memory.current
: 读取文件内容,直接解析为u64
。memory.pressure
: 读取文件内容,解析some
和full
字段,提取其计数。为了简化,初期可以只关注是否存在some
或full
压力,并将其作为字符串存储。
- 更新
Metrics
: 将解析到的数值更新到传入的Metrics
结构体中对应的字段(cgroup_memory_current_bytes
,cgroup_memory_pressure_level
)。
示例解析逻辑 (伪代码):
// 在 CgroupV2Source::collect 方法中
let current_path = &self.cgroup_root.join("memory.current");
let current_bytes = read_and_parse_u64(current_path)?;
metrics.cgroup_memory_current_bytes = Some(current_bytes);
let pressure_path = &self.cgroup_root.join("memory.pressure");
let pressure_content = read_file_to_string(pressure_path)?; // 辅助函数读取文件内容
// 简单示例:检查是否存在 "full" 压力
if pressure_content.contains("full") {
metrics.cgroup_memory_pressure_level = Some("full".to_string());
} else if pressure_content.contains("some") {
metrics.cgroup_memory_pressure_level = Some("some".to_string());
} else {
metrics.cgroup_memory_pressure_level = None;
}
fn read_and_parse_u64(path: &Path) -> Result<u64, Error> {
// 读取文件内容,trim,然后解析为 u64
// 错误处理:Io 错误或 Parse 错误
}
fn read_file_to_string(path: &Path) -> Result<String, Error> {
// 读取文件内容为字符串
// 错误处理:Io 错误
}
10.3.3. 错误处理
CgroupV2Source
的 collect
方法将返回 Result<(), Error>
。
- I/O 错误: 文件打开或读取失败时,返回
Error::Io
。 - 解析错误: 如果文件内容格式不符合预期,导致无法提取或解析数值,返回
Error::Parse
或Error::CgroupV2Specific
,并包含详细的错误信息。
11. Collector
结构体设计
Collector
是本库的核心协调者,负责聚合所有 MetricSource
并提供统一的接口来采集系统指标。它将根据系统检测到的 cgroup 版本动态地初始化相应的 cgroup 指标源。
11.1. 结构体定义
Collector
结构体将持有所有 MetricSource
的实例。为了支持动态选择 cgroup 版本,cgroup 相关的 MetricSource
将使用 Box<dyn MetricSource>
进行类型擦除。
use std::sync::Arc; // 用于共享 MetricSource 实例,如果需要
#[derive(Debug, Clone)]
pub struct Collector {
meminfo_source: MeminfoSource,
vmstat_source: VmstatSource,
cpu_stat_source: CpuStatSource,
diskstats_source: DiskstatsSource,
cgroup_source: Option<Box<dyn MetricSource + Send + Sync>>, // 动态 cgroup 源
}
11.2. 初始化逻辑 (Collector::new()
)
Collector::new()
方法将负责初始化所有 MetricSource
,并根据 cgroup::detect_version()
的结果来初始化 cgroup_source
。
impl Collector {
pub fn new() -> Result<Self, Error> {
let cgroup_version = cgroup::detect_version();
let cgroup_source: Option<Box<dyn MetricSource + Send + Sync>> = match cgroup_version {
cgroup::CgroupVersion::V1 => {
// 假设 cgroup v1 的根路径是 /sys/fs/cgroup/memory/
// 实际应用中可能需要更复杂的路径检测或配置
let cgroup_root = PathBuf::from("/sys/fs/cgroup/memory/");
if cgroup_root.exists() {
Some(Box::new(cgroup::v1::CgroupV1Source::new(cgroup_root)))
} else {
eprintln!("Warning: Cgroup v1 detected but root path /sys/fs/cgroup/memory/ does not exist.");
None
}
},
cgroup::CgroupVersion::V2 => {
// 假设 cgroup v2 的根路径是 /sys/fs/cgroup/
let cgroup_root = PathBuf::from("/sys/fs/cgroup/");
if cgroup_root.exists() {
Some(Box::new(cgroup::v2::CgroupV2Source::new(cgroup_root)))
} else {
eprintln!("Warning: Cgroup v2 detected but root path /sys/fs/cgroup/ does not exist.");
None
}
},
cgroup::CgroupVersion::Unknown => {
eprintln!("Warning: Could not detect cgroup version. Cgroup metrics will not be collected.");
None
}
};
Ok(Collector {
meminfo_source: MeminfoSource::new(),
vmstat_source: VmstatSource::new(),
cpu_stat_source: CpuStatSource::new(),
diskstats_source: DiskstatsSource::new(),
cgroup_source,
})
}
}
11.3. 聚合指标 (collect_metrics()
)
collect_metrics()
方法将遍历所有内部的 MetricSource
实例,调用它们的 collect
方法,并将数据聚合到一个 Metrics
结构体中。
impl Collector {
pub fn collect_metrics(&self) -> Result<Metrics, Error> {
let mut metrics = Metrics::default();
// 采集系统通用指标
self.meminfo_source.collect(&mut metrics)?;
self.vmstat_source.collect(&mut metrics)?;
self.cpu_stat_source.collect(&mut metrics)?;
self.diskstats_source.collect(&mut metrics)?;
// 采集 cgroup 特定指标(如果可用)
if let Some(cgroup_source) = &self.cgroup_source {
cgroup_source.collect(&mut metrics)?;
}
Ok(metrics)
}
}
11.4. 性能考虑
- 避免重复文件读取: 每个
MetricSource
负责自己的文件读取,但Collector
确保每个源只被调用一次。 - 最小化分配:
collect_metrics
方法在开始时分配一个Metrics
结构体,然后由各个MetricSource
填充,避免在采集过程中频繁分配。 - 错误处理: 任何一个
MetricSource
采集失败,collect_metrics
都会立即返回错误,避免不必要的后续操作。
11.5. 易用性
Collector::new()
提供了一个简单的入口点来初始化所有采集器。collect_metrics()
提供了一个单一的方法来获取所有最新的系统指标。
12. 性能优化策略
为了确保数据采集库对系统资源的占用最小化,我们将采取以下性能优化策略:
12.1. 文件读取优化
- 使用
BufReader
: 在所有需要读取文件的MetricSource
实现中,都将使用std::io::BufReader
。BufReader
会在内部缓冲数据,减少底层系统调用的次数,从而提高文件读取效率。 - 最小化文件打开/关闭: 尽可能在
MetricSource
实例的生命周期内保持文件句柄打开,或者在每次collect
调用时只打开一次文件并立即关闭。对于/proc
和/sys
下的伪文件系统,每次打开的开销通常很小,但使用BufReader
仍然有益。 - 避免不必要的读取: 只读取和解析与所需指标相关的行和字段,避免读取整个文件或解析不相关的数据。
12.2. 数据解析效率
- 字符串查找而非正则表达式: 对于简单的键值对或固定格式的行,优先使用
str::starts_with()
,str::find()
,str::split_whitespace()
等高效的字符串操作,而不是功能强大但开销更大的正则表达式。 - 直接解析为数值类型: 一旦提取到数值字符串,直接尝试解析为
u64
等基本数值类型,避免中间的字符串转换。 - 预分配数据结构:
Metrics
结构体是预定义的,避免在每次采集时动态分配大量内存。
12.3. 内存占用
- 精简
Metrics
结构体:Metrics
结构体只包含必要的指标字段,避免存储冗余数据。 - 无状态或最小状态的
MetricSource
: 大多数MetricSource
实例将是无状态的,或者只存储少量配置信息(如文件路径),从而减少内存占用。 - 避免克隆: 在
collect
方法中,通过传入可变的Metrics
引用 (&mut Metrics
) 来更新数据,避免不必要的数据克隆。
12.4. CPU 使用率
- 按需采集:
Collector
的collect_metrics()
方法只在被调用时才执行采集操作。用户态 OOM killer 可以根据其自身的决策频率来调用此方法,避免不必要的频繁采集。 - 增量计算: 对于像 CPU 时间或磁盘 I/O 计数器这样的累积指标,如果需要计算速率,应在
Collector
或更高层逻辑中维护上一次采集的值,并计算两次采集之间的差值,而不是在每个MetricSource
中独立进行。这可以避免重复的状态管理。
13. 易用性
为了确保本库能够被用户态 OOM killer 或其他监控工具轻松集成和使用,我们将重点关注以下易用性方面:
13.1. 简洁的 API 设计
- 单一入口点:
Collector::new()
和Collector::collect_metrics()
方法将作为库的主要入口点,提供清晰、直观的接口。 - 直观的
Metrics
结构体:Metrics
结构体字段命名清晰,单位明确,方便用户理解和使用采集到的数据。 - 避免复杂配置: 尽量减少需要用户手动配置的选项。对于 cgroup 路径等,尝试自动检测或提供合理的默认值。如果需要配置,应提供简单明了的配置方式。
13.2. 错误报告
- 清晰的错误信息:
Error
枚举将提供详细的错误信息,帮助用户快速诊断问题。例如,Error::Io
会包含底层的std::io::Error
,Error::Parse
会指出哪个文件哪一行解析失败。 - 可区分的错误类型: 不同的错误场景对应不同的
Error
变体,方便用户进行模式匹配和针对性处理。 - 警告机制: 对于非致命但可能影响数据完整性的问题(例如,某个 cgroup 文件不存在但其他文件仍然可读),可以通过
eprintln!
或日志库输出警告,而不是直接返回错误中断整个采集过程。
13.3. 依赖管理
- 最小化外部依赖: 尽量减少对第三方库的依赖,以降低项目的复杂性和编译时间。目前,
thiserror
是一个轻量级且广泛使用的错误处理库,是合理的选择。 - 清晰的 Cargo.toml:
Cargo.toml
文件将清晰地列出所有依赖项,并提供必要的元数据(如版本、作者、描述)。
13.4. 文档和示例
- 详尽的文档: 提供清晰、全面的 Rust Doc 文档,解释每个模块、结构体、Trait 和函数的作用、参数、返回值和可能抛出的错误。
- 使用示例: 提供简单的代码示例,演示如何初始化
Collector
、采集指标以及如何访问Metrics
结构体中的数据。这将大大降低新用户的学习曲线。 - 设计文档: 本设计文档本身就是易用性的一部分,它为库的架构和实现提供了高级视图。
14. 使用示例和集成指南
本节将提供一个简单的使用示例,演示如何集成和使用本数据采集库。
14.1. Cargo.toml
配置
为了使用本库,需要在项目的 Cargo.toml
文件中添加相应的依赖。
[dependencies]
cg_stats = { path = "path/to/this/library" } # 假设本库名为 cg_stats
thiserror = "1.0" # 如果使用 thiserror 进行错误处理
14.2. 基本使用示例
以下是一个简单的 Rust 代码示例,演示如何初始化 Collector
并周期性地采集指标。
use cg_stats::{Collector, Metrics, Error};
use std::time::Duration;
use std::thread;
fn main() -> Result<(), Error> {
// 1. 初始化 Collector
// Collector 会自动检测 cgroup 版本并初始化相应的指标源
let collector = Collector::new()?;
println!("Collector initialized successfully.");
// 2. 周期性采集指标
loop {
let metrics = collector.collect_metrics()?;
println!("\n--- System Metrics ---");
println!("MemAvailable: {} KB", metrics.mem_available_kb);
println!("SwapOut: {} pages", metrics.pswpout);
println!("CPU IOWait: {} jiffies", metrics.cpu_iowait);
println!("Disk Read Sectors: {}", metrics.disk_read_sectors);
if let Some(current_bytes) = metrics.cgroup_memory_current_bytes {
println!("Cgroup Memory Current: {} bytes", current_bytes);
}
if let Some(pressure_level) = metrics.cgroup_memory_pressure_level {
println!("Cgroup Memory Pressure: {}", pressure_level);
}
// 在实际应用中,这里可以将 metrics 发送到 OOM killer 决策模块
// 或进行日志记录、告警等操作。
thread::sleep(Duration::from_secs(5)); // 每 5 秒采集一次
}
}
14.3. 集成指南
- OOM 决策逻辑: 本库只负责数据采集。用户需要根据采集到的
Metrics
数据,自行实现 OOM 决策逻辑。例如,可以设置阈值:当mem_available_kb
低于某个值且cpu_iowait
持续升高时,触发 Near-OOM 预警。 - 错误处理: 在集成时,应妥善处理
Collector::new()
和collector.collect_metrics()
可能返回的Error
。根据错误类型,可以选择重试、记录日志或退出程序。 - 异步采集: 对于对性能要求极高的场景,可以考虑在单独的线程或异步任务中运行
collect_metrics()
,以避免阻塞主应用逻辑。 - 配置: 如果未来需要支持更复杂的配置(例如,指定 cgroup 路径、过滤特定磁盘设备),可以在
Collector::new()
中引入配置结构体。