WEBKT

系统管理员如何使用 eBPF 追踪特定进程的 CPU 使用和内存分配?

72 0 0 0

为什么选择 eBPF?

eBPF 的工作原理

实战:追踪特定进程的 CPU 使用情况

实战:追踪特定进程的内存分配情况

总结

扩展思考

作为一名系统管理员,服务器性能监控绝对是日常工作的重中之重。面对日益复杂的应用环境,传统的监控工具往往显得力不从心,难以深入到内核层面进行细粒度的分析。这时,eBPF (extended Berkeley Packet Filter) 就如同一个瑞士军刀,能帮助我们洞察系统内部的运作机制,从而更好地优化性能、排查故障。今天,我就来分享一下如何利用 eBPF 追踪特定进程的 CPU 使用情况和内存分配,希望能给你带来一些启发。

为什么选择 eBPF?

在深入探讨具体实现之前,我们先来简单聊聊为什么选择 eBPF。传统的性能监控工具,例如 topvmstat 等,虽然能提供一些基本的系统信息,但它们往往存在以下局限性:

  • 开销大:传统工具通常需要频繁地进行上下文切换,这会带来额外的 CPU 消耗,尤其是在高负载环境下,对系统性能的影响不容忽视。
  • 信息有限:它们只能提供一些宏观的统计数据,难以深入到内核层面,了解特定进程的详细行为。
  • 可定制性差:它们的功能相对固定,难以根据实际需求进行定制。

eBPF 的出现,彻底改变了这种局面。它具有以下显著优势:

  • 高性能:eBPF 程序运行在内核态,避免了频繁的上下文切换,大大降低了开销。
  • 细粒度:eBPF 可以挂载到内核的各种事件探针上,例如函数调用、系统调用等,从而实现对系统行为的精细化监控。
  • 可编程性:eBPF 允许我们编写自定义的监控逻辑,根据实际需求进行灵活定制。
  • 安全性:eBPF 程序在运行前会经过内核的验证器 (verifier) 检查,确保其安全性,避免对系统造成危害。

eBPF 的工作原理

要理解 eBPF 的强大之处,我们还需要简单了解一下它的工作原理。可以将 eBPF 想象成一个运行在内核态的虚拟机,它允许我们编写一些小程序 (eBPF 程序),并将这些程序挂载到内核的各种事件探针上。当事件发生时,eBPF 程序会被触发执行,收集相关数据,并将数据存储到用户态可以访问的映射 (map) 中。用户态程序可以通过读取这些映射,获取监控数据。

实战:追踪特定进程的 CPU 使用情况

