WEBKT

性能优化利器:用 eBPF 追踪系统调用,揪出性能瓶颈!

35 0 0 0

性能优化利器:用 eBPF 追踪系统调用,揪出性能瓶颈!

作为一名追求极致的程序员,你是否经常遇到这样的困扰?线上服务 CPU 占用率居高不下,却苦于无法定位到具体是哪个函数、哪行代码导致的性能问题。传统的性能分析工具,要么侵入性太强,影响线上服务稳定性;要么信息不够详细,难以找到根本原因。别担心,今天就为你介绍一款强大的性能优化利器——eBPF

什么是 eBPF?

eBPF (extended Berkeley Packet Filter) 是一种革命性的内核技术,它允许你在内核中安全地运行自定义代码,而无需修改内核源码或加载内核模块。可以把它想象成一个内核“沙箱”,你可以在里面运行一些小程序,用来监控、分析甚至修改内核的行为。eBPF 最初的设计目的是为了网络数据包过滤,但现在已经被广泛应用于性能分析、安全监控等领域。

为什么选择 eBPF 进行系统调用追踪?

  • 高性能: eBPF 程序在内核中运行,避免了用户态和内核态之间频繁切换的开销,性能损耗极低。
  • 安全性: eBPF 程序会经过内核的验证器 (verifier) 的严格检查,确保其不会导致内核崩溃或安全问题。
  • 灵活性: 你可以根据自己的需求,编写自定义的 eBPF 程序,来追踪各种系统调用,收集各种性能指标。
  • 无需修改内核: eBPF 程序可以直接加载到内核中运行,无需修改内核源码或重新编译内核。

如何使用 eBPF 追踪系统调用?

接下来,我们就以追踪 openreadwrite 这三个常用的系统调用为例,来演示如何使用 eBPF 进行系统调用追踪。

1. 环境准备

首先,你需要一个支持 eBPF 的 Linux 系统。建议使用较新的 Linux 发行版,例如 Ubuntu 18.04+、CentOS 8+ 等。同时,你需要安装一些必要的工具,例如 bcc (BPF Compiler Collection)。bcc 提供了一系列的工具和库,可以帮助你更方便地编写和运行 eBPF 程序。

在 Ubuntu 上,你可以使用以下命令安装 bcc

sudo apt-get update
sudo apt-get install bpfcc-tools linux-headers-$(uname -r)

2. 编写 eBPF 程序

使用 bcc 提供的 Python 接口,可以很方便地编写 eBPF 程序。下面是一个简单的例子,用于追踪 open 系统调用的参数和返回值:

#!/usr/bin/env python
from bcc import BPF
# 定义 eBPF 程序
program = '''
#include <uapi/linux/ptrace.h>
struct data_t {
u32 pid;
u64 ts;
char comm[64];
char filename[128];
int retval;
};
BPF_PERF_OUTPUT(events);
int kprobe__sys_enter_open(struct pt_regs *ctx, const char *filename, int flags, umode_t mode) {
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
bpf_get_current_comm(&data.comm, sizeof(data.comm));
bpf_probe_read_user_str(&data.filename, sizeof(data.filename), (void *)filename);
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
int kretprobe__sys_exit_open(struct pt_regs *ctx) {
struct data_t data = {};
int retval = PT_REGS_RC(ctx);
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
bpf_get_current_comm(&data.comm, sizeof(data.comm));
data.retval = retval;
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
'''
# 创建 BPF 实例
bpf = BPF(text=program)
# 定义回调函数,用于处理 eBPF 程序输出的事件
def print_event(cpu, data, size):
event = bpf["events"].event(data)
print(f"{event.pid} {event.comm.decode()} {event.filename.decode()} {event.retval}")
# 绑定回调函数
bpf["events"].open_perf_buffer(print_event)
# 循环读取事件
while True:
try:
bpf.perf_buffer_poll()
except KeyboardInterrupt:
exit()

代码解释:

  • #include <uapi/linux/ptrace.h>: 包含必要的头文件,用于访问内核数据结构。
  • struct data_t: 定义一个数据结构,用于存储我们想要追踪的信息,例如进程 ID、时间戳、进程名、文件名和返回值。
  • BPF_PERF_OUTPUT(events): 定义一个 perf 输出,用于将数据从 eBPF 程序发送到用户态。
  • kprobe__sys_enter_open: 定义一个 kprobe,它会在 sys_enter_open 函数 (即 open 系统调用的入口) 被调用时触发。struct pt_regs *ctx 参数包含了函数的寄存器信息,我们可以从中获取函数的参数。
  • bpf_get_current_pid_tgid(): 获取当前进程的 PID。
  • bpf_ktime_get_ns(): 获取当前时间的纳秒值。
  • bpf_get_current_comm(): 获取当前进程的进程名。
  • bpf_probe_read_user_str(): 从用户态内存中读取字符串,这里我们读取的是 open 系统调用的 filename 参数。
  • events.perf_submit(ctx, &data, sizeof(data)): 将数据发送到用户态。
  • kretprobe__sys_exit_open: 定义一个 kretprobe,它会在 sys_exit_open 函数 (即 open 系统调用的出口) 返回时触发。PT_REGS_RC(ctx) 可以获取函数的返回值。
  • BPF(text=program): 创建一个 BPF 实例,并将 eBPF 程序加载到内核中。
  • print_event: 定义一个回调函数,用于处理 eBPF 程序输出的事件。这个函数会将事件中的数据打印到控制台。
  • bpf["events"].open_perf_buffer(print_event): 将回调函数绑定到 perf 输出。
  • bpf.perf_buffer_poll(): 循环读取事件。

