eBPF + bpftrace 生产环境调试实战:不用改代码、不用重启,一行命令定位线上问题(2026)

📝 517 字 · ☕ 2 分钟阅读

一个凌晨三点的事故

那是一个周三的凌晨,钉钉告警群炸了。生产环境一台 Nginx 机器 CPU 飙到 100%,但 top 看到的是软中断(%si)占大头,而不是用户态进程。常规手段——perf、strace、tcpdump——要么太重(影响线上),要么信息不够细。

运维小哥在群里发了一句「要不要重启?」。我回:「别急,给我两分钟。」

然后我敲了一行 bpftrace 命令——没有重启,没有加日志,没有重新部署。两分钟后定位到是一个新上线的微服务在疯狂建短连接。改了连接池参数,CPU 从 100% 掉到 15%。

这就是 eBPF 的快感。今天这篇文章,带你从零上手 bpftrace,学会几个「救过命的命令」,下次遇到类似场景你就是那个说「别急」的人。

eBPF 是什么?为什么它这么狠?

eBPF(extended Berkeley Packet Filter)是 Linux 内核里一个沙箱化的虚拟机。你可以往内核里注入一段字节码程序,在内核事件(系统调用、函数进入/退出、网络包到达等)发生时执行,不需要改内核代码、不需要重启、不影响线上服务

传统排查工具的问题:

  • strace:每个系统调用都要走 ptrace,开销巨大。在 QPS 上万的机器上跑 strace 等于自残。
  • perf:采样型,只能看 CPU 热点,看不到具体参数和返回值。
  • tcpdump:能看到包,但不知道是哪个进程、哪个系统调用发的。
  • 加日志:需要改代码 + 重新部署。等你改完部署完,问题可能已经自己恢复了。

eBPF 的核心优势:你在内核的「插桩点」上挂载一段小程序,无论线上跑了多久、QPS 多高,它都能安全高效地采集数据。并且 eBPF 程序会经过内核验证器检查,不会把内核搞崩。

而 bpftrace 就是 eBPF 的高级封装——你不用写 C 代码编译成 BPF 字节码,一行 awk 风格的命令就能用。

安装 bpftrace

在 Ubuntu 24.04 / 22.04 上安装很简单:

$ sudo apt install bpftrace
# 验证
$ sudo bpftrace -e 'BEGIN { printf("eBPF ready!\n"); exit(); }'
Attaching 1 probe...
eBPF ready!

CentOS / RHEL 需要先启用 EPEL:

$ sudo dnf install epel-release
$ sudo dnf install bpftrace

内核版本要求 ≥ 4.9(推荐 ≥ 5.4)。现在的服务器基本都是 5.x 或 6.x,没什么好担心的。

五个救过命的 bpftrace 一行命令

1. 谁在疯狂调用某个系统调用?

场景:CPU 软中断高,怀疑是有程序在大量建连/断连。

$ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_accept* { @[comm] = count(); }'
# Ctrl-C 退出
@[nginx]: 1523
@[my-service]: 48723   # ← 这个有问题!
@[sshd]: 2

@[comm] = count() 是按进程名做聚合计数。跑 10 秒钟 Ctrl-C,一眼看出 my-service 在疯狂 accept。

继续深挖——看它到底在 accept 什么:

$ sudo bpftrace -e 'tracepoint:syscalls:sys_enter_accept4 { 
    printf("pid=%d comm=%s\n", pid, comm); 
}'

如果你想看每次 accept 的 fd、源 IP 等细节,可以 hook sys_exit_accept4(返回时),因为 fd 在返回值里:

$ sudo bpftrace -e 'tracepoint:syscalls:sys_exit_accept4 { 
    printf("pid=%d fd=%ld\n", pid, args->ret);
}'

2. 哪个文件在被高频读写?

场景:磁盘 IO 打满,iotop 看到一堆进程但不知道谁在写哪个文件。

$ sudo bpftrace -e 'kprobe:vfs_read,kprobe:vfs_write { 
    @bytes[comm, str(args->buf ? ((struct file *)args->filp)->f_path.dentry->d_name.name : "?")] = sum(arg2); 
}'
# 10秒后 Ctrl-C

