WEBKT

还在用老方法排查性能瓶颈?试试 eBPF 内核级性能分析,快到飞起!

34 0 0 0

还在用老方法排查性能瓶颈?试试 eBPF 内核级性能分析,快到飞起!

什么是 eBPF?

eBPF 如何实现内核级性能分析?

1. CPU 使用率分析
2. 内存分配分析
3. IO 等待时间分析

eBPF 的进阶应用

总结

还在用老方法排查性能瓶颈?试试 eBPF 内核级性能分析,快到飞起!

作为一名资深运维工程师,我深知性能问题是日常工作中挥之不去的阴影。CPU 占用率飙升、内存疯狂分配、IO 等待时间过长… 每一个问题都可能让线上服务岌岌可危。传统的排查方法,例如 topvmstatiostat 等工具,虽然也能提供一些信息,但往往只能看到表面现象,无法深入到内核层面去挖掘根本原因。更让人头疼的是,这些工具本身也会消耗系统资源,在生产环境中使用时需要格外小心。

有没有一种方法,既能深入内核,又能低开销地进行性能分析呢?答案是肯定的,那就是 eBPF (extended Berkeley Packet Filter)。

什么是 eBPF?

简单来说,eBPF 是一种可以在内核中安全高效地运行用户代码的技术。你可以把它想象成一个内核“沙箱”,允许你编写一些小程序(eBPF 程序),在特定的内核事件发生时被触发执行。这些小程序可以访问内核数据,收集性能指标,甚至可以修改内核行为。

与传统的内核模块相比,eBPF 具有以下优势:

  • 安全性: eBPF 程序在运行前会经过严格的验证,确保不会导致系统崩溃或安全漏洞。
  • 高效性: eBPF 程序运行在内核中,可以直接访问内核数据,避免了用户态和内核态之间频繁的切换。
  • 灵活性: 你可以根据自己的需求,编写各种各样的 eBPF 程序,实现各种各样的性能分析功能。

eBPF 如何实现内核级性能分析?

eBPF 的强大之处在于它能够 hook 到内核中的各种事件,例如函数调用、系统调用、网络事件等等。通过在这些事件发生时执行 eBPF 程序,我们可以收集到非常详细的性能数据。

下面,我们以几个常见的性能分析场景为例,来具体看看 eBPF 是如何发挥作用的。

1. CPU 使用率分析

高 CPU 使用率是性能问题最常见的表征之一。要分析 CPU 使用率,我们可以使用 eBPF 来跟踪进程的调度行为。具体来说,我们可以 hook 到 sched:sched_process_execsched:sched_process_exit 这两个 tracepoint,分别记录进程的启动和退出时间。然后,我们可以统计每个进程在 CPU 上运行的时间,从而计算出 CPU 使用率。

例如,我们可以使用 bcc (BPF Compiler Collection) 这个工具包来编写 eBPF 程序。bcc 提供了一系列的 Python 脚本,可以简化 eBPF 程序的开发和部署。

下面是一个使用 bcc 编写的 CPU 使用率分析脚本:

from bcc import BPF
# 定义 eBPF 程序
program = '''
#include <uapi/linux/ptrace.h>
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
};
BPF_PERCPU_ARRAY(start, struct data_t, 1);
BPF_HASH(output, u32, u64);
tracepoint(sched, sched_process_exec) {
struct data_t data = {};
data.pid = pid;
data.ts = bpf_ktime_get_ns();
bpf_get_current_comm(&data.comm, sizeof(data.comm));
start.update(&pid, &data);
}
tracepoint(sched, sched_process_exit) {
struct data_t *data = start.lookup(&pid);
if (data) {
u64 delta = bpf_ktime_get_ns() - data->ts;
output.increment(data->pid, delta);
start.delete(&pid);
}
}
'''
# 加载 eBPF 程序
bpf = BPF(text=program)
# 打印头部信息
print("Tracing... Ctrl-C to end.")
# 循环打印 CPU 使用率
while True:
try:
sleep(1)
print("\n%-6s %-16s %s" % ("PID", "COMM", "CPU (ms)"))
for k, v in bpf["output"].items():
pid = k.value
delta = v.value
comm = bpf.get_syscall_fnname(pid).decode('utf-8')
print("%-6d %-16s %.2f" % (pid, comm, delta / 1000000.0))
bpf["output"].clear()
except KeyboardInterrupt:
exit()

这个脚本会 hook 到 sched:sched_process_execsched:sched_process_exit 这两个 tracepoint,记录进程的启动和退出时间,并计算每个进程的 CPU 运行时间。最后,脚本会循环打印每个进程的 PID、进程名和 CPU 使用时间(单位:毫秒)。

2. 内存分配分析

内存泄漏和过度内存分配是导致性能问题的另一个常见原因。要分析内存分配情况,我们可以使用 eBPF 来跟踪 kmallockfree 这两个内核函数。通过记录每次内存分配和释放的大小,我们可以了解内存的使用情况。

下面是一个使用 bcc 编写的内存分配分析脚本:

