WEBKT

使用eBPF统计进程CPU使用率并按进程名聚合的实践方案

186 0 0 0

本文将探讨如何使用eBPF技术来统计系统中所有进程的CPU使用情况,并按照进程名进行聚合,最终找出最消耗CPU资源的进程。我们将深入研究eBPF程序的编写、部署以及用户态程序的实现,提供一个可操作的实践方案。

1. eBPF简介

eBPF(extended Berkeley Packet Filter)是一种强大的内核技术,允许用户在内核中安全地运行自定义代码,而无需修改内核源代码或加载内核模块。eBPF最初设计用于网络数据包过滤,但现在已广泛应用于性能分析、安全监控等领域。

eBPF程序运行在内核虚拟机中,具有高性能和低开销的特点。通过与内核事件关联,eBPF程序可以在特定事件发生时被触发,例如进程调度、系统调用等。这使得eBPF成为性能分析的理想工具。

2. 方案设计

我们的目标是统计每个进程的CPU使用情况,并按进程名聚合。为了实现这个目标,我们可以采用以下方案:

  1. 内核事件监听: 使用perf_events来监听CPU调度事件(sched:sched_process_exec, sched:sched_switch)。sched_process_exec用于捕获新进程的启动,sched_switch用于捕获进程切换。
  2. eBPF程序: 编写eBPF程序,在sched_process_exec事件中记录进程名和PID,在sched_switch事件中记录进程切换的时间戳。通过计算进程在CPU上运行的时间,可以得到每个进程的CPU使用情况。
  3. 数据聚合: 使用eBPF的map数据结构来存储和聚合CPU使用情况。我们可以使用进程名作为key,CPU使用时间作为value。
  4. 用户态程序: 编写用户态程序,加载eBPF程序到内核,并定期从map中读取统计结果。用户态程序负责将结果按进程名进行聚合,并找出最耗CPU的进程。

3. eBPF程序实现

下面是一个简化的eBPF程序示例,用于统计进程的CPU使用情况:

#include <linux/bpf.h>
#include <bpf_helpers.h>
#include <linux/sched.h>

#define MAX_PROCESS_NAME 64

struct process_key {
    char name[MAX_PROCESS_NAME];
};

struct value_t {
    u64 duration;
    u64 last_timestamp;
};

BPF_HASH(cpu_usage, struct process_key, struct value_t);
BPF_HASH(process_start, u32, u64); // pid -> timestamp

// 辅助函数,用于复制进程名
static int copy_process_name(char *dest, const char *source)
{
    int i = 0;
    for (; i < MAX_PROCESS_NAME - 1 && source[i] != '\0'; i++)
        dest[i] = source[i];
    dest[i] = '\0';
    return 0;
}

// 处理进程启动事件
int kprobe__sched_process_exec(struct pt_regs *ctx, struct task_struct *p)
{
    struct process_key key = {};
    struct value_t zero = {};

    // 复制进程名
    copy_process_name(key.name, p->comm);

    // 初始化cpu_usage map
    cpu_usage.insert(&key, &zero);

    // 记录进程启动时间
    u64 now = bpf_ktime_get_ns();
    u32 pid = bpf_get_current_pid_tgid();
    process_start.update(&pid, &now);

    return 0;
}

// 处理进程切换事件
int kprobe__sched_switch(struct pt_regs *ctx, struct task_struct *prev)
{
    u32 pid = prev->pid; // 获取前一个进程的PID
    u64 *start_time = process_start.lookup(&pid);
    if (!start_time) {
        return 0; // 进程启动时间未找到
    }

    // 计算进程运行时间
    u64 now = bpf_ktime_get_ns();
    u64 duration = now - *start_time;
    process_start.delete(&pid);

    struct process_key key = {};
    copy_process_name(key.name, prev->comm);

    // 更新CPU使用情况
    struct value_t *value = cpu_usage.lookup(&key);
    if (value) {
        value->duration += duration;
        value->last_timestamp = now;
        cpu_usage.update(&key, value);
    }

    return 0;
}

char _license[] SEC("license") = "GPL";

代码解释:

  • cpu_usage:eBPF hash map,用于存储每个进程的CPU使用情况。Key是process_key结构体,包含进程名;Value是value_t结构体,包含CPU使用时间和上次更新时间戳。
  • process_start:eBPF hash map,用于存储进程启动时间。Key是PID,Value是启动时间戳。
  • kprobe__sched_process_exec:kprobe函数,在sched_process_exec事件发生时被调用。该函数负责记录进程名和启动时间。
  • kprobe__sched_switch:kprobe函数,在sched_switch事件发生时被调用。该函数负责计算进程的CPU使用时间,并更新cpu_usage map。

编译eBPF程序:

可以使用clangllvm工具链来编译eBPF程序:

clang -O2 -target bpf -c ebpf_cpu_usage.c -o ebpf_cpu_usage.o

4. 用户态程序实现

用户态程序负责加载eBPF程序到内核,并定期从map中读取统计结果。以下是一个简化的Python用户态程序示例:

from bcc import BPF
import time

# 加载eBPF程序
b = BPF(src_file="ebpf_cpu_usage.c")

# attach kprobes
b.attach_kprobe(event="sched_process_exec", fn_name="kprobe__sched_process_exec")
b.attach_kprobe(event="sched_switch", fn_name="kprobe__sched_switch")

# 打印表头
print("%-20s %-10s" % ("Process Name", "CPU Usage (ms)"))

# 循环读取数据
while True:
    time.sleep(2)
    cpu_usage = b["cpu_usage"]
    for k, v in cpu_usage.items():
        duration_ms = v.duration / 1000000  # 纳秒转换为毫秒
        print("%-20s %-10.2f" % (k.name.decode('utf-8', 'replace'), duration_ms))
    print("\n")

代码解释:

  • BPF(src_file="ebpf_cpu_usage.c"):加载eBPF程序。
  • b.attach_kprobe(...):将kprobe函数与内核事件关联。
  • b["cpu_usage"]:访问eBPF的cpu_usage map。
  • 循环读取cpu_usage map中的数据,并将结果打印到屏幕上。

运行用户态程序:

需要root权限才能运行用户态程序:

sudo python3 user_program.py

5. 结果分析

用户态程序会定期打印每个进程的CPU使用情况。通过分析这些数据,可以找出最耗CPU的进程。例如,如果发现某个进程的CPU使用率异常高,可能需要进一步调查该进程是否存在性能问题。

6. 优化和改进

  • 减少开销: 可以通过调整eBPF程序的采样频率来减少开销。例如,可以只采样一部分CPU调度事件。
  • 更精确的统计: 可以使用CPU cycle counter来更精确地统计CPU使用情况。
  • 更详细的信息: 可以在eBPF程序中记录更多的进程信息,例如PID、UID等。
  • 动态过滤: 可以在用户态程序中动态地过滤进程,只统计感兴趣的进程。

7. 总结

本文介绍了一种使用eBPF技术来统计进程CPU使用情况并按进程名聚合的实践方案。通过编写eBPF程序和用户态程序,我们可以高效地监控系统中进程的CPU使用情况,并找出最耗CPU的进程。该方案具有高性能、低开销的特点,适用于各种性能分析场景。

通过本文的学习,相信您已经掌握了使用eBPF进行进程CPU使用率统计的基本方法。希望本文能够帮助您更好地理解和应用eBPF技术。

BPF探索者 eBPFCPU使用率进程监控

评论点评