3. 运行 eBPF 程序

将上面的代码保存为 open_tracer.py,然后运行它:

sudo python open_tracer.py

运行后,你会看到程序开始不断地打印 open 系统调用的相关信息。例如:

1234 bash /etc/passwd 3
5678 cat /tmp/test.txt 4

这表示 PID 为 1234 的 bash 进程打开了 /etc/passwd 文件,返回值是 3。PID 为 5678 的 cat 进程打开了 /tmp/test.txt 文件,返回值是 4。

4. 扩展到其他系统调用

有了 open 系统调用的例子,你可以很容易地将其扩展到其他系统调用,例如 readwrite。只需要修改 eBPF 程序中的 kprobekretprobe 的名称,以及读取参数的方式即可。

下面是追踪 read 系统调用的例子:

#!/usr/bin/env python
from bcc import BPF
# 定义 eBPF 程序
program = '''
#include <uapi/linux/ptrace.h>
struct data_t {
u32 pid;
u64 ts;
char comm[64];
int fd;
size_t count;
int retval;
};
BPF_PERF_OUTPUT(events);
int kprobe__sys_enter_read(struct pt_regs *ctx, int fd, void *buf, size_t count) {
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
bpf_get_current_comm(&data.comm, sizeof(data.comm));
data.fd = fd;
data.count = count;
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
int kretprobe__sys_exit_read(struct pt_regs *ctx) {
struct data_t data = {};
int retval = PT_REGS_RC(ctx);
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
bpf_get_current_comm(&data.comm, sizeof(data.comm));
data.retval = retval;
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
'''
# 创建 BPF 实例
bpf = BPF(text=program)
# 定义回调函数,用于处理 eBPF 程序输出的事件
def print_event(cpu, data, size):
event = bpf["events"].event(data)
print(f"{event.pid} {event.comm.decode()} {event.fd} {event.count} {event.retval}")
# 绑定回调函数
bpf["events"].open_perf_buffer(print_event)
# 循环读取事件
while True:
try:
bpf.perf_buffer_poll()
except KeyboardInterrupt:
exit()

5. 分析系统调用的性能瓶颈

通过追踪系统调用,我们可以收集到大量的性能数据。例如,我们可以统计每个系统调用的耗时、调用次数、参数和返回值等。然后,我们可以使用这些数据来分析系统调用的性能瓶颈。

一些常用的分析方法:

  • 火焰图 (Flame Graph): 火焰图是一种可视化的性能分析工具,它可以将函数调用栈以火焰的形式展示出来。通过观察火焰图,我们可以很容易地找到 CPU 占用率最高的函数。
  • 延迟统计 (Latency Histogram): 延迟统计可以帮助我们了解系统调用的延迟分布情况。例如,我们可以统计 read 系统调用的延迟分布,从而判断是否存在 I/O 瓶颈。
  • 调用次数统计 (Call Count): 调用次数统计可以帮助我们了解每个系统调用的调用频率。例如,我们可以统计 malloc 系统调用的调用次数,从而判断是否存在内存分配瓶颈。

实际案例:使用 eBPF 优化 Redis 性能

假设我们发现 Redis 服务的 CPU 占用率很高,但是我们无法定位到具体是哪个操作导致的。这时,我们可以使用 eBPF 来追踪 Redis 的系统调用,例如 readwrite。通过分析 readwrite 的参数和返回值,我们可以了解到 Redis 在哪些文件上进行了大量的 I/O 操作。如果发现 Redis 在某个日志文件上进行了大量的 write 操作,那么我们可以考虑优化日志的写入方式,例如使用异步写入或减少日志的输出量。

总结

eBPF 是一种强大的性能优化利器,它可以帮助我们追踪系统调用,收集各种性能指标,从而分析系统调用的性能瓶颈。掌握 eBPF 技术,可以让你在性能优化的道路上更进一步,成为一名真正的性能优化大师!

进阶学习:

  • BCC (BPF Compiler Collection): bcc 提供了大量的 eBPF 工具和示例,可以帮助你更方便地学习和使用 eBPF。
  • bpftrace: bpftrace 是一种高级的 eBPF 追踪语言,它提供了一种更简洁、更易用的方式来编写 eBPF 程序。
  • Linux Observability with BPF: 这本书详细介绍了 eBPF 的原理和应用,是学习 eBPF 的经典之作。

希望这篇文章能够帮助你了解 eBPF 的基本原理和使用方法。如果你在学习过程中遇到任何问题,欢迎留言交流!

性能调优侠 eBPF系统调用性能优化

评论点评

打赏赞助
sponsor

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

分享

QRcode

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