WEBKT

eBPF实战:如何用eBPF揪出CPU占用率飙升的“罪魁祸首”?(附代码示例)

42 0 0 0

1. 为什么选择eBPF?

2. eBPF基础知识回顾

3. 编写eBPF程序:CPU占用率监控

3.1. eBPF代码(C语言)

3.2. 用户态代码(Python)

3.3. 编译和运行

4. 高CPU占用率进程分析

5. 总结与展望

线上服务器CPU占用率突然飙升,报警信息铺天盖地,作为一名身经百战的运维工程师,你是否也曾经历过这样的“至暗时刻”? 面对这种情况,传统的排查手段往往显得笨重而低效,犹如大海捞针。而eBPF,作为近年来备受瞩目的内核观测技术,为我们提供了一种全新的、高效的问题定位思路。

本文将带你深入了解如何利用eBPF编写一个简单的CPU监控程序,并针对高CPU占用率的进程进行分析,最终找到导致CPU瓶颈的根本原因。我们将从eBPF的基本概念入手,逐步深入到实际的代码编写和调试,让你能够快速上手eBPF,解决实际问题。

1. 为什么选择eBPF?

在深入代码之前,我们首先需要了解为什么选择eBPF来解决CPU占用率分析的问题。相比于传统的性能分析工具,eBPF具有以下显著优势:

  • 内核级观测,性能损耗低: eBPF程序运行在内核态,可以直接访问内核数据,无需像传统的用户态工具那样进行大量的上下文切换,因此性能损耗极低,几乎可以忽略不计。这使得我们可以在生产环境中安全地使用eBPF进行性能分析,而不用担心对系统造成额外的负担。
  • 高度可编程性: eBPF提供了一套灵活的指令集和丰富的API,允许我们自定义观测逻辑,根据实际需求收集和分析数据。这意味着我们可以针对特定的问题,编写定制化的eBPF程序,从而获得更精确、更有价值的信息。
  • 安全可靠: eBPF程序在加载到内核之前,会经过严格的验证,确保其不会崩溃或影响系统稳定性。此外,eBPF程序还受到权限控制,只能访问预定义的内核数据,避免了潜在的安全风险。
  • 无需修改内核: eBPF程序可以动态加载到内核中,无需修改内核源码或重启系统。这使得我们可以快速部署和更新eBPF程序,而无需中断服务。

2. eBPF基础知识回顾

在开始编写eBPF程序之前,我们需要对eBPF的一些基本概念进行回顾:

  • eBPF程序类型: eBPF程序可以附加到不同的hook点,例如kprobe、uprobe、tracepoint等,以捕获不同的事件。常见的eBPF程序类型包括:
    • kprobe/kretprobe: 附加到内核函数的入口和出口,可以用来跟踪内核函数的调用和执行时间。
    • uprobe/uretprobe: 附加到用户态函数的入口和出口,可以用来跟踪用户态函数的调用和执行时间。
    • tracepoint: 附加到内核中的静态跟踪点,可以用来捕获特定的内核事件。
    • perf_event: 附加到性能事件,可以用来采样CPU周期、指令数等性能指标。
  • eBPF Map: eBPF Map是一种内核态的数据结构,用于存储eBPF程序收集的数据,并与用户态程序进行交互。常见的eBPF Map类型包括:
    • Hash Map: 基于哈希表的键值对存储。
    • Array Map: 基于数组的键值对存储。
    • LRU Hash Map: 带有最近最少使用(LRU)策略的哈希表。
    • Ring Buffer: 环形缓冲区,用于高效地将数据从内核态传递到用户态。
  • BPF Helper Functions: BPF Helper Functions是由内核提供的一组函数,eBPF程序可以调用这些函数来执行各种操作,例如获取当前时间、访问内核数据、发送事件等。

3. 编写eBPF程序:CPU占用率监控

接下来,我们将编写一个简单的eBPF程序,用于监控CPU占用率。该程序将使用perf_event程序类型,采样CPU周期,并统计每个进程的CPU占用时间。

3.1. eBPF代码(C语言)

// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include <linux/sched.h>
#define MAX_ENTRIES 1024
struct key_t {
pid_t pid;
uid_t uid;
char comm[TASK_COMM_LEN];
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(key_size, sizeof(struct key_t));
__uint(value_size, sizeof(long unsigned int));
__uint(max_entries, MAX_ENTRIES);
} cpu_usage SEC(".maps");
SEC("perf_event")
int count_cpu_cycles(void *ctx) {
struct key_t key = {};
long unsigned int *value, delta;
struct task_struct *task;
task = (struct task_struct *)bpf_get_current_task();
key.pid = bpf_get_current_pid_tgid() >> 32;
key.uid = bpf_core_read(&task->real_cred->uid.val, sizeof(key.uid));
bpf_core_read_str(&key.comm, sizeof(key.comm), task->comm);
value = bpf_map_lookup_elem(&cpu_usage, &key);
if (!value) {
long unsigned int init_value = 0;
bpf_map_update_elem(&cpu_usage, &key, &init_value, BPF_ANY);
value = bpf_map_lookup_elem(&cpu_usage, &key);
if (!value)
return 0; // Handle potential lookup failure
}
delta = bpf_ktime_get_ns();
*value += delta;
return 0;
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";

代码解释:

  • #include:包含必要的头文件,例如linux/bpf.hbpf/bpf_helpers.h等,这些头文件定义了eBPF程序所需的API和数据结构。
  • MAX_ENTRIES:定义了cpu_usage Map的最大条目数,用于限制Map的大小,防止内存耗尽。
  • struct key_t:定义了cpu_usage Map的Key,包括进程ID(pid)、用户ID(uid)和进程名(comm)。
  • cpu_usage:定义了一个BPF_MAP_TYPE_HASH类型的Map,用于存储每个进程的CPU占用时间。Key是struct key_t,Value是long unsigned int类型的CPU占用时间(纳秒)。
  • count_cpu_cycles:这是一个perf_event类型的eBPF程序,当CPU发生周期事件时,该程序会被调用。该程序首先获取当前进程的PID、UID和进程名,然后查找cpu_usage Map中是否存在该进程的记录。如果不存在,则创建一个新的记录,并将CPU占用时间初始化为0。最后,获取当前时间,并将其累加到该进程的CPU占用时间中。
  • LICENSE:定义了程序的License,必须声明为Dual BSD/GPL,否则程序无法加载到内核。

3.2. 用户态代码(Python)

#!/usr/bin/env python3
from bcc import BPF
import time
import argparse
# 定义命令行参数
parser = argparse.ArgumentParser(description="Monitor CPU usage by process.")
parser.add_argument("-i", "--interval", type=int, default=1, help="Sampling interval in seconds.")
parser.add_argument("-d", "--duration", type=int, default=60, help="Duration of monitoring in seconds.")
parser.add_argument("-n", "--top", type=int, default=10, help="Number of top processes to display.")
args = parser.parse_args()
# 加载eBPF程序
program = BPF(src_file="cpu_monitor.c") # 替换为你的C代码文件名
# 附加perf_event程序到CPU周期事件
program.attach_perf_event(ev_type=BPF.PERF_TYPE_SOFTWARE,
ev_config=BPF.PERF_COUNT_SW_CPU_CLOCK,
fn_name="count_cpu_cycles",
sample_period=99999) # 调整采样频率
# 获取cpu_usage Map
cpu_usage = program["cpu_usage"]
# 循环采样并打印结果
start_time = time.time()
while time.time() - start_time < args.duration:
time.sleep(args.interval)
print("\n%-6s %-16s %-10s" % ("PID", "COMM", "CPU(ms)"))
# 排序并打印CPU占用率最高的进程
for k, v in sorted(cpu_usage.items(), key=lambda cpu_usage: cpu_usage[1].value, reverse=True)[:args.top]:
cpu_time_ms = v.value / 1000000 # 纳秒转换为毫秒
print("%-6d %-16s %-10.2f" % (k.pid, k.comm.decode(), cpu_time_ms))
cpu_usage.clear()
print("\nMonitoring finished.")

代码解释:

  • from bcc import BPF:导入bcc库,用于与eBPF程序进行交互。
  • program = BPF(src_file="cpu_monitor.c"):加载eBPF程序,cpu_monitor.c是包含eBPF代码的C语言文件。
  • program.attach_perf_event(...):将count_cpu_cycles函数附加到CPU周期事件。ev_type指定事件类型,ev_config指定事件配置,fn_name指定要附加的函数名,sample_period指定采样周期。
  • cpu_usage = program["cpu_usage"]:获取eBPF程序中定义的cpu_usage Map。
  • 循环采样并打印结果:程序每隔args.interval秒采样一次cpu_usage Map,并按照CPU占用时间对进程进行排序,然后打印CPU占用率最高的args.top个进程的PID、进程名和CPU占用时间。
  • cpu_usage.clear(): 每次打印完结果后,清空cpu_usage Map,以便进行下一次采样。

3.3. 编译和运行

  1. 安装bcc: 首先需要安装bcc工具包,它是eBPF开发的重要依赖。具体的安装方法可以参考bcc官方文档:https://github.com/iovisor/bcc

  2. 编译eBPF代码: 将上述C语言代码保存为cpu_monitor.c文件,然后使用clang编译器将其编译成eBPF字节码:

    clang -O2 -target bpf -c cpu_monitor.c -o cpu_monitor.o
    
  3. 运行用户态程序: 将上述Python代码保存为cpu_monitor.py文件,确保cpu_monitor.pycpu_monitor.o文件在同一目录下,然后运行该程序:

    sudo python3 cpu_monitor.py -i 1 -d 60 -n 10
    

    该命令将以root权限运行cpu_monitor.py程序,采样间隔为1秒,持续时间为60秒,并显示CPU占用率最高的10个进程。

4. 高CPU占用率进程分析

通过上述eBPF程序,我们可以快速定位到CPU占用率较高的进程。接下来,我们需要进一步分析这些进程,找出导致CPU瓶颈的根本原因。常用的分析方法包括:

  • perf: perf是一个强大的性能分析工具,可以用来分析CPU周期、指令数、缓存命中率等性能指标。可以使用perf record命令记录进程的性能数据,然后使用perf report命令生成性能报告。
  • 火焰图: 火焰图是一种可视化性能分析工具,可以直观地展示程序的CPU调用栈。可以使用perf record命令记录进程的性能数据,然后使用火焰图工具生成火焰图。
  • strace: strace是一个系统调用跟踪工具,可以用来跟踪进程的系统调用。可以使用strace命令跟踪进程的系统调用,从而了解进程的I/O行为、网络行为等。
  • gdb: gdb是一个调试工具,可以用来调试进程。可以使用gdb命令调试进程,从而了解进程的内部状态和执行流程。

案例分析:

假设我们通过上述eBPF程序发现一个名为nginx的进程CPU占用率很高。我们可以使用perf工具来分析nginx进程的性能瓶颈:

sudo perf record -p <nginx_pid> -g -o perf.data
sudo perf report -i perf.data

通过perf report命令生成的性能报告,我们可以看到nginx进程中哪个函数占用的CPU时间最多。例如,我们发现ngx_http_process_request函数占用了大量的CPU时间,这表明nginx进程正在处理大量的HTTP请求。此时,我们可以考虑优化nginx的配置,例如增加worker进程的数量、调整缓存大小等,以提高nginx的处理能力。

5. 总结与展望

本文介绍了如何使用eBPF编写一个简单的CPU监控程序,并针对高CPU占用率的进程进行分析,最终找到导致CPU瓶颈的根本原因。eBPF作为一种强大的内核观测技术,为我们提供了一种全新的性能分析思路。随着eBPF技术的不断发展,相信它将在未来的性能优化领域发挥越来越重要的作用。

希望本文能够帮助你快速上手eBPF,解决实际问题。当然,eBPF的学习之路还很长,需要不断地实践和探索。祝你在eBPF的世界里取得更大的成就!

一些额外的思考:

  • 动态调整采样频率: 可以根据CPU占用率的变化动态调整采样频率,以提高程序的精度和效率。
  • 更丰富的监控指标: 除了CPU占用率,还可以监控内存占用、磁盘I/O、网络流量等指标。
  • 集成到监控系统: 可以将eBPF程序集成到现有的监控系统中,实现自动化的性能分析和报警。

通过不断地学习和实践,我们可以将eBPF应用到更多的场景中,解决更多复杂的问题。

性能调优师兄 eBPFCPU监控性能分析

评论点评

打赏赞助
sponsor

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

分享

QRcode

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