bpftrace
是什么 / 解决什么问题
bpftrace 是基于 eBPF 的高级追踪语言,类似 awk/DTrace。用一行或几行脚本就能动态观测内核和用户态行为,开销极低(生产安全),无需重启或修改程序。
使用模式
基本语法
bpftrace -e 'probe /filter/ { action }'
probe: 挂载点(在哪里触发)
filter: 条件(可选)
action: 执行的动作
常用探针类型
| 探针 |
语法 |
说明 |
| tracepoint |
tracepoint:category:name |
内核静态追踪点 |
| kprobe |
kprobe:function_name |
内核函数入口 |
| kretprobe |
kretprobe:function_name |
内核函数返回 |
| uprobe |
uprobe:/path/bin:function |
用户态函数入口 |
| uretprobe |
uretprobe:/path/bin:function |
用户态函数返回 |
| software |
software:event:count |
软件事件 |
| profile |
profile:hz:99 |
定时采样 |
| interval |
interval:s:1 |
定时输出 |
一行命令示例集
# 统计 syscall 调用频率
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
# 统计进程的 read 大小分布
bpftrace -e 'tracepoint:syscalls:sys_exit_read /pid == 1234 && args->ret > 0/ {
@bytes = hist(args->ret);
}'
# 追踪文件打开
bpftrace -e 'tracepoint:syscalls:sys_enter_openat {
printf("%s %s\n", comm, str(args->filename));
}'
# 统计 TCP 连接延迟
bpftrace -e '
kprobe:tcp_v4_connect { @start[tid] = nsecs; }
kretprobe:tcp_v4_connect /@start[tid]/ {
@connect_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# 查看进程的 write 延迟
bpftrace -e '
tracepoint:syscalls:sys_enter_write /pid == 1234/ { @start[tid] = nsecs; }
tracepoint:syscalls:sys_exit_write /@start[tid]/ {
@write_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# CPU 上下文切换频率
bpftrace -e 'tracepoint:sched:sched_switch { @[comm] = count(); }'
# 每秒页缺失数
bpftrace -e 'software:page-faults:1 { @[comm] = count(); }
interval:s:1 { print(@); clear(@); }'
# 磁盘 I/O 延迟直方图
bpftrace -e 'tracepoint:block:block_rq_issue { @start[args->dev, args->sector] = nsecs; }
tracepoint:block:block_rq_complete /@start[args->dev, args->sector]/ {
@io_ms = hist((nsecs - @start[args->dev, args->sector]) / 1000000);
delete(@start[args->dev, args->sector]);
}'
多行脚本
#!/usr/bin/env bpftrace
// tcp_life.bt: 追踪 TCP 连接生命周期
#include <net/sock.h>
kprobe:tcp_set_state {
$sk = (struct sock *)arg0;
$newstate = arg1;
if ($newstate == 1) { // TCP_ESTABLISHED
@established[comm, pid] = count();
}
if ($newstate == 7) { // TCP_CLOSE
@closed[comm, pid] = count();
}
}
interval:s:5 {
print(@established);
print(@closed);
clear(@established);
clear(@closed);
}
行为与语义
内置变量
| 变量 |
含义 |
pid |
进程 ID |
tid |
线程 ID |
comm |
进程名 |
nsecs |
纳秒时间戳 |
cpu |
CPU 核编号 |
uid |
用户 ID |
args |
tracepoint 参数 |
arg0-argN |
kprobe 函数参数 |
retval |
kretprobe/uretprobe 返回值 |
kstack |
内核调用栈 |
ustack |
用户态调用栈 |
内置函数
count() # 计数
hist(value) # 对数直方图
lhist(value, min, max, step) # 线性直方图
sum(value) # 求和
avg(value) # 平均值
min(value) # 最小值
max(value) # 最大值
str(ptr) # 内核字符串→bpftrace 字符串
printf(fmt, ...) # 格式化输出
time(fmt) # 时间戳
kstack(limit) # 内核栈(限制深度)
ustack(limit) # 用户栈
Map(关联数组)
# Map 是 bpftrace 的核心数据结构
@name[key1, key2] = value;
# 示例
@bytes[comm] = sum(args->ret); # 按进程名累计字节数
@latency[pid] = hist(delta); # 按 PID 的延迟直方图
@start[tid] = nsecs; # 按线程记录起始时间
性能考量
bpftrace 的开销
原则:探针触发频率越高,开销越大
低开销(生产安全):
- 低频 tracepoint (< 10K/s):< 1%
- kprobe 非热路径:< 1%
- profile:hz:99:接近 0
中等开销:
- 高频 syscall 追踪 (> 100K/s):1-5%
- 每次 read/write 都记录:视频率而定
高开销(谨慎使用):
- 追踪所有内存分配:可能 > 10%
- uprobe 热函数:5-10%
减少开销的技巧
# 1. 用 filter 减少触发次数
bpftrace -e '... /pid == 1234/ { ... }'
# 2. 用 count/sum 聚合而非 printf 每次输出
# 差:每次触发都 printf
bpftrace -e 'kprobe:vfs_read { printf("%d\n", pid); }'
# 好:聚合后定时输出
bpftrace -e 'kprobe:vfs_read { @[comm] = count(); }
interval:s:5 { print(@); clear(@); }'
# 3. 限制 kstack/ustack 深度
@[kstack(5)] = count(); # 只取 5 层
实用脚本集
网络
# TCP 重传追踪
bpftrace -e 'tracepoint:tcp:tcp_retransmit_skb {
printf("%s:%d -> %s:%d\n",
ntop(args->saddr), args->sport,
ntop(args->daddr), args->dport);
}'
# 新 TCP 连接(accept)
bpftrace -e 'kretprobe:inet_csk_accept /retval != 0/ {
@accepts[comm] = count();
}'
# socket recv buffer 满
bpftrace -e 'tracepoint:sock:sock_rcvqueue_full { @[comm] = count(); }'
文件系统
# 慢 fsync
bpftrace -e '
tracepoint:syscalls:sys_enter_fsync { @start[tid] = nsecs; }
tracepoint:syscalls:sys_exit_fsync /@start[tid]/ {
$dur = (nsecs - @start[tid]) / 1000;
if ($dur > 1000) {
printf("%s fsync took %d us\n", comm, $dur);
}
@fsync_us = hist($dur);
delete(@start[tid]);
}'
# VFS read/write 的文件追踪
bpftrace -e 'kprobe:vfs_read { @read[comm, str(((struct file *)arg0)->f_path.dentry->d_name.name)] = count(); }'
内存
# 页分配追踪
bpftrace -e 'tracepoint:kmem:mm_page_alloc { @order = hist(args->order); }'
# brk 调用(堆扩展)
bpftrace -e 'tracepoint:syscalls:sys_enter_brk { printf("%s brk(%p)\n", comm, args->brk); }'
常见陷阱与 FAQ
1. 需要 root 权限
# bpftrace 需要 root(加载 eBPF 程序需要 CAP_BPF/CAP_SYS_ADMIN)
sudo bpftrace -e '...'
2. 内核版本要求
bpftrace 需要 Linux 4.9+(基本功能)
推荐 5.x+(更多特性支持)
BTF 支持 (5.2+) 让结构体访问更简单
3. tracepoint 比 kprobe 更稳定
# kprobe: 挂载内核函数,函数名可能随版本变化
kprobe:tcp_sendmsg # 如果内核重构可能找不到
# tracepoint: 稳定 ABI,不随内核版本变化
tracepoint:syscalls:sys_enter_write # 始终存在
4. 查找可用探针
# 列出所有 tracepoint
bpftrace -l 'tracepoint:*'
# 搜索特定 tracepoint
bpftrace -l 'tracepoint:*tcp*'
bpftrace -l 'tracepoint:syscalls:*write*'
# 查看 tracepoint 的参数
bpftrace -lv 'tracepoint:syscalls:sys_enter_read'
延伸阅读