from bcc import BPF
# 定义 eBPF 程序
program = '''
#include <uapi/linux/ptrace.h>
struct data_t {
u32 pid;
u64 size;
char comm[TASK_COMM_LEN];
};
BPF_PERCPU_ARRAY(allocs, struct data_t, 1);
BPF_HASH(output, u32, u64);
kprobe(kmalloc) {
struct data_t data = {};
data.pid = pid;
data.size = arg0;
bpf_get_current_comm(&data.comm, sizeof(data.comm));
allocs.update(&pid, &data);
}
kprobe(kfree) {
struct data_t *data = allocs.lookup(&pid);
if (data) {
output.increment(data->pid, data->size);
allocs.delete(&pid);
}
}
'''
# 加载 eBPF 程序
bpf = BPF(text=program)
# 打印头部信息
print("Tracing... Ctrl-C to end.")
# 循环打印内存分配情况
while True:
try:
sleep(1)
print("\n%-6s %-16s %s" % ("PID", "COMM", "ALLOC (KB)"))
for k, v in bpf["output"].items():
pid = k.value
size = v.value
comm = bpf.get_syscall_fnname(pid).decode('utf-8')
print("%-6d %-16s %.2f" % (pid, comm, size / 1024.0))
bpf["output"].clear()
except KeyboardInterrupt:
exit()

这个脚本会 hook 到 kmallockfree 这两个内核函数,记录内存分配和释放的大小,并统计每个进程的内存分配总量。最后,脚本会循环打印每个进程的 PID、进程名和内存分配总量(单位:KB)。

3. IO 等待时间分析

IO 等待时间过长是导致性能问题的另一个常见原因,尤其是在磁盘 I/O 密集型的应用中。要分析 IO 等待时间,我们可以使用 eBPF 来跟踪块设备 I/O 操作。具体来说,我们可以 hook 到 block:block_rq_issueblock:block_rq_complete 这两个 tracepoint,分别记录 I/O 请求的发出和完成时间。然后,我们可以计算每个 I/O 请求的等待时间。

下面是一个使用 bcc 编写的 IO 等待时间分析脚本:

from bcc import BPF
# 定义 eBPF 程序
program = '''
#include <uapi/linux/ptrace.h>
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
};
BPF_PERCPU_ARRAY(start, struct data_t, 1);
BPF_HASH(output, u32, u64);
tracepoint(block, block_rq_issue) {
struct data_t data = {};
data.pid = pid;
data.ts = bpf_ktime_get_ns();
bpf_get_current_comm(&data.comm, sizeof(data.comm));
start.update(&pid, &data);
}
tracepoint(block, block_rq_complete) {
struct data_t *data = start.lookup(&pid);
if (data) {
u64 delta = bpf_ktime_get_ns() - data->ts;
output.increment(data->pid, delta);
start.delete(&pid);
}
}
'''
# 加载 eBPF 程序
bpf = BPF(text=program)
# 打印头部信息
print("Tracing... Ctrl-C to end.")
# 循环打印 IO 等待时间
while True:
try:
sleep(1)
print("\n%-6s %-16s %s" % ("PID", "COMM", "IO Latency (ms)"))
for k, v in bpf["output"].items():
pid = k.value
delta = v.value
comm = bpf.get_syscall_fnname(pid).decode('utf-8')
print("%-6d %-16s %.2f" % (pid, comm, delta / 1000000.0))
bpf["output"].clear()
except KeyboardInterrupt:
exit()

这个脚本会 hook 到 block:block_rq_issueblock:block_rq_complete 这两个 tracepoint,记录 I/O 请求的发出和完成时间,并计算每个进程的 IO 等待时间。最后,脚本会循环打印每个进程的 PID、进程名和 IO 等待时间(单位:毫秒)。

eBPF 的进阶应用

除了上述的性能分析场景,eBPF 还可以用于很多其他的用途,例如:

  • 网络监控: 捕获和分析网络数据包,实现流量监控、入侵检测等功能。
  • 安全审计: 跟踪系统调用,检测恶意行为。
  • 自定义内核功能: 在不修改内核代码的情况下,添加新的内核功能。

随着 eBPF 技术的不断发展,越来越多的工具和框架涌现出来,例如 Cilium、Falco 等等。这些工具和框架都基于 eBPF 技术,提供了强大的功能和易用的接口,使得 eBPF 技术的应用更加广泛。

总结

eBPF 是一项强大的内核技术,可以用于实现各种各样的性能分析和监控功能。相比传统的性能分析工具,eBPF 具有更低的开销和更高的灵活性。如果你是一名运维工程师或系统管理员,想要深入了解系统的性能瓶颈,那么 eBPF 绝对值得你学习和掌握。它就像一把瑞士军刀,能够帮助你快速定位和解决各种性能问题,让你的系统运行得更加稳定和高效。

当然,学习 eBPF 也需要一定的门槛,你需要了解一些内核相关的知识,并掌握 eBPF 程序的编写和调试技巧。不过,随着 eBPF 技术的不断普及,越来越多的学习资源和工具涌现出来,相信你会很快上手 eBPF,并体验到它带来的强大力量。

所以,还在等什么呢?赶快开始你的 eBPF 之旅吧!

性能猎手 eBPF性能分析内核

评论点评

打赏赞助
sponsor

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

分享

QRcode

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