eBPF 不是用来替代 top 或 perf 的,而是当 perf 火焰图满是内核符号、strace 挂上即卡、业务日志查不到延迟毛刺时,唯一能穿透内核执行路径的活体探针。

直接上结论:eBPF 不是用来“替代 top 或 perf”的,而是当你发现 perf 火焰图里全是内核符号、strace 挂上去就卡死、业务日志里查不到延迟毛刺时,唯一能穿透到内核执行路径内部的活体探针。
为什么传统工具在隐蔽瓶颈前集体失效
典型现象包括:接口 P99 延迟突增但 CPU 使用率平稳;某个微服务偶发 500ms+ 延迟,perf record -e cycles,uops_issued.any 却显示无热点;strace -p 一挂上,问题反而消失。这些不是假象,是传统工具的固有盲区:
-
strace依赖 ptrace,每次系统调用都会触发上下文切换和信号拦截,在高并发下引入毫秒级干扰,掩盖真实行为 -
perf默认采样基于硬件事件(如 cycles),对短时抖动、锁竞争、调度延迟等非 CPU-bound 问题不敏感 - 应用层 APM(如 SkyWalking)看不到内核态阻塞点,比如
tcp_sendmsg卡在sk_stream_wait_memory、或do_sys_open被inode_lock拖住
用 bpftrace 快速定位“看不见”的阻塞点
bpftrace 是最轻量、最贴近实战的 eBPF 入口,无需写 C、不用编译,一条命令就能验证假设。关键不是“看全”,而是“精准打点”:
- 怀疑网络发送卡在内存等待?运行:
bpftrace -e 'kprobe:tcp_sendmsg { @start[tid] = nsecs; } kretprobe:tcp_sendmsg /@start[tid]/ { $d = (nsecs - @start[tid]) / 1000000; if ($d > 10) {@us[comm, $d] = count();} delete(@start[tid]); }' - 想确认是否被文件锁阻塞?用:
bpftrace -e 'kprobe:__flock64 { printf("flock on %s by %s\n", comm, str(args->filp->f_path.dentry->d_name.name)); }' - 排查调度延迟?直接抓
sched_wakeup到实际运行的时间差:bpftrace -e 'kprobe:sched_wakeup { @start[pid] = nsecs; } kprobe:finish_task_switch /@start[pid]/ { $delay = nsecs - @start[pid]; if ($delay > 1000000) {@wakeup[comm, $delay/1000000] = count();} delete(@start[pid]); }'
注意:所有 kprobe 和 kretprobe 名称必须与当前内核符号严格一致(可用 sudo cat /proc/kallsyms | grep tcp_sendmsg 核对),函数参数访问需用 args->xxx 且不能越界——这是 verifier 拦截最多的地方。
tcprtt 和 profile 工具组合识别微秒级抖动源
当瓶颈表现为网络 RTT 分布长尾(比如 95% 在 1ms 内,但总有 0.1% 超过 50ms),单靠 ping 或 tcpdump 无法归因。此时要用内核协议栈原生统计:
-
tcprtt直接从 TCP 控制块读取 RTT 样本,不依赖抓包:sudo tcprtt -i 1 -d 10输出中若出现 “8 -> 15 : 23” 这类跨数量级分布,说明存在链路抖动或接收端处理延迟 - 进一步定位是本机还是远端问题?配合
bpftrace -e 'profile:hz:99 /pid == 1234/ {@ns[ustack] = count();}'抓目标进程的用户态栈,看是否卡在 SSL write 或 gRPC 序列化;再用sudo bpftool prog list | grep sched查是否有自定义 BPF 调度器干扰 - 如果抖动集中在特定 CPU 上,检查该 CPU 是否被 irqbalance 锁定、是否有 NIC 中断集中绑定——这时
bpftrace -e 'kprobe:handle_irq { @irq[cpu] = count(); }'能快速暴露中断倾斜
容易被忽略的三个硬约束
eBPF 强大但有边界,踩坑往往不是因为不会用,而是没看清限制:
- 内核版本必须 ≥ 4.18(推荐 ≥ 5.4),否则
kretprobe对某些函数不可用,tcprtt也不存在 - 所有 eBPF 程序加载前必过 verifier,禁止循环、禁止未初始化变量、禁止直接访问结构体嵌套字段(必须用
bpf_probe_read_kernel安全读取) - 生产环境慎用
uprobe跟踪用户态函数——若目标进程是 JIT 编译语言(Go/Java),函数地址会动态变化,导致 probe 失效或 crash
真正难的从来不是写出第一条 bpftrace 命令,而是读懂输出里那个 @us[nginx, 42] 后面的 42 毫秒,到底来自 socket buffer 拥塞、TCP retransmit,还是 cgroup cpu bandwidth 限频。这需要你同时理解内核网络栈、调度器行为和业务流量模型。



