接下来,我们就通过一个实际的例子,来演示如何使用 eBPF 追踪特定进程的 CPU 使用情况。这里我们使用 bcc (BPF Compiler Collection) 工具包,它提供了一系列 Python 封装,使得编写 eBPF 程序更加方便。

  1. 安装 bcc

    首先,我们需要安装 bcc 工具包。具体的安装步骤可以参考 bcc 的官方文档:https://github.com/iovisor/bcc

  2. 编写 eBPF 程序

    创建一个名为 cpu_usage.py 的 Python 文件,并添加以下代码:

    #!/usr/bin/env python
    from bcc import BPF
    import argparse
    import time
    # 定义命令行参数
    parser = argparse.ArgumentParser(description="追踪特定进程的 CPU 使用情况")
    parser.add_argument("-p", "--pid", type=int, help="要追踪的进程 PID")
    args = parser.parse_args()
    if not args.pid:
    print("请指定要追踪的进程 PID,例如:./cpu_usage.py -p 1234")
    exit()
    # 定义 eBPF 程序
    program = '''
    #include <uapi/linux/ptrace.h>
    #include <linux/sched.h>
    // 定义一个 map,用于存储进程的 CPU 使用时间
    BPF_HASH(start, u32, u64);
    // kprobe 函数,在进程执行时记录开始时间
    int kprobe__sched_process_exec(struct pt_regs *ctx, struct task_struct *p)
    {
    u32 pid = p->pid;
    // 只追踪指定 PID 的进程
    if (pid != PID) {
    return 0;
    }
    u64 ts = bpf_ktime_get_ns();
    start.update(&pid, &ts);
    return 0;
    }
    // kprobe 函数,在进程切换时计算 CPU 使用时间
    int kprobe__finish_task_switch(struct pt_regs *ctx, struct task_struct *prev)
    {
    u32 pid = prev->pid;
    // 只追踪指定 PID 的进程
    if (pid != PID) {
    return 0;
    }
    u64 *tsp = start.lookup(&pid);
    if (tsp == 0) {
    return 0;
    }
    u64 delta = bpf_ktime_get_ns() - *tsp;
    // 输出 CPU 使用时间 (纳秒)
    bpf_trace_printk("PID %d: CPU 使用时间 = %llu ns\n", pid, delta);
    start.delete(&pid);
    return 0;
    }
    '''
    # 替换 PID
    program = program.replace("PID", str(args.pid))
    # 加载 eBPF 程序
    b = BPF(text=program)
    # 打印输出信息
    print("正在追踪 PID 为 %d 的进程... 按 Ctrl+C 停止" % args.pid)
    # 循环读取 eBPF 程序的输出
    try:
    while True:
    time.sleep(0.1)
    except KeyboardInterrupt:
    pass

    这段代码主要做了以下几件事:

    • 定义了一个命令行参数 -p,用于指定要追踪的进程 PID。
    • 定义了一个 eBPF 程序,该程序包含两个 kprobe 函数:kprobe__sched_process_execkprobe__finish_task_switch
      • kprobe__sched_process_exec 函数在进程执行时被调用,用于记录进程的开始时间。
      • kprobe__finish_task_switch 函数在进程切换时被调用,用于计算进程的 CPU 使用时间。
    • 使用 BPF_HASH 定义了一个 map,用于存储进程的开始时间。
    • 使用 bpf_trace_printk 函数将 CPU 使用时间输出到 trace buffer 中。
    • 使用 bcc.BPF 加载 eBPF 程序。
    • 循环读取 eBPF 程序的输出,并打印到屏幕上。
  3. 运行 eBPF 程序

    保存 cpu_usage.py 文件,并使用以下命令运行:

    sudo ./cpu_usage.py -p <进程 PID>
    

    <进程 PID> 替换为你要追踪的进程的实际 PID。例如,如果要追踪 PID 为 1234 的进程,则运行以下命令:

    sudo ./cpu_usage.py -p 1234
    

    运行后,你将会看到类似以下的输出:

    正在追踪 PID 1234 的进程... Ctrl+C 停止
    PID 1234: CPU 使用时间 = 1234567 ns
    PID 1234: CPU 使用时间 = 2345678 ns
    PID 1234: CPU 使用时间 = 3456789 ns
    ...

    这表示 PID 为 1234 的进程在不断地使用 CPU,并且每次 CPU 使用的时间分别为 1234567 纳秒、2345678 纳秒、3456789 纳秒等。

  4. 停止 eBPF 程序

    按下 Ctrl+C 即可停止 eBPF 程序的运行。

实战:追踪特定进程的内存分配情况

