用户态 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 内存压力等级,如 somefull)。
      • 预期解析内容: 内存使用量、内存压力等级。
      • 重要性: 提供特定 cgroup 的内存使用详情和更直接的内存压力信号。

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. 错误处理

MeminfoSourcecollect 方法将返回 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. 错误处理

VmstatSourcecollect 方法将返回 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. 错误处理

CpuStatSourcecollect 方法将返回 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. 错误处理

DiskstatsSourcecollect 方法将返回 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. 检测策略

  1. 检查 /sys/fs/cgroup/cgroup.controllers: 这是 cgroup v2 的一个标志性文件。如果该文件存在且可读,则系统很可能正在使用 cgroup v2。
  2. 检查 /proc/cgroups: 如果 cgroup v2 的标志文件不存在,则检查 /proc/cgroups。该文件列出了所有可用的 cgroup 子系统。如果其中包含 memory 子系统,并且其 hierarchy 字段不为 0,则可能正在使用 cgroup v1。
  3. 默认回退: 如果以上检测都无法明确判断,可以考虑一个默认回退策略(例如,假设为 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_filetotal_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. 错误处理

CgroupV1Sourcecollect 方法将返回 Result<(), Error>

  • I/O 错误: 文件打开或读取失败时,返回 Error::Io
  • 解析错误: 如果文件内容格式不符合预期,导致无法提取或解析数值,返回 Error::ParseError::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: 读取文件内容,解析 somefull 字段,提取其计数。为了简化,初期可以只关注是否存在 somefull 压力,并将其作为字符串存储。
  • 更新 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. 错误处理

CgroupV2Sourcecollect 方法将返回 Result<(), Error>

  • I/O 错误: 文件打开或读取失败时,返回 Error::Io
  • 解析错误: 如果文件内容格式不符合预期,导致无法提取或解析数值,返回 Error::ParseError::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::BufReaderBufReader 会在内部缓冲数据,减少底层系统调用的次数,从而提高文件读取效率。
  • 最小化文件打开/关闭: 尽可能在 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 使用率

  • 按需采集: Collectorcollect_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::ErrorError::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() 中引入配置结构体。