bpftrace是Linux内核eBPF技术最流行的高级追踪语言前端之一,它可以用一行命令追踪内核函数、系统调用、进程行为和性能指标,不需要重启系统,不需要修改应用程序源码,开销可控,特别适合在生产服务器上做临时性的性能分析和故障定位。这篇文章从原理、安装、语法到实战案例,完整讲一遍bpftrace的动态追踪流程,帮你把eBPF这套内核级观测能力真正用起来。

bpftrace是什么,它和eBPF是什么关系
eBPF(extended Berkeley Packet Filter)是Linux内核提供的沙箱化虚拟机机制,允许用户编写的程序安全地加载到内核中运行,常用于网络过滤、安全监控和性能观测。但直接编写eBPF程序需要处理字节码、地图(map)等底层细节,门槛较高。bpftrace就是在eBPF之上封装的一层高级追踪语言,语法类似awk,写一句脚本,bpftrace负责编译成eBPF字节码,通过bpf()系统调用注入内核执行,事件触发时把结果回传到用户态打印出来。
理解这个分层结构很重要:底层是内核的eBPF虚拟机和各类探针点,中间是LLVM编译和加载框架,上层是bpftrace、BCC、perf等工具。BCC偏重用Python写复杂工具,适合开发长期使用的监控程序;bpftrace偏向一次性、交互式的排查,一句命令就能出结果,被很多人称为内核观测领域的“瑞士军刀”。如果只是临时回答“这个进程在等什么”“磁盘IO是谁发起的”这类问题,bpftrace通常是效率最高的选择。
安装bpftrace与运行环境准备
bpftrace对内核版本有要求,一般建议内核4.9以上,4.18及以上才能使用更完整的探针类型。可以用uname -r查看内核版本。运行还需要内核符号表和头文件支持,比如内核调试信息包(如CentOS上的kernel-debuginfo)在追踪内核函数时是必需的,否则会报找不到符号或探针的错误。
主流发行版安装都很简单。Ubuntu和Debian直接用apt install bpftrace,CentOS和RHEL可以先安装epel-release再yum install bpftrace,openSUSE、Fedora也都有现成的软件包。如果想用最新功能,可以从源码编译,依赖llvm、clang、libbpf等库。安装完成后执行bpftrace -e 'BEGIN { printf("hello\n"); }',如果输出hello说明环境正常。注意运行必须具备root权限或CAP_BPF、CAP_SYS_ADMIN等能力,生产环境建议用sudo执行。
必须掌握的核心语法:探针、过滤与输出
bpftrace脚本的基本结构是:探针 /过滤器/ { 动作 }。探针指定在什么事件点触发,过滤器决定哪些事件要处理,动作块里写采集逻辑。常用探针类型包括kprobe和kretprobe(内核函数入口与返回)、tracepoint(内核静态追踪点,稳定可靠)、uprobe和uretprobe(用户态函数)、profile和interval(定时采样)、software和hardware(软件硬件计数器)等。例如tracepoint:syscalls:sys_enter_openat表示进入openat系统调用时触发。
变量方面,内置变量pid、tid、comm、cpu、args、retval分别对应进程号、线程号、进程名、CPU编号、参数和返回值。输出用printf(),格式化字符串语法和C语言一致。聚合分析是bpftrace的精髓:@count统计次数、@sum求和、@avg平均、@min和@max极值、@hist做直方图,例如@usecs = hist(nsecs / 1000)可以把延迟分布画成ASCII直方图。还有join()拼接字符串参数、ksym()把地址翻译成内核符号、stack()抓调用栈等实用函数。
写脚本时要养成加过滤器的习惯,比如/pid == 12345/,只采集目标进程,避免全量事件把系统打爆。字符串比较用str(comm) == "nginx"这种形式,因为comm本身是数组,必须先转成字符串再比较。
典型场景实战:四个直接可用的案例
案例一:追踪文件打开行为排查异常读取
想知道某个进程到底读了哪些文件,一句命令就能搞定:
bpftrace -e 'tracepoint:syscalls:sys_enter_openat /str(comm) == "mysqld"/ { printf("%s %s\n", comm, str(args->filename)); }'
这条命令在进入openat系统调用时判断进程名是否为mysqld,是则打印进程名和文件路径。排查配置加载错误、隐藏文件读取、安全问题排查时非常有效。如果要统计打开次数排行,把printf换成@[str(args->filename)] = count(),最后按聚合结果排序即可看到最热文件。
案例二:定位磁盘IO延迟瓶颈
利用block层的tracepoint可以测量每次IO的耗时分布:
bpftrace -e 'tracepoint:block:block_rq_issue { @start[args->dev, args->sector] = nsecs; } tracepoint:block:block_rq_done /@start[args->dev, args->sector]/ { @usecs = hist((nsecs - @start[args->dev, args->sector]) / 1000); delete(@start[args->dev, args->sector]); }'
脚本在IO下发时记录时间戳,在IO完成时计算差值,用直方图展示微秒级延迟分布。如果大量请求落在几十毫秒区间,说明存储设备确实存在瓶颈;如果延迟很低但应用仍然慢,就要往锁等待、调度延迟方向排查。这种分发配对是bpftrace的经典模式,很多官方工具如biolatency就是同样原理。
案例三:分析CPU热点函数
采样式分析CPU占用,用profile探针定期抓取运行位置:
bpftrace -e 'profile:hz:99 /pid == 8888/ { @[ustack] = count(); }'
每秒采样99次目标进程的用户态调用栈,统计各栈出现的次数,出现越多的栈说明CPU时间消耗越多。配合debuginfo可以精确到代码行。99赫兹而不是100赫兹是有意为之,避免与系统周期性任务同步造成采样偏差。
案例四:测量网络请求延迟
排查网络问题时,可以围绕TCP状态变迁做统计,比如统计各个端口上的连接建立情况:
bpftrace -e 'tracepoint:tcp:tcp_connect { @[comm] = count(); }'
如果想测量某个服务的处理耗时,可以在用户态函数入口和返回处分别用uprobe、uretprobe打点,配合时间戳算出单次处理延迟。这种思路对没有埋点的第三方程序尤其有用,不需要改一行代码就能拿到延迟数据。
生产环境使用注意事项与性能开销控制
虽然eBPF本身经过验证器保证安全,但不当的脚本仍可能带来明显开销。几个原则要记住:第一,尽量用tracepoint替代kprobe,静态追踪点在不同内核版本间更稳定;第二,热路径探针必须加过滤器,比如高并发系统上追踪所有网络包很容易丢事件甚至拉高CPU;第三,聚合统计优先于printf全量打印,把数据留在内核map里,退出时一次性输出;第四,长时间运行的任务加interval定时输出,避免map无限增长占满内存。
故障诊断之外,bpftrace也常用于容量分析和性能回归验证,比如上线前后对比系统调用延迟直方图。官方仓库自带了大量现成脚本,如execsnoop监控进程执行、opensnoop监控文件打开、runqlat测量调度延迟,先学会用这些工具再改造成自己的脚本,上手速度会快很多。掌握bpftrace之后,你会发现过去很多依赖重启、加日志、复现半天的问题,现在一条命令几秒钟就能看到内核里真实发生了什么。