eBPF 与 bpftrace:更深入地观测内核
实验环境:Ubuntu 24.04 / 内核 6.8.0-106-generic / Cgroup v2 / 华为云 FlexusX 8C16G
本文所有命令输出均来自真实实验机,可直接复现。bpftrace 版本 v0.20.2,内核自带 BTF。
一、引子:ftrace 还不够"聪明"的时候
上一篇 ftrace 能看调用链、能插桩内核函数,但它有个天花板:所有逻辑都在内核态"记录",聚合、过滤、统计得事后在用户态做。如果你想问"这个容器过去 10 秒里,每次read的延时分布是多少?"“哪个 cgroup 的openat最多?”“把几十万次事件按进程聚合出一张直方图”——ftrace 做不到在内核里就地算好再回传。
这时候需要eBPF:一套运行在内核态、带 JIT、有 Verifier 安全保障的虚拟机,让你写一小段 C 逻辑,挂到 kprobe / tracepoint / syscalls 上,在内核里直接聚合数据进 map,用户态只来取结果。再配上bpftrace(类 awk 的一行脚本语言),无需写 C、无需编译,就能把上面的问题一行搞定。
二、eBPF 原理简述
2.1 它是怎么跑起来的
- 写程序:用 C(BCC)或 bpftrace 脚本描述"探针触发时做什么"。
- 编译 + Verifier:LLVM 把 eBPF 字节码编译,内核Verifier在加载前做严格校验——无越界、无死循环、只访问合法内存、调用受限 helper——不通过就拒绝加载,因此 eBPF 不会把内核搞崩。
- JIT:校验通过后,eBPF 字节码被JIT 编译为原生机器码,执行开销接近普通内核代码。
- Map:eBPF 程序与用户态通过Map(内核里的键值存储,哈希表/数组/直方图等)交换数据。探针高频写 map,用户态低频读 map——这就是"内核态聚合"的关键。
- 挂载点:eBPF 程序可挂到kprobe / kretprobe(动态,同 ftrace 的 kprobe)、tracepoint(静态)、perf event、XDP(网络)、cgroup等。
2.2 它比 ftrace 强在哪
| 维度 | ftrace | eBPF |
|---|---|---|
| 聚合 | 只能全量记录,事后分析 | 内核态就地聚合(hist/sum/count) |
| 过滤 | 函数级 / 简单谓词 | 任意 C 表达式谓词(comm==、pid==、算数) |
| 数据结构 | 文本 trace 缓冲 | Map(直方图、统计、栈表) |
| 编程能力 | 无(配置式) | 图灵完备(可计算延时、关联事件) |
| 安全 | 内核内置,稳 | Verifier 沙箱保障 |
| 典型用途 | 看调用链/耗时 | 统计分布、按 cgroup 聚合、网络观测 |
一句话:ftrace 回答"发生了什么调用",eBPF 回答"这些调用按 X 维度聚合后长什么样"。
三、bpftrace 单行实战
bpftrace 语法:probe { action },变量@name是全局 Map,退出时自动打印。下面每个都真跑过。
3.1 系统调用次数 Top(谁最忙)
root@ecs-a8bb-0002:~# bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'Attaching1probe... @[wpa_supplicant]:1@[cron]:7@[timeout]:9@[sh]:109@[irqbalance]:102@[multipathd]:97@[pluginctl]:148@[bpftrace]:304@[remotectl]:348@[containerd]:929@[uniagentd]:3848一眼看出本机系统调用最密集的是uniagentd(云监控 agent,3848 次)、containerd(929 次)。count()在 Map 里累加,退出时按值排序打印——这正是 eBPF "内核态聚合"的缩影。
3.2 跟踪 execve(execsnoop 效果)
execsnoop是经典工具:谁在偷偷起进程(可能是攻击、cron、 Hook)。bpftrace 一行复刻:
root@ecs-a8bb-0002:~# bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%-6d %-12s %s\n", pid, comm, str(args->filename)); }'Attaching1probe...49032bash/usr/bin/ls49033bash/usr/bin/whoami49034bash/usr/bin/sleep49035bash/usr/bin/grep49036bash/usr/bin/headargs->filename是 tracepoint 的上下文字段,str()把它从内核指针解成字符串。跑ls/whoami时被实时抓到——排查"未知进程从哪来"非常直接。
3.3 I/O 延时直方图
本来想做"块 I/O 延时直方图"(block:block_rq_issue/block_rq_complete算差值)。但本环境块设备走 virtio 半虚拟化,实测block:*tracepoint 完全不触发(已用count()验证block_rq_complete恒为 0)。这是云环境的真实限制,按"替代方案"原则,我换成VFS read 延时直方图——技法完全一样(入口记时间戳、出口算差值、丢进hist()),且必然可复现:
root@ecs-a8bb-0002:~# bpftrace -e 'tracepoint:syscalls:sys_enter_read{@start[pid]=nsecs;}tracepoint:syscalls:sys_exit_read /@start[pid]/{@us=hist((nsecs - @start[pid])/1000);delete(@start[pid]);}' @us:[0]190|@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@[1]210|@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|[2,4)194|@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@[4,8)20|@@@@@@[8,16)23|@@@@@@@[16,32)28|@@@@@@@@@[32,64)15|@@@@[128,256)11|@@@[256,512)5|@[512, 1K)1|[1K, 2K)8|@@[2K, 4K)1|[4K, 8K)3|解读:绝大多数read在0~4µs内完成(命中页缓存,极快);右侧长尾(ms 级)是首次读/页错误/落到磁盘的那几次。这正是直方图的价值——一眼看出"分布"而非只看平均值。同样的hist()+ issue/complete 时间戳差值技法,可直接套到块 I/O / 网络 / 任意两点间延时。
3.4 跟踪容器内进程(按 comm 过滤)
要观测某个容器,在宿主机上用 eBPF 最方便的是按进程名(comm)过滤——容器的业务进程通常 comm 固定且不同于宿主。我编译了一个只做open/read的iohog,放容器里跑(comm=iohog),宿主机上按 comm 过滤统计它的系统调用:
root@ecs-a8bb-0002:~# docker run -d -v /root/iohog:/iohog --name bpf-ct alpine /iohogroot@ecs-a8bb-0002:~# bpftrace -e 'tracepoint:syscalls:sys_enter_openat /comm=="iohog"/{@open=count();}tracepoint:syscalls:sys_enter_read /comm=="iohog"/{@read=count();}' @open:5787@read:115987 秒内,容器内iohog进程触发了 5787 次openat、11598 次read——完全隔离于宿主其他进程。如果把探针换成sys_enter_openat打印str(args->filename),就能逐条列出容器打开了哪些文件;换成sys_enter_accept4 /comm=="nginx"/就是经典的nginx 容器连接接受观测。这就是生产里"看清一个容器在干啥"的标准手法。
关于 cgroup 过滤:更精确的做法是按 cgroup id 过滤(bpftrace 有
cgroup内建函数返回当前任务的 cgroup id)。本机 cgroup v2 目录无cgroup.id文件,但 cgroup id 等于该目录的 inode 号(stat -c %i /sys/fs/cgroup/.../docker-<id>.scope可取),用/ cgroup == <id> /即可精准锁定某容器。本实验用 comm 过滤已能干净隔离,故采用之。
四、BCC 与 bpftrace:怎么选
| bpftrace | BCC | |
|---|---|---|
| 形态 | 单行/短脚本,类 awk | Python + C,完整项目 |
| 上手 | 极快,改一行就能跑 | 需写 C 程序 + Python 胶水 |
| 能力 | 覆盖 90% 观测场景 | 任意复杂逻辑、自定义 Map、USDT、网络/XDP |
| 依赖 | 少(bpftrace + BTF) | 多(LLVM、Python、头文件) |
| 典型 | 临时排障、快速验证 | 常驻监控、定制化工具、产品化 |
实践建议:
- 临时排查、验证假设:用bpftrace 一行脚本,5 秒出结果。
- 要做成常驻监控 / 复杂关联(如按 cgroup 聚合所有容器的 TCP 重传):用BCC写工具,或基于 eBPF 的 Prometheus exporter(如
cilium/ebpf、bcc的TC/socket类)。 - 容器场景:bpftrace 在宿主机跑,配合
comm/cgroup/pid过滤观测任意容器,无需进容器。
五、排查思路:eBPF 在排障里的位置
- 先问"聚合后长啥样":延时分布(hist)、Top N(count)、速率(interval)这类问题,首选 eBPF/bpftrace。
- 容器观测范式:宿主机起 bpftrace → 用
comm/cgroup/pid过滤器圈定目标容器 → 选 tracepoint(syscalls/sched/block 等)或 kprobe → 用 Map 聚合。 - 与 perf/ftrace 联动:perf 定位"CPU 热点函数" → ftrace 看"单次调用链与耗时" → eBPF 看"海量事件的分布与按维度聚合"。三者构成完整的核观测栈。
- 注意环境限制:如本机
block:*tracepoint 不触发(半虚拟化存储)、部分 tracepoint 在容器视角的参数需str()解引用。遇到不触发就换同类探针(如块 I/O 改 VFS read),别卡死。
六、小结与思考题
小结:eBPF 是运行在内核态、经 Verifier 校验、JIT 执行的虚拟机,通过 Map 在"内核态聚合、用户态取数",比 ftrace 更适合统计分布与多维过滤。bpftrace 用一行脚本即可完成系统调用 Top、execsnoop、延时直方图、按 comm 过滤观测容器等任务。本环境实测block:*不触发,以 VFS read 延时直方图替代,技法一致。BCC 适合复杂/常驻场景,bpftrace 适合临时排障。
思考题:
- eBPF 的 Verifier 为什么要禁止"无界循环"?如果允许,会有什么后果?
- 上面 read 延时直方图里,为什么大部分在 0~4µs,却有 ms 级长尾?(提示:页缓存命中 vs 页错误/磁盘)
- 要统计"每个 cgroup 的 TCP 重传次数",bpftrace 该挂哪个探针、用什么 Map?
- 为什么用
comm过滤容器不如用cgroup过滤精确?什么情况下 comm 过滤会误伤或漏掉? block:block_rq_complete在本机恒为 0,这能说明"磁盘没有 I/O"吗?为什么?
(内核调试工具专题完。perf / ftrace / eBPF 三件套,分别对应"热点占比 / 调用链耗时 / 内核态聚合"三层观测能力,配合使用可覆盖绝大多数内核态排障场景。)