这个稍微复杂点——kprobe:vfs_read 是内核函数的动态探针,args->filp 是文件指针,我们顺着它拿到文件名。输出类似:

@bytes[my-service, access.log]: 2147483648
@bytes[nginx, error.log]: 52428800
@bytes[python3, data.db]: 67108864

一眼看出 my-service 在往 access.log 里写了 2GB——这日志量显然不正常。

3. 慢文件操作:什么东西在卡 IO?

场景:业务接口 P99 延迟突然从 50ms 涨到 2s,怀疑是磁盘 IO 卡住。

$ sudo bpftrace -e 'kprobe:vfs_read { @start[tid] = nsecs; @file[tid] = str(args->filp->f_path.dentry->d_name.name); }
kretprobe:vfs_read /@start[tid]/ { 
    $dur_ms = (nsecs - @start[tid]) / 1000000;
    if ($dur_ms > 100) {
        printf("SLOW READ: %s pid=%d tid=%d duration=%dms\n", @file[tid], pid, tid, $dur_ms);
    }
    delete(@start[tid]);
    delete(@file[tid]);
}'

这个一行命令做了几件事:
kprobe:vfs_read 在进入时记录时间戳和文件名;kretprobe:vfs_read 在返回时算耗时,>100ms 的打印出来。

输出示例:

SLOW READ: data.db pid=12345 tid=12346 duration=3200ms
SLOW READ: cache.bin pid=12345 tid=12347 duration=1800ms

磁盘 IO 延迟 3 秒——大概率是磁盘故障或文件系统锁竞争。

4. 哪个进程在 fork 大量子进程?

场景:服务器 load average 突然暴增但 CPU 不高,怀疑有进程在疯狂 fork 后立即退出。

$ sudo bpftrace -e 'tracepoint:sched:sched_process_fork { 
    @[comm] = count();
}'
# 10秒后 Ctrl-C

类似地,看谁在大量创建线程:

$ sudo bpftrace -e 'kprobe:copy_process { @[comm] = count(); }'

5. 一次性看所有系统调用分布

场景:不知道是什么问题,先摸个底。

$ sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { 
    @syscalls[comm] = count(); 
}'

# 或者直接看被调用最多的系统调用
$ sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { 
    @[probe] = count(); 
}'

10 秒后 Ctrl-C,你说不定能看到 futex: 3000000——恭喜,有人在锁上打架。

进阶:用 bpftrace 写个迷你火焰图

perf 能生成火焰图,但 bpftrace 也能做堆栈采样,而且更轻量:

$ sudo bpftrace -e 'profile:hz:99 { @[kstack, ustack, comm] = count(); }' > /tmp/stacks.txt
# 跑 30 秒后 Ctrl-C

profile:hz:99 是 timer-based 采样,每秒 99 次(和 perf 默认频率一样)。kstack 是内核堆栈,ustack 是用户态堆栈。

拿到堆栈数据后用 Brendan Gregg 的 FlameGraph 工具转成 SVG:

$ git clone https://github.com/brendangregg/FlameGraph
$ # 先把 bpftrace 输出转成折叠格式(需要自己写个简单脚本)
$ ./FlameGraph/flamegraph.pl folded.txt > flame.svg

在你没有 perf 权限或者 perf 采集太重的场景下,bpftrace 采样是一个很好的替代。

什么时候不该用 bpftrace?

bpftrace 虽然狠,但不是银弹:

  • 长时间持续监控:bpftrace 更适合跑几分钟然后 Ctrl-C 看聚合结果。长时间 24×7 监控应该用 BCC 工具(如 tcptop、biolatency)或者切换到 eBPF 的 Go/Rust SDK。
  • 非常高频的事件:比如每个网络包都 hook,在高流量机器上会导致丢事件。bpftrace 会自动丢弃来不及处理的事件并打印 lost events 计数。
  • 复杂状态机:bpftrace 的脚本语言不是图灵完备的(有意为之,保证安全性),如果你需要复杂的状态跟踪(如跟踪一个 TCP 连接从建立到关闭的全生命周期),用 BCC Python 或者 libbpf。
  • 需要看用户态变量:bpftrace 可以通过 uprobe 看用户态函数,但解析复杂数据结构(如 C++ 的 std::string)比较痛苦。

