跳转至

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'

延伸阅读