我最早接触BPF(Berkeley Packet Filter,伯克利包过滤器)这个名词,是在大学里学网络抓包的时候,当时它只是tcpdump底层那个用来过滤数据包的小模块,没觉得有多神奇。直到后来做后端服务性能排查,在一个CPU跑满但找不到元凶的深夜,同事用一条bpftrace命令直接定位到是某个内核函数被频繁调用,我才意识到BPF早就不是当年的“抓包过滤器”了,它已经变成了Linux内核观测与跟踪领域的一把“手术刀”。
这篇文章我想和你聊聊,作为一个不搞内核研发、平时只做服务端开发和运维的普通工程师,我是怎么把BPF环境跑起来、怎么做基础的内核观测与跟踪测试的。内容会涉及环境准备、工具链选型、实际观测实验和一堆我踩过的坑,整体偏实战,适合同样在做性能排查、链路追踪、系统调优,但对内核源码望而却步的朋友。
1. 为什么我最终选择BPF来做内核观测
先说说我遇到的真实场景。当时我们的网关服务频繁出现请求延迟抖动,从应用日志看,耗时全部卡在等待响应上,但下游服务说自己一切正常。用传统的方式排查,无非是登到机器上top看一眼,或者用perf record抓一阵子,然后再用perf report慢慢看。perf其实已经很强了,但它的输出对内核态的分析比较粗,很多时候只能看到热点函数符号,看不到具体是哪个进程、哪个线程、哪条调用路径引起的,信息量不足以定位问题。
后来我试着在内核里加日志重新编译,这个思路直接被同事否决了——生产环境的内核是万万不能随便动的,而且编译内核、灰度发布、回滚的成本实在太高。就在那个阶段,我花了一个周末把BPF相关的资料翻了一遍,又在自己的一台测试服务器上完整跑通了环境,才发现BPF解决的核心痛点正是我需要的:
- 它可以直接挂载到内核的tracepoint、kprobe、uprobe、perf_event等观测点上,几乎可以在内核运行时不修改代码、不重启进程的情况下拿到第一手数据。
- 它带有类似C语言的验证器机制,程序在加载进内核之前就会做安全校验,防止把你的内核搞崩,这让运维同学在测试和生产环境里使用它的心理负担小了很多。
- 它把数据从内核高效地传递到用户态,配合映射(map)结构,可以实现统计、直方图、跟踪等复杂能力。
用一句比较糙的话总结:以前要看内核里发生了什么,要么靠猜,要么靠改代码重编译;现在有了BPF,就好比给运行中的内核装了一套无损内窥镜,哪里不对劲直接伸进去看,看完把镜子抽出来,内核根本不知道你来过。
需要说明的是,BPF并不等于某一个单一工具,而是一整套技术栈。内核侧从4.x开始逐步完善,到了较新的内核版本已经非常成熟。用户态侧涌现了bcc、bpftrace、libbpf等工具库。掌握了这层技术栈,你在分析高CPU、高磁盘IO、网络延迟、锁竞争、内存分配异常等问题时,就不再是两眼一抹黑了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:内核版本、编译工具链与WSL2的坑
如果你只是想在本地快速试验BPF,不打算专门搞一台物理机,那我得先泼一盆冷水:BPF的环境搭建比想象中要麻烦一点点,尤其是内核版本和内核配置选项这两关,绕不过去。
2.1 先确认你的内核版本够不够新
BPF的核心特性(比如BTF、BPF CO-RE、可休眠BPF程序等)在不同内核版本上支持程度不同。我个人的实测经验是,4.9以上可以跑一些基础功能,但要比较顺畅地使用BPF CO-RE(一次编译到处运行)和较新的映射类型,建议内核在5.4以上;如果要体验最新的特性,比如BPF link、trampoline等,5.10到5.15以上会更舒服。
查询内核版本很简单:
bash复制uname -r
我当时用的测试服务器内核是5.15,整体体验很顺。如果你手里的机器内核版本偏低,有两条路可以走:一是重新编译内核开启BPF相关选项,二是在容器或虚拟机里跑一个较新的发行版内核。要注意,容器共享宿主机内核,所以容器里的uname -r其实还是宿主机的,这个方法只在你的宿主机内核本身够新时有效。
2.2 内核配置项必须开启
这是环境准备里最容易被忽略、也最容易折腾人的地方。BPF功能依赖内核的编译选项,光有内核版本号是不够的。你要在编译内核时确保下面这些CONFIG项打开或者以模块方式启用:
config复制CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_JIT=y
CONFIG_HAVE_EBPF_JIT=y
CONFIG_BPF_EVENTS=y
CONFIG_KPROBE_EVENTS=y
CONFIG_UPROBE_EVENTS=y
CONFIG_DEBUG_INFO_BTF=y
CONFIG_DEBUG_INFO=y
CONFIG_DEBUG_INFO_DWARF4=y
其中CONFIG_DEBUG_INFO_BTF尤其重要,它决定了内核是否导出BTF信息,而BTF是BPF CO-RE机制的基础。如果没有BTF,很多较新的工具会报Unable to find kernel BTF之类的错误。
验证当前内核是否已经支持BTF,可以看这个文件是否存在:
bash复制ls /sys/kernel/btf/vmlinux
如果这个文件存在,说明内核已经提供了BTF信息,后面用libbpf、bpftool等工具会省心很多。
2.3 准备编译工具链
就算不重编内核,很多BPF工具本身也需要编译器来生成BPF字节码。我自己在测试机上装了这些:
bash复制sudo apt update
sudo apt install -y build-essential clang llvm git linux-tools-common linux-tools-generic
# 同时安装bpftool
sudo apt install -y linux-tools-$(uname -r) bpftool
注意,linux-tools-generic和后面带内核版本号的tools包可能会因为版本不匹配产生一些小摩擦。如果提示找不到linux-tools-$(uname -r),可以先只装linux-tools-common,然后用源码编译bpftool,后面工具链章节我会细说。
在这里我强烈建议不要用老旧的gcc去编译BPF程序,BPF程序主流是用clang编译的,因为clang对BPF后端支持得更好,而且很多BPF代码里的内核辅助函数注解、BTF生成都需要clang配合。我遇到过用gcc硬编之后加载时报unknown opcode的情况,换成clang之后问题直接消失。
2.4 在WSL2里测试的注意事项
我看到不少朋友喜欢在WSL2里搭Linux环境。WSL2本身用的是微软定制的内核,好处是你不用自己折腾双系统,坏处是那个内核的配置和标准发行版内核有差异。我看到的热词里有一条“wsl 2 linux 内核压缩包”,其实就对应这个问题。
在WSL2里跑BPF程序,我踩
