如何对 eBPF 代码进行性能分析?
如果我们正在运行任何 eBPF 工作负载或编写 eBPF 代码,我们希望衡量其性能影响,在这篇文章中,我们将展示一个如何做到这一点的例子。在这个例子中,我们的目标是衡量文件打开操作的性能,这是操作系统中最关键的功能之一。我们的代码使用了 eBPF 的文件打开钩子,我们想要衡量添加此钩子所带来的性能开销。为了识别可能的瓶颈,我们需要一个简单的 C 测试工具,具有最少的依赖项,旨在测量文件打开性能。 #define _GNU_SOURCE #include <fcntl.h> #include <stdio.h> #include <time.h> #include <stdint.h> #include <stdlib.h> #include <unistd.h> #include <sched.h> #include <sys/mman.h> #include <sys/syscall.h> static inline uint64_t now_ns ( void ) { struct timespec ts; clock_gettime (CLOCK_MONOTONIC, & ts); /* VDSO,无系统调用 */ return ( uint64_t )ts.tv_sec * 1000000000ull + ts.tv_nsec; } int main ( int argc, char ** argv) { const char * path = argv[ 1 ]; uint64_t n = strtoull (argv[ 2 ], NULL, 10 ); uint64_t warm = n / 10 ; /* 预分配 + 预故障 + 锁定:循环中没有分页故障 */ uint32_t * d = mmap (NULL, n * sizeof ( uint32_t ), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, - 1 , 0 ); mlock (d, n * sizeof ( uint32_t )); for ( uint64_t i = 0 ; i < n; i ++ ) d[i] = 0 ; /* 让所有内容出故障 */ for ( uint64_t i = 0 ; i < n; i ++ ) { uint64_t t0 = now_ns (); long fd = syscall (SYS_openat, AT_FDCWD, path, O_RDONLY); /* 原始,不使用 libc 包装器 */ uint64_t t1 = now_ns (); if (fd >= 0 ) close (fd); d[i] = ( uint32_t )(t1 - t0); } /* 仅在循环之后转储 */ for ( uint64_t i = warm; i < n; i ++ ) printf ( "%u " , d[i]); return 0 ; } 上述代码示例的目标是保持简单,并在温暖的缓存条件下重新打开相同的文件,最小化不相关的文件系统和磁盘 I/O 变异性,并帮助识别 p50/p99。该代码调用 syscall(SYS_openat, …) 而不是 libc openat() 包装器,前 10% 的结果被丢弃作为热身期。这个测试工具会生成打开一个文件 x 次及其耗时的结果。因此,我们可以用它来测量 eBPF 钩子附加之前和之后的情况。设置 在对 eBPF 代码进行性能分析时,我们希望 perf 工具能够解析符号,以便我们可以分析代码中的问题所在。为此,我们必须运行以下命令。 sudo sysctl -w net.core.bpf_jit_enable = 1 sudo sysctl -w net.core.bpf_jit_kallsyms = 1 上述命令启用了 JIT 并暴露 JIT 编译的 BPF 符号,以便 perf 报告可以显示程序名称而不是未知地址。要检查符号是否出现在 perf 工具中,请运行 eBPF 代码并使用如下命令。 sudo bpftool prog show | rg -A4 ' lsm ' sudo rg 'bpf_prog_[0-9a-f]+_ ' /proc/kallsyms | rg 'security|path|file|open' 在上述 rg 命令中,我们正在检查 lsm,因为我们正在测量 LSM 钩子。此外,因为我们使用了自定义内核版本,所以该内核版本的 perf 不在标准路径中,我们在示例中已安装: PERF=/usr/lib/linux-tools/6.8.0-134-generic/perf 测量 现在我们已经设置了所有必要的工具,第一步是测量在没有运行 eBPF 代码的情况下的性能,这时可以使用上述 C 代码进行帮助。我们通过打开文件 /etc/hostname 测量代码并将结果输出到文件,以便我们可以计算 p50/p99。 sudo taskset -c 3 chrt -f 99 ./bench /etc/hostname 100000 > /tmp/samples.txt taskset -c 3 使执行固定在 CPU 3,减少 CPU 迁移噪声,chrt -f 99 则为基准提供了极高的 CPU 优先级。它在几乎所有正常程序之前运行,并保持运行直到完成、被阻塞或被中断。C 代码丢弃前 10%;文件应包含 90,000 samples.txt。接下来,运行 eBPF 代码并执行如下命令。 sudo $PERF record \ -g \ --call-graph fp \ -e cycles:k \ -F 997 \ -o ~/perf.data \ -- \ taskset -c 3 \ chrt -f 99 \ ./bench /etc/hostname 200000 \ > /tmp/samples.txt -g 记录调用栈,--call-graph fp 使用帧指针展开栈,-e 仅在内核模式下采样 CPU 周期,包括系统调用、VFS、LSM 和 eBPF 执行而不涉及用户空间基准工作。-F 997 请求每秒 997 个样本,非整数频率有助于避免周期性对齐。运行上述命令后,运行此命令以对数据进行排序。 sudo "$PERF" report -i ~/perf.data --stdio --sort comm,dso,symbol > perf.txt 使用 Inferno 生成的相同 perf.data 的火焰图。此处感兴趣的栈是 bpf_lsm_file_open 及其上方的所有内容。这是 perf.txt 的示例输出,显示在 bpf_lsm_file_open 及其尾调用上花费的时间是性能瓶颈所在。这最终是在热点路径中,这意味着每次分配到 CPU 的周期削减都会对系统性能产生显著影响。
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