实战复盘:短连接风暴

回到开头的故事。那晚的排查路径:

  1. top → CPU 100%,但 %si(软中断)占了 60%,不是用户态进程。
  2. sar -n DEV 1 → 网卡流量正常,不是 DDoS。
  3. ss -s → TCP 连接数 80000+,远超正常水平的 5000。
  4. bpftrace 一行命令定位 sys_enter_connect → 是 Java 微服务在循环建连。
  5. 看代码 → 每次 HTTP 请求都 new 一个 HttpClient,用完不关。
  6. Fix → 改连接池配置 + 加连接复用。重启后连接数从 80000 降到 2000。

整个排查从收到告警到定位根因,8 分钟。如果没有 bpftrace,我们大概率会走:看监控大盘 → 看不懂 → 怀疑网络 → 拉网工 → 网工说没问题 → 怀疑应用 → 加日志 → 灰度发布 → 等复现 → 再分析。这个循环至少半天。

一句话总结 bpftrace 的价值:它让你「不用改一行代码、不用重启服务」就能知道内核里正在发生什么。

FAQ

Q: bpftrace 和 BCC 工具(如 execsnoop、biolatency)是什么关系?

BCC(BPF Compiler Collection)是一套用 Python 写的 eBPF 工具集,适合持续运行和复杂场景。bpftrace 是快速临时排查工具,一行命令即用即走。BCC 工具更稳定(有完善的错误处理和输出格式),bpftrace 更灵活(你可以现场拼一个查询)。

Q: 跑 bpftrace 会影响线上性能吗?

取决于你在 hook 什么。hook 一个冷门系统调用(如 sys_enter_mkdir)几乎零开销。hook sys_enter_read 且在高 IO 场景下会有 1-3% 的 CPU 开销。bpftrace 内置了开销控制——每个探针有默认的开销上限,超过会自动报 lost events 而不是把机器拖垮。另外,eBPF 程序运行在内核的 JIT 编译器中,执行效率远高于传统的 kprobe + printk。

Q: bpftrace 命令报 “ERROR: Error attaching probe” 怎么办?

常见原因:1) 没加 sudo;2) 内核版本太低(需要 ≥ 4.9);3) 目标内核符号不存在(kprobe 挂的函数名不对)。排查方法:sudo bpftrace -l 'kprobe:vfs_*' 列出所有可用探针,确认函数名正确。另外 BTF(BPF Type Format)需要内核 ≥ 5.2 才完全支持,旧内核访问结构体字段可能受限。

Q: 生产环境的容器里能用 bpftrace 吗?

eBPF 是内核级能力,需要在宿主机上跑(或者 privileged 容器)。普通容器受限于 namespace 和 capabilities。推荐:在宿主机上安装 bpftrace,用 --pid 参数限定追踪目标容器进程:bpftrace --pid $(docker inspect -f '{{.State.Pid}}' container_name) -e '...'

相关阅读:

小结

eBPF 和 bpftrace 是现代 Linux 运维和性能分析的「核武器」。它把以前需要改内核、改应用、加日志、重启服务的排查路径压缩成一行命令。如果你还没用过,今天就把这五个一行命令记下来——哪天半夜告警响了,你会感谢现在的自己。

记住这三句话:

  • 看谁在调用什么系统调用tracepoint:raw_syscalls:sys_enter
  • 看谁在读写什么文件kprobe:vfs_read / kprobe:vfs_write
  • 看什么东西慢kretprobe 记录进入和退出时间差

然后你就拥有了「别急,给我两分钟」的底气。

🔗 延伸阅读:bpftrace 适合内核级深挖,但如果你只需要快速定位”进程在等什么”,用 strace 生产环境调试完全指南 会更轻量。两把刀各有各的顺手场景。

📡 延伸阅读:Linux 网络故障排查实战:从 TCP 超时到连接池耗尽的全链路诊断

📤 分享这篇文章