接下来,我们再通过一个例子,来演示如何使用 eBPF 追踪特定进程的内存分配情况。同样,我们使用 bcc 工具包。

  1. 编写 eBPF 程序

    创建一个名为 memory_alloc.py 的 Python 文件,并添加以下代码:

    #!/usr/bin/env python
    from bcc import BPF
    import argparse
    import time
    # 定义命令行参数
    parser = argparse.ArgumentParser(description="追踪特定进程的内存分配情况")
    parser.add_argument("-p", "--pid", type=int, help="要追踪的进程 PID")
    args = parser.parse_args()
    if not args.pid:
    print("请指定要追踪的进程 PID,例如:./memory_alloc.py -p 1234")
    exit()
    # 定义 eBPF 程序
    program = '''
    #include <uapi/linux/ptrace.h>
    #include <linux/sched.h>
    // 定义一个 map,用于存储进程的内存分配大小
    BPF_HISTOGRAM(alloc_size);
    // kprobe 函数,在 malloc 函数被调用时记录内存分配大小
    int kprobe__malloc(struct pt_regs *ctx, size_t size)
    {
    u32 pid = bpf_get_current_pid_tgid();
    // 只追踪指定 PID 的进程
    if (pid != PID) {
    return 0;
    }
    alloc_size.increment(bpf_log2l(size));
    return 0;
    }
    // kprobe 函数,在 free 函数被调用时不做任何操作
    int kprobe__free(struct pt_regs *ctx, void *addr)
    {
    return 0;
    }
    '''
    # 替换 PID
    program = program.replace("PID", str(args.pid))
    # 加载 eBPF 程序
    b = BPF(text=program)
    # 打印输出信息
    print("正在追踪 PID 为 %d 的进程... 按 Ctrl+C 停止" % args.pid)
    # 循环读取 eBPF 程序的输出
    try:
    while True:
    time.sleep(1)
    print("\n----------------------------------------")
    alloc_size = b["alloc_size"]
    alloc_size.print_log2_hist("内存分配大小 (bytes)")
    alloc_size.clear()
    except KeyboardInterrupt:
    pass

    这段代码主要做了以下几件事:

    • 定义了一个命令行参数 -p,用于指定要追踪的进程 PID。
    • 定义了一个 eBPF 程序,该程序包含两个 kprobe 函数:kprobe__mallockprobe__free
      • kprobe__malloc 函数在 malloc 函数被调用时被触发,用于记录内存分配的大小。
      • kprobe__free 函数在 free 函数被调用时被触发,这里我们不做任何操作。
    • 使用 BPF_HISTOGRAM 定义了一个 histogram,用于统计内存分配大小的分布情况。
    • 使用 bpf_get_current_pid_tgid 函数获取当前进程的 PID。
    • 使用 bpf_log2l 函数计算内存分配大小的以 2 为底的对数,用于 histogram 的统计。
    • 使用 bcc.BPF 加载 eBPF 程序。
    • 循环读取 histogram 的数据,并打印到屏幕上。
  2. 运行 eBPF 程序

    保存 memory_alloc.py 文件,并使用以下命令运行:

    sudo ./memory_alloc.py -p <进程 PID>
    

    <进程 PID> 替换为你要追踪的进程的实际 PID。例如,如果要追踪 PID 为 1234 的进程,则运行以下命令:

    sudo ./memory_alloc.py -p 1234
    

    运行后,你将会看到类似以下的输出:

    正在追踪 PID 为 1234 的进程... 按 Ctrl+C 停止
    ----------------------------------------
    内存分配大小 (bytes):
    value ------------- count -------------
    0 | 0
    1 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 100
    2 |@@@@@@@@@@@@@@@@@@@@@@ 50
    4 |@@@@@@@@@@ 25
    8 |@@@@ 12
    16 |@ 3
    32 | 0
    64 |@ 1

    这表示 PID 为 1234 的进程在不断地进行内存分配,并且内存分配大小的分布情况如上所示。例如,分配大小为 1 字节的次数为 100 次,分配大小为 2 字节的次数为 50 次,等等。

  3. 停止 eBPF 程序

    按下 Ctrl+C 即可停止 eBPF 程序的运行。

总结

通过以上两个例子,我们学习了如何使用 eBPF 追踪特定进程的 CPU 使用情况和内存分配情况。eBPF 的强大之处在于它的灵活性和可定制性,我们可以根据实际需求编写自定义的 eBPF 程序,从而实现对系统行为的精细化监控。希望这些内容能帮助你更好地理解和使用 eBPF,从而更好地管理和优化你的服务器。

扩展思考

  • 更复杂的监控指标:除了 CPU 使用情况和内存分配情况,我们还可以使用 eBPF 追踪其他更复杂的监控指标,例如网络 I/O、磁盘 I/O、系统调用延迟等。
  • 实时告警:我们可以将 eBPF 采集到的数据与预设的阈值进行比较,当超过阈值时,触发实时告警。
  • 数据可视化:我们可以将 eBPF 采集到的数据进行可视化,例如使用 Grafana 等工具,从而更直观地了解系统的运行状态。

希望这些扩展思考能给你带来更多的灵感,让你更好地利用 eBPF 解决实际问题。

Linux运维老司机 eBPF性能监控系统管理

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/9410