在可观测性的演进史上,扩展的伯克利包过滤器(eBPF,extended Berkeley Packet Filter)是近十年最具颠覆性的内核技术之一。它让我们可以在不修改一行业务代码、不重启任何进程的前提下,将自定义的、经过安全校验的小程序动态挂载到内核与用户态进程的执行路径上,采集系统调用、网络报文、调度事件、文件 I/O、块设备延迟、加密流量等几乎一切可被观察的事件。这种能力对传统监控体系是一次根本性的改写:应用程序不需要主动暴露埋点,运维和 SRE 也不需要打包内核模块,只要一条合适的 eBPF 程序就能让一台在生产环境跑了五年的机器回答”谁打开了这个文件”、“哪个容器的哪条 SQL 慢了”、“TLS 握手卡在哪一步”这类问题。
本篇是可观测性工程系列的第 14 篇,目标是从钩子机制出发,系统梳理 eBPF 可观测性的三条主流工程路径:字节码编译器集合(bcc,BPF Compiler Collection)、bpftrace 脚本语言、以及 libbpf 配合一次编译到处运行(CO-RE,Compile Once – Run Everywhere)的现代工程方案。在此之上再穿过 BPF 类型格式(BTF,BPF Type Format)、BPF 骨架(skeleton)、内核版本兼容矩阵、生产部署中的权能(capability)与验证器(verifier)坑点,直到 Pixie、DeepFlow、Grafana Beyla、Parca 等商业或开源产品的落地路径,最后给出面向国内生产环境的选型建议。阅读完本篇后,你应当能够独立判断:一个具体的可观测性需求,到底应该用 bpftrace 写两行一把梭、用 bcc 拉起一个 Python 脚本、还是沉到 libbpf + CO-RE 做一个可发布的二进制。
一、eBPF 的可观测性意义
在 eBPF 普及之前,Linux 生态里做深度可观测性的方法大致分三类,每一类都有显著的工程代价。
第一类是用户态插桩,典型做法包括动态链接预加载(LD_PRELOAD)、应用层 SDK 埋点、代理(Agent)注入字节码。这类方案的优势是语义清晰,可以精确拿到业务上下文;缺点是必须修改部署,重启进程,语言和运行时绑定严重,且看不到内核发生了什么。比如一个请求在内核 TCP 层丢包,在 sidecar 里什么也抓不到。
第二类是内核跟踪工具,如 strace、ltrace、perf、ftrace,甚至更早年的 SystemTap 和 DTrace。strace 基于进程跟踪(ptrace),对目标进程的性能影响是数量级的,典型场景下会让系统调用慢十倍以上;perf 和 ftrace 能力强,但输出需要后处理,不便直接做生产级别的聚合;SystemTap 要编译内核模块,意味着每次升级、每个发行版都要适配,在生产里几乎没人敢用。DTrace 在 Solaris 上一骑绝尘,但在 Linux 上始终因为许可和工程代价没有进入主流。
第三类是内核模块,直接在内核态写 C 代码。能力无上限,代价是风险无上限:一行空指针解引用就会导致宿主机崩溃,一个不小心的持锁会让整台机器软锁死(softlockup)。没有哪个 SRE 团队会允许把一个未经严格评审的第三方内核模块装到线上机器上。
eBPF 的出现,同时解决了这三条路线的核心痛点:
- 零侵入(Zero-instrumentation):绝大多数 eBPF 可观测性场景下,被观测的应用完全无感,不需要重新编译、不需要链接任何 SDK、不需要重启。这对已经跑在生产的遗留系统(legacy system)尤其重要。
- 内核级可见性(Kernel-level visibility):eBPF 程序可以挂载到内核函数、静态跟踪点(tracepoint)、系统调用、调度事件、网络驱动收发包、块设备 I/O 等路径。它看到的是内核视角的第一手数据,不需要从用户态反推。
- 低开销(Low overhead):eBPF 程序以字节码形式加载,经过即时编译(JIT,Just-In-Time compilation)翻译成本地指令执行,一次典型的 kprobe 命中开销在纳秒级(约 50–200ns),比 ptrace 路径低两到三个数量级。
- 安全(Safety):所有 eBPF 程序在加载时都要通过内核验证器(verifier)的静态分析,确保不会访问非法内存、不会死循环、不会越界。这让”在内核里跑第三方代码”第一次成为工程上可以接受的事情。
- 可编程性(Programmability):相比 ftrace、perf 这类”开关式”工具,eBPF 允许你在数据源端直接做过滤、聚合、哈希,而不是把所有原始事件吐到用户态再处理,这在高吞吐场景下是质变。
从历史上看,eBPF 的前身是 1992 年提出的经典 BPF(cBPF,classic BPF),最初仅用于 tcpdump 的过滤表达式编译。2014 年,Alexei Starovoitov 将其扩展为 eBPF 并合入 Linux 3.18,自此 BPF 从”内核包过滤器”演化为”内核可编程平面”。2016 年 XDP(eXpress Data Path)合入,BPF 开始承担高性能网络;2018 年 BTF 合入,为 CO-RE 铺路;2020 年前后 fentry/fexit、BPF 蹦床(trampoline)、LSM BPF 陆续进入主线。到本文成稿时(对应 Linux 6.x 主线),eBPF 已覆盖跟踪、网络、安全、调度等几乎所有内核子系统。
对可观测性工程而言,eBPF 的意义不仅是”多了一种工具”,而是让内核第一次真正成为可观测性数据平面的一等公民。过去我们习惯把应用层指标、日志、追踪视作三大支柱,但只要内核是黑盒,所有排障路径都会在某个节点遇到”不知道”。eBPF 把这层黑盒打开,且代价可控。
二、eBPF 的钩子类型与发展路线
eBPF 程序本身并不神奇,真正决定它能观察到什么的是挂载点(attach point / hook)。不同挂载点对应不同的程序类型(program type),每种程序类型有不同的上下文结构、能调用的内核辅助函数(helper)集合、稳定性保证和性能特征。对于可观测性场景,最常用的挂载点可以分为七大类。
2.1 kprobe / kretprobe
内核探针(kprobe)是 eBPF 可观测性中最朴素也最强大的机制。原理是在目标内核函数的入口处动态替换一条指令为 int3 断点(x86 上),命中断点时陷入异常处理程序,执行挂载的 eBPF 程序,然后返回原指令继续执行。kretprobe 类似,但挂在函数返回路径上,用于拿到返回值。
kprobe
的杀手级优势是几乎可以挂到任何内核符号上:tcp_connect、vfs_read、do_sys_openat2、ext4_file_write_iter
等等。只要这个符号没有被内联(inlined),kprobe
就能生效。缺点是:
- 不稳定:内核函数签名不是稳定应用程序二进制接口(ABI,Application Binary Interface),版本变了字段偏移就变了,需要 CO-RE 或手工适配。
- 开销相对较大:int3 断点涉及中断处理和上下文切换,单次命中约 100–300ns,高频函数上挂 kprobe 会显著拖慢系统。
- 无法挂到内联函数:现代内核在优化下内联了大量小函数,kprobe 会报”找不到符号”。
一个用 bpftrace 写的示例,跟踪所有 TCP 主动连接:
bpftrace -e 'kprobe:tcp_connect { printf("%s -> pid=%d\n", comm, pid); }'2.2 uprobe / uretprobe
用户态探针(uprobe)是 kprobe 在用户态的镜像,原理同样是 int3 替换。uprobe 可以挂到任何用户态二进制或共享库的任意符号、甚至任意偏移上。这使得我们可以在不重编应用的情况下,跟踪应用自己的函数、跟踪标准库函数、跟踪加密库函数。
一个典型用例是加密流量可观测性:HTTPS
流量在网卡上是密文,传统抓包无法看到 HTTP
请求内容,但如果我们在 OpenSSL 的 SSL_read /
SSL_write 上挂
uprobe,就能在加密前和解密后的明文缓冲区拿到完整 HTTP
报文。这正是 Pixie、DeepFlow 等商业化 eBPF 产品实现”零侵入
HTTPS 可观测”的底层机制。
bpftrace -e '
uprobe:/usr/lib/x86_64-linux-gnu/libssl.so.3:SSL_write {
printf("pid=%d comm=%s wrote %d bytes\n", pid, comm, arg2);
}'uprobe 的开销比 kprobe 更高(每次命中约 1–3μs),因为涉及用户态到内核态的往返,因此在热路径(如每次调用都命中的小函数)上要谨慎使用。
2.3 静态跟踪点(Tracepoints)
静态跟踪点是内核开发者预先在关键路径上埋下的稳定探针,格式为
subsystem:event_name,例如
sched:sched_switch(进程切换)、syscalls:sys_enter_read(read
系统调用入口)、block:block_rq_issue(块设备请求下发)、net:netif_receive_skb(网卡收包)。
相比 kprobe,tracepoint 的优势是:
- 稳定 ABI:tracepoint 是内核对外承诺的接口,跨版本字段布局基本不变,CO-RE 场景下可移植性最好。
- 语义清晰:每个 tracepoint 带有明确字段,不需要了解函数实现细节。
- 开销略低:没有 int3 断点,静态注册,开销略小于 kprobe。
查看当前内核支持的所有 tracepoint:
sudo ls /sys/kernel/debug/tracing/events/生产环境可观测性程序应优先选 tracepoint,只有在 tracepoint 无法覆盖时才退化到 kprobe。
2.4 USDT:用户态静态跟踪点
USDT(User Statically-Defined
Tracing)是用户态程序在源码里预先埋好的静态探针,最早由
DTrace 引入。应用通过 DTRACE_PROBE 宏(或
systemtap 的
STAP_PROBE)在关键路径上放置一条空指令(nop),eBPF
可以把这条 nop 替换为 int3,从而实现类似 tracepoint
的用户态稳定探针。
大量主流软件内置 USDT,包括:
- Python:
python:function__entry、python:function__return、python:gc__start。 - Node.js:
node:http__server__request、node:http__server__response。 - MySQL /
PostgreSQL:
mysql:query__start、postgresql:query__start。 - OpenJDK:
hotspot:method__entry、hotspot:gc__begin。
相比 uprobe 挂到 SSL_write
这种”第三方函数名”,USDT
是应用作者主动暴露的”稳定可观测接口”,可移植性好得多。
2.5 fentry / fexit(BPF 蹦床)
从 Linux 5.5 开始,内核引入了 BPF 蹦床(BPF trampoline)机制,对应的程序类型是 fentry(function entry)和 fexit(function exit)。与 kprobe 相比,fentry/fexit 的实现方式完全不同:它不依赖 int3 断点,而是通过 ftrace 的函数跟踪器(ftrace function tracer)直接在函数入口处跳转到一段生成的蹦床代码,蹦床调用 BPF 程序后再跳回原函数。
实测数据(Brendan Gregg 等人的基准测试)显示,fentry 的单次命中开销约为 kprobe 的 1/2 到 1/3(在约 30–80ns 量级),特别是在多个 BPF 程序挂到同一函数时,fentry 可以批量调用,效率进一步提升。此外,fexit 与 kretprobe 的一个关键区别是,fexit 能同时拿到入参和返回值,而不需要像 kretprobe 那样用 map 存入参、在返回时取。
SEC("fentry/tcp_connect")
int BPF_PROG(trace_tcp_connect, struct sock *sk) {
bpf_printk("tcp_connect: sk=%p\n", sk);
return 0;
}
SEC("fexit/tcp_connect")
int BPF_PROG(trace_tcp_connect_ret, struct sock *sk, int ret) {
bpf_printk("tcp_connect returned %d\n", ret);
return 0;
}建议:目标内核 ≥ 5.5 时,优先使用 fentry/fexit;需要兼容老内核时才用 kprobe/kretprobe。
2.6 XDP / TC / Socket Filter
网络方向的 eBPF 钩子不是本篇重点,但可观测性视角需要略作介绍。
- 快速数据路径(XDP,eXpress Data Path):挂载在网卡驱动层,收到数据包的瞬间就执行,用于超低延迟丢包、负载均衡、DDoS 缓解。对可观测性的价值是能在最早路径上采样或计数。
- 流量控制(TC,Traffic Control):挂在协议栈的 TC 子系统,介于驱动和协议栈之间,能看到完整的 sk_buff,适合做连接级别的统计。
- 套接字过滤器(Socket Filter):挂在单个 socket 上,能力较弱但对容器场景很友好。
性能上,XDP 处理 64 字节小包可以达到每秒千万级(10Mpps+),TC 次之,socket filter 最慢。可观测性用例里,XDP 通常用于全网抓包采样,TC 用于 L4 连接追踪,socket filter 用于按进程过滤。
2.7 LSM BPF 钩子
从 Linux 5.7 开始,eBPF 可以挂载到 Linux
安全模块(LSM,Linux Security Module)钩子上,如
file_open、socket_connect、bprm_check_security。这条路径主要面向安全场景(如运行时威胁检测),但对可观测性的意义是:LSM
钩子位于权限检查路径,能稳定地观察到”谁在访问什么资源”,比
kprobe 挂到 VFS 函数更规范。
Tetragon(Isovalent 开源)和 Falco(CNCF 毕业项目)都大量使用 LSM BPF 做运行时观测。
三、bcc(BPF Compiler Collection)
bcc 是 IO Visor 项目于 2015 年开源的 BPF 工具集,是 eBPF 在可观测性领域进入大众视野的第一代工程方案,也是 Brendan Gregg 著作《BPF Performance Tools》中绝大部分示例的基础。
3.1 bcc 的架构与工作流程
bcc 的核心设计是”Python 前端 + Clang/LLVM 后端 + 运行时内核头文件”:
- 用户编写一个 Python 脚本,其中内嵌 C 语言的 BPF 程序字符串。
- Python 侧的 bcc 库在运行时调用 Clang/LLVM,把 C 字符串编译为 BPF 字节码。
- 字节码通过
bpf()系统调用加载进内核,挂载到指定钩子。 - Python 侧通过 BPF 映射(BPF map)读取内核态采集的数据,做展示或聚合。
这个架构的核心问题是:Clang/LLVM
和内核头文件必须在目标机器上可用。在最初几年的 bcc
部署里,运维要在每台被观测机器上装几百兆的 LLVM 工具链和
kernel-devel / linux-headers
包,并且每次内核升级都要同步更新。这在大规模生产集群里是巨大的工程负担,也是后来
libbpf + CO-RE 要解决的核心痛点。
3.2 bcc
工具集:bcc-tools 开箱即用清单
bcc 项目附带数十个现成的诊断工具,位于
/usr/share/bcc/tools/
目录。以下是最常用的子集:
| 工具 | 功能 | 典型用法 |
|---|---|---|
execsnoop |
跟踪进程创建(exec) | 谁在偷偷跑 shell 脚本 |
opensnoop |
跟踪文件打开 | 哪个进程在读敏感文件 |
tcpconnect |
跟踪 TCP 主动连接 | 容器在连哪些外部地址 |
tcpaccept |
跟踪 TCP 被动接受 | 服务端连接情况 |
tcpretrans |
跟踪 TCP 重传 | 网络质量诊断 |
biolatency |
块设备 I/O 延迟直方图 | 磁盘慢请求定位 |
biosnoop |
块设备 I/O 逐事件跟踪 | 哪个进程在大量写盘 |
runqlat |
调度器运行队列延迟 | CPU 竞争分析 |
offcputime |
进程离 CPU 时间堆栈 | 阻塞在哪一步 |
profile |
基于采样的 CPU 性能剖析 | 火焰图原始数据 |
cachestat |
页缓存命中率 | 内存压力分析 |
funclatency |
任意函数耗时分布 | 热点函数定位 |
filetop |
按进程统计文件 I/O | I/O 大户排查 |
sslsniff |
OpenSSL/GnuTLS 明文抓取 | 加密流量可观测 |
这些工具绝大多数只需 root 权限即可运行,不需要重启任何服务,也不需要修改被观察进程。
3.3 一个真实的 bcc Python 脚本
下面是一个简化版的 HTTP 请求跟踪器,挂 kprobe 到
tcp_sendmsg,在 payload 里识别 HTTP
请求行:
#!/usr/bin/env python3
from bcc import BPF
from time import strftime
bpf_text = r"""
#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <bcc/proto.h>
struct event_t {
u32 pid;
u32 size;
char comm[16];
char payload[64];
};
BPF_PERF_OUTPUT(events);
int trace_tcp_sendmsg(struct pt_regs *ctx, struct sock *sk,
struct msghdr *msg, size_t size) {
if (size < 4) return 0;
struct event_t ev = {};
ev.pid = bpf_get_current_pid_tgid() >> 32;
ev.size = size;
bpf_get_current_comm(&ev.comm, sizeof(ev.comm));
struct iov_iter *iter = &msg->msg_iter;
const struct iovec *iov = iter->iov;
void *base = 0;
bpf_probe_read_kernel(&base, sizeof(base), &iov->iov_base);
bpf_probe_read_user(&ev.payload, sizeof(ev.payload), base);
// 简单判断是否 HTTP 请求
if (ev.payload[0] == 'G' && ev.payload[1] == 'E' && ev.payload[2] == 'T') {
events.perf_submit(ctx, &ev, sizeof(ev));
}
return 0;
}
"""
b = BPF(text=bpf_text)
b.attach_kprobe(event="tcp_sendmsg", fn_name="trace_tcp_sendmsg")
def handle(cpu, data, size):
ev = b["events"].event(data)
print("%s pid=%d comm=%s size=%d payload=%s" % (
strftime("%H:%M:%S"), ev.pid, ev.comm.decode(), ev.size,
bytes(ev.payload).split(b'\r\n')[0].decode('utf-8', 'replace')))
b["events"].open_perf_buffer(handle)
while True:
try: b.perf_buffer_poll()
except KeyboardInterrupt: break这个脚本展示了 bcc 的典型工作流:内嵌 C、Python 加载、perf buffer 回传、用户态解析。开发体验很不错,但部署到没有 LLVM 的机器上会直接报错。
3.4 bcc 的局限
- 运行时依赖 LLVM/Clang(数百 MB)。
- 运行时依赖
linux-headers或等价包。 - 启动慢:每次运行要花 1–3 秒编译。
- 内存占用高:LLVM 动辄吃 100MB+。
- 跨内核版本仍需内嵌版本判断,可移植性差。
这些问题在笔记本上做一次性排障时完全可以接受,但要把 bcc 工具嵌入生产 agent 就捉襟见肘了。libbpf + CO-RE 正是为此诞生。
四、bpftrace 脚本语言
如果说 bcc 是”给工程师用的 eBPF 开发框架”,bpftrace 就是”给 SRE 用的 eBPF 命令行”。bpftrace 由 Alastair Robertson 发起,现已成为 IO Visor 下与 bcc 并列的顶级项目,灵感直接来自 DTrace 的 D 语言。
4.1 bpftrace 的架构
bpftrace 的内部架构是”领域特定语言(DSL,Domain-Specific Language)→ LLVM IR → BPF 字节码”:
- 用户写一段 bpftrace 脚本或一行命令。
- bpftrace 前端解析脚本,输出 LLVM 中间表示(IR)。
- LLVM 后端把 IR 编译为 BPF 字节码并加载。
- bpftrace 运行时负责读 map、格式化输出、退出时自动打印聚合结果。
bpftrace 同样依赖 LLVM,但它对用户隐藏了所有 C 语言细节,上手门槛极低。
4.2 语法速览
bpftrace 的基本单元是探针块(probe block),格式为:
probe-type:target /predicate/ { action; }
例如:
bpftrace -e 'tracepoint:syscalls:sys_enter_openat /comm == "cat"/ {
printf("%s opened %s\n", comm, str(args->filename));
}'- 探针类型(probe
type):
kprobe、kretprobe、uprobe、tracepoint、usdt、profile、interval、software、hardware等。 - 谓词(predicate):可选的过滤条件,仅当为真时执行动作。
- 动作(action):C 风格表达式序列,以分号分隔。
4.3 内建变量
bpftrace 提供一组开箱即用的上下文变量:
| 变量 | 含义 |
|---|---|
pid |
当前进程 PID |
tid |
当前线程 TID |
uid |
当前用户 UID |
comm |
当前进程名(16 字节截断) |
cpu |
当前 CPU 编号 |
nsecs |
当前纳秒时间戳 |
kstack |
内核态调用栈 |
ustack |
用户态调用栈 |
args |
探针参数结构体(tracepoint 专用) |
arg0..arg9 |
探针位置参数(kprobe/uprobe) |
retval |
返回值(kretprobe/uretprobe/fexit) |
4.4 内建函数与聚合
bpftrace 的聚合语法是它的杀手锏:
| 聚合函数 | 作用 |
|---|---|
count() |
计数 |
sum(x) |
求和 |
avg(x) |
平均 |
min(x) / max(x) |
极值 |
hist(x) |
指数直方图(2 的幂) |
lhist(x, min, max, step) |
线性直方图 |
stats(x) |
聚合统计 |
聚合结果保存在映射(map)里,脚本退出时自动打印。
4.5 经典单行命令
文件打开跟踪(opensnoop):
bpftrace -e 'tracepoint:syscalls:sys_enter_openat {
printf("%-6d %-16s %s\n", pid, comm, str(args->filename));
}'调度器运行队列延迟(runqlat):
bpftrace -e '
tracepoint:sched:sched_wakeup { @qt[args->pid] = nsecs; }
tracepoint:sched:sched_wakeup_new{ @qt[args->pid] = nsecs; }
tracepoint:sched:sched_switch {
if (@qt[args->next_pid]) {
@us = hist((nsecs - @qt[args->next_pid]) / 1000);
delete(@qt[args->next_pid]);
}
}'块设备 I/O 追踪(biosnoop):
bpftrace -e '
tracepoint:block:block_rq_issue { @start[args->dev, args->sector] = nsecs; }
tracepoint:block:block_rq_complete {
$s = @start[args->dev, args->sector];
if ($s) {
printf("%-8d %-16s %d us\n", pid, comm, (nsecs - $s) / 1000);
delete(@start[args->dev, args->sector]);
}
}'TCP 连接跟踪(tcpconnect):
bpftrace -e '
kprobe:tcp_v4_connect {
@sk[tid] = arg0;
}
kretprobe:tcp_v4_connect /@sk[tid]/ {
$sk = (struct sock *)@sk[tid];
$dport = ((uint16)$sk->__sk_common.skc_dport);
printf("%s pid=%d dport=%d\n", comm, pid, ($dport >> 8) | (($dport & 0xff) << 8));
delete(@sk[tid]);
}'基于采样的 CPU 剖析(profile):
bpftrace -e 'profile:hz:99 { @[kstack] = count(); }'这一行就能以 99Hz 的频率采集内核栈,停止时自动输出每条栈路径的次数,直接可以扔进 FlameGraph 画火焰图。
4.6 bpftrace 与 bcc 的选择
经验法则:
- 一次性排障、Ad-hoc 问题定位:bpftrace。一行命令解决的事不要写脚本。
- 需要复杂逻辑、需要对接外部系统、要做成产品化工具:bcc 或 libbpf。
- 需要打包成 agent 分发到上万台机器:libbpf + CO-RE,不要选 bpftrace 也不要选 bcc。
五、libbpf + CO-RE(Compile Once – Run Everywhere)
libbpf 是内核团队维护的 C 语言 BPF 用户态库,位于
tools/lib/bpf/,也以独立项目
libbpf 发布。它是当前 eBPF
工程化的事实标准路径。
5.1 问题:bcc 的部署痛点
回顾 bcc 的架构:运行时编译依赖 Clang/LLVM 和内核头文件。这意味着:
- 发布包必须附带 LLVM,体积膨胀。
- 每台目标机器必须安装匹配的
linux-headers-$(uname -r)。 - 启动延迟高,不能做”加载后即用”的轻量 agent。
- 同一份 C 源码在不同内核版本下,因
struct字段偏移变化可能读错内存。
5.2 CO-RE 的核心思路
CO-RE 的目标是:一次在开发机上编译出 BPF 字节码,在任意内核版本上直接加载运行。它由三部分协同完成:
- BTF:内核把自身所有类型信息以紧凑格式嵌入
/sys/kernel/btf/vmlinux。 - CO-RE 重定位(relocation):Clang 编译
BPF
程序时,对字段访问生成特殊的重定位记录(
.BTF.ext段),记录”我要读struct task_struct的pid字段”这种语义信息,而不是写死的偏移。 - libbpf 运行时修正:加载时,libbpf
读取目标内核的 BTF,查到
task_struct.pid在当前内核的真实偏移,patch 进字节码,再提交给验证器。
这样即使内核版本变了、字段偏移变了、甚至字段换了位置,同一份字节码依然能正确工作。
5.3 BTF 简介
BTF 是一种极简的类型描述格式,比 DWARF 小两个数量级,专为
BPF 设计。一个现代 Linux 发行版(Ubuntu 20.10+、Fedora
31+、CentOS Stream 9+、RHEL 8.6+、openEuler
22.03+)都会默认开启
CONFIG_DEBUG_INFO_BTF=y,把 BTF
塞进内核镜像,运行时可以直接读:
ls -lh /sys/kernel/btf/vmlinux对于老内核(如 CentOS 7 的 3.10),需要借助
BTFGen 或第三方维护的 BTF 包(BTFHub
项目维护了绝大部分主流发行版的 BTF 生成结果),把 BTF
单独带进去。
5.4 libbpf API 速览
libbpf 的核心 API 围绕”对象(object)→ 程序(program)→ 链接(link)→ 映射(map)“四个概念:
struct bpf_object *obj = bpf_object__open_file("prog.bpf.o", NULL);
bpf_object__load(obj);
struct bpf_program *prog = bpf_object__find_program_by_name(obj, "handle_exec");
struct bpf_link *link = bpf_program__attach(prog);
struct bpf_map *map = bpf_object__find_map_by_name(obj, "events");5.5 BPF 骨架(skeleton)
手写这些 API
调用很繁琐。bpftool gen skeleton 能根据
.bpf.o 自动生成一个 C
头文件(.skel.h),把加载、挂载、读 map
全部封装成结构体成员调用:
#include "execsnoop.skel.h"
int main() {
struct execsnoop_bpf *skel = execsnoop_bpf__open_and_load();
execsnoop_bpf__attach(skel);
// 读 skel->maps.events
execsnoop_bpf__destroy(skel);
}5.6 一个完整的 libbpf + CO-RE 程序
内核侧(execsnoop.bpf.c):
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
char LICENSE[] SEC("license") = "GPL";
struct event {
u32 pid;
u32 ppid;
char comm[16];
char filename[128];
};
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_execve")
int handle_execve(struct trace_event_raw_sys_enter *ctx) {
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e) return 0;
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
e->pid = bpf_get_current_pid_tgid() >> 32;
e->ppid = BPF_CORE_READ(task, real_parent, tgid);
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_probe_read_user_str(&e->filename, sizeof(e->filename),
(const char *)ctx->args[0]);
bpf_ringbuf_submit(e, 0);
return 0;
}用户态侧(execsnoop.c):
#include <stdio.h>
#include <signal.h>
#include <bpf/libbpf.h>
#include "execsnoop.skel.h"
static volatile bool exiting;
static void sig_handler(int sig) { exiting = true; }
static int handle_event(void *ctx, void *data, size_t sz) {
const struct event *e = data;
printf("%-6d %-6d %-16s %s\n", e->pid, e->ppid, e->comm, e->filename);
return 0;
}
int main(int argc, char **argv) {
struct ring_buffer *rb = NULL;
struct execsnoop_bpf *skel;
int err;
signal(SIGINT, sig_handler);
skel = execsnoop_bpf__open_and_load();
if (!skel) return 1;
err = execsnoop_bpf__attach(skel);
if (err) goto cleanup;
rb = ring_buffer__new(bpf_map__fd(skel->maps.events),
handle_event, NULL, NULL);
if (!rb) goto cleanup;
printf("%-6s %-6s %-16s %s\n", "PID", "PPID", "COMM", "FILENAME");
while (!exiting) {
err = ring_buffer__poll(rb, 100 /* ms */);
if (err == -EINTR) { err = 0; break; }
if (err < 0) break;
}
cleanup:
ring_buffer__free(rb);
execsnoop_bpf__destroy(skel);
return 0;
}编译命令(在任意带 Clang 的开发机上执行一次即可):
clang -g -O2 -target bpf -D__TARGET_ARCH_x86 \
-I. -c execsnoop.bpf.c -o execsnoop.bpf.o
bpftool gen skeleton execsnoop.bpf.o > execsnoop.skel.h
clang -O2 execsnoop.c -lbpf -o execsnoop得到的 execsnoop 是一个约 1MB
的静态二进制,拷到任意支持 BTF 的内核上都能直接运行,不需要
Clang,不需要内核头文件,启动时间毫秒级。
5.7 vmlinux.h 生成
vmlinux.h 是从 BTF
反生成的”整合版内核头文件”,包含了你能用到的所有内核类型定义:
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h开发时用它代替一堆
#include <linux/...>,既方便又稳定。
六、BTF 与 BPF 骨架
上一节介绍了 CO-RE 的思想,这一节展开 BTF 的工程细节。
6.1 BTF 格式
BTF 是一种类型描述格式,由以下若干”类型条目”组成:
BTF_KIND_INT:基本整型。BTF_KIND_STRUCT/BTF_KIND_UNION:结构体、联合体。BTF_KIND_ARRAY:数组。BTF_KIND_ENUM:枚举。BTF_KIND_FUNC_PROTO/BTF_KIND_FUNC:函数签名。BTF_KIND_TYPEDEF:类型别名。
与 DWARF 相比,BTF 省略了所有行号、变量位置、表达式等调试信息,只保留”类型布局”,体积只有 DWARF 的 1%–3%。
6.2 BTF 生成链
- 内核编译时,
pahole(dwarves 项目)从 DWARF 调试信息抽取类型并转为 BTF。 - BTF 被嵌入 vmlinux 的
.BTF段。 - 运行时通过
/sys/kernel/btf/vmlinux暴露给用户态。 - 内核模块的 BTF 以
/sys/kernel/btf/<modname>暴露。
6.3 查看 BTF
bpftool btf dump file /sys/kernel/btf/vmlinux | grep 'STRUCT task_struct'
bpftool btf dump file /sys/kernel/btf/vmlinux format c | less6.4 骨架生成的内部细节
bpftool gen skeleton 输出的
.skel.h 文件本质上是:
- 把
.bpf.o的字节内容用__attribute__((section))内嵌到 C 文件里。 - 为每个 map、每个 prog 生成对应的成员指针。
- 提供
open、load、attach、destroy四个封装函数。
这让用户态代码不再需要 bpf_object__open_file
这类 API,加载目标 BPF 对象就像使用一个普通的 C 库。
七、生产部署:内核版本兼容性
eBPF 生态的工程现实是:哪个特性能用,取决于你集群里最老的那台机器的内核版本。下表列出了可观测性相关特性在主要内核版本中的可用性:
| 内核版本 | 发布时间 | 关键特性 | 生产定位 |
|---|---|---|---|
| 3.18 | 2014-12 | eBPF 初版,kprobe | 不推荐 |
| 4.1 | 2015-06 | BPF_PROG_TYPE_KPROBE | 不推荐 |
| 4.4 | 2016-01 | perf event output | 老系统最低线 |
| 4.14 | 2017-11 | LTS,basic BPF maps、socket filter | 兼容底线 |
| 4.18 | 2018-08 | BTF 最早版本 | 过渡 |
| 4.19 | 2018-10 | LTS,XDP 稳定 | 网络场景 |
| 5.1 | 2019-05 | BPF_PROG_TYPE_TRACING | fentry 基础 |
| 5.2 | 2019-07 | 百万指令上限(1M insns) | 复杂程序 |
| 5.4 | 2019-11 | LTS,bpf_d_path,fentry 部分 |
生产可用 |
| 5.5 | 2020-01 | fentry/fexit 完整 | 推荐起点 |
| 5.7 | 2020-05 | LSM BPF、ring buffer | 推荐 |
| 5.8 | 2020-08 | BTF-enabled maps、CAP_BPF 雏形 | 推荐 |
| 5.10 | 2020-12 | LTS,BPF 特性大量成熟 | 生产主流 |
| 5.11 | 2021-02 | task local storage | 可选 |
| 5.13 | 2021-06 | CAP_BPF 完整拆分 | 容器友好 |
| 5.15 | 2021-10 | LTS,BPF 生产最佳实践 | 强烈推荐 |
| 6.1 | 2022-12 | LTS,kfunc 广泛可用 | 未来 |
| 6.6 | 2023-10 | LTS,BPF open-coded iterators | 未来 |
7.1 推荐的最低内核版本
- 排障工具场景(bcc/bpftrace 随手跑):4.15+ 可用,4.18+ 体验好。
- agent 产品化(libbpf + CO-RE):强烈建议 ≥ 5.4,最好 ≥ 5.10。
- 新特性开发(fentry、ring buffer、LSM BPF):≥ 5.8,最好 ≥ 5.15。
- 安全观测场景(Tetragon 类):≥ 5.10。
7.2 CAP_BPF vs CAP_SYS_ADMIN
Linux 5.8 引入了独立的 CAP_BPF
权能(capability),5.13 完成拆分。在此之前,加载 BPF
程序需要 CAP_SYS_ADMIN,这是一种”近
root”权限,给容器授权风险极大。
5.13+ 之后,典型的 eBPF agent 容器只需要:
CAP_BPF:加载 BPF 程序、创建 map。CAP_PERFMON:读取 perf 事件,做 profile 和 tracing。CAP_NET_ADMIN:挂载网络类程序(XDP/TC)。
这远比 privileged: true 安全。
7.3 容器部署
在 Kubernetes 里部署 eBPF agent,典型的 DaemonSet 配置:
apiVersion: apps/v1
kind: DaemonSet
metadata: { name: ebpf-agent, namespace: obs }
spec:
selector: { matchLabels: { app: ebpf-agent } }
template:
metadata: { labels: { app: ebpf-agent } }
spec:
hostPID: true
hostNetwork: true
containers:
- name: agent
image: registry.example.com/ebpf-agent:v1.2.0
securityContext:
capabilities:
add: ["BPF", "PERFMON", "NET_ADMIN", "SYS_RESOURCE"]
privileged: false
volumeMounts:
- { name: sys, mountPath: /sys, readOnly: true }
- { name: debugfs, mountPath: /sys/kernel/debug }
- { name: btf, mountPath: /sys/kernel/btf, readOnly: true }
volumes:
- { name: sys, hostPath: { path: /sys } }
- { name: debugfs, hostPath: { path: /sys/kernel/debug } }
- { name: btf, hostPath: { path: /sys/kernel/btf } }几点关键:
hostPID: true:让 agent 能看到宿主机全部 PID,便于 resolve。hostNetwork: true:XDP/TC 程序需要挂在宿主机网络命名空间。- 挂载
/sys/kernel/btf:让 libbpf 能读 BTF 做 CO-RE 重定位。 - 避免
privileged: true,用细粒度 capability 替代。
7.4 红帽系(RHEL/CentOS)的 backport
红帽对 RHEL 有强烈的 ABI
稳定承诺,导致它会把大量新内核特性回溯(backport)到老版本号。比如
RHEL 8.6 标称 4.18,但实际 BPF 能力接近上游 5.4;RHEL 9.0
标称 5.14,能力接近上游 5.15。判断能力时不能只看
uname -r,应该直接测试某个 helper
或程序类型是否可用。
八、常用 eBPF Observability 程序
下表是生产环境最常用的 eBPF 可观测性程序清单,覆盖进程、文件、网络、存储、调度、CPU 六大方向:
| 程序 | 功能 | 最低内核 | 典型用法 |
|---|---|---|---|
execsnoop |
跟踪新进程 exec | 4.8 | 定位”谁在跑 shell” |
exitsnoop |
跟踪进程退出 | 4.9 | 诊断进程频繁重启 |
opensnoop |
跟踪文件打开 | 4.8 | 找敏感文件读写者 |
statsnoop |
跟踪 stat 调用 | 4.8 | 目录遍历风暴 |
biolatency |
块 I/O 延迟直方图 | 4.9 | 磁盘慢 |
biosnoop |
块 I/O 逐事件 | 4.9 | I/O 大户 |
ext4slower |
ext4 慢操作 | 4.9 | 文件系统级延迟 |
xfsslower |
xfs 慢操作 | 4.9 | 同上 |
tcpconnect |
TCP 主动连接 | 4.8 | 出向连接审计 |
tcpaccept |
TCP 被动接受 | 4.8 | 入向连接统计 |
tcpretrans |
TCP 重传 | 4.8 | 网络质量 |
tcplife |
TCP 连接生命周期 | 4.15 | 连接时长分析 |
profile |
CPU 采样剖析 | 4.9 | 火焰图 |
offcputime |
off-CPU 时间分析 | 4.9 | 阻塞诊断 |
runqlat |
运行队列延迟 | 4.9 | CPU 竞争 |
runqlen |
运行队列长度 | 4.9 | 调度压力 |
cpudist |
进程 on-CPU 时长分布 | 4.9 | 批量长任务 |
funclatency |
任意函数延迟 | 4.9 | 热点函数 |
funccount |
任意函数调用次数 | 4.9 | 调用压力 |
hardirqs |
硬中断统计 | 4.19 | 中断风暴 |
softirqs |
软中断统计 | 4.19 | 同上 |
cachestat |
页缓存命中率 | 4.9 | 内存压力 |
memleak |
内存泄漏检测 | 4.9 | 长期内存问题 |
sslsniff |
SSL/TLS 明文抓取 | 4.18 | 加密流量观测 |
tcpstates |
TCP 状态转换 | 4.15 | 连接异常 |
netqtop |
网卡队列 Top | 5.0 | 网络队列不均 |
llcstat |
LLC 命中率 | 4.9 | CPU 缓存分析 |
每一个都是一条命令就能跑起来的生产级工具。值得记忆:
sudo /usr/share/bcc/tools/execsnoop
sudo /usr/share/bcc/tools/biolatency 10 1
sudo /usr/share/bcc/tools/profile -F 99 30
sudo /usr/share/bcc/tools/tcpretrans
sudo /usr/share/bcc/tools/offcputime -df -p $(pgrep -n java) 30 > out.stacks九、商业化 eBPF 可观测性工具
eBPF 的工程化红利已经催生了一批商业化或产品化的可观测性工具。以下几款是当前最具代表性的方案。
9.1 Pixie(New Relic)
Pixie 是 2020 年开源、后被 New Relic 收购的 Kubernetes 原生可观测性平台,定位是”无需埋点的 K8s 应用可观测”。它的架构由三部分组成:
- Pixie 边缘模块(PEM,Pixie Edge Module):运行在每个节点的 DaemonSet,负责通过 eBPF 采集数据。
- Vizier:集群内的控制平面,负责调度采集、存储时序数据、执行脚本。
- 云端控制台(Cloud):可选的外部 UI 与 API 平面。
关键能力:
- 协议自动识别:通过在
SSL_read/SSL_write以及tcp_sendmsg/tcp_recvmsg上挂 uprobe/kprobe,自动解析 HTTP/1、HTTP/2、gRPC、DNS、MySQL、PostgreSQL、Redis、Cassandra、Kafka、NATS、Mongo 等十余种协议,提取请求响应字段。 - 集群内存储:默认数据不出集群,保留窗口为分钟到小时级。
- Pixie 脚本语言(PxL,Pixie Language):类 Python/PromQL 的 DSL,用户可以用 PxL 快速查询。
- Kubernetes 元数据自动关联:所有请求自动打上 pod、namespace、deployment 标签。
Pixie 的优势是真正零改造:给 K8s 集群装一个 DaemonSet,五分钟内就能看到所有服务间的 HTTP/gRPC 调用链,包括 TLS 加密的流量。局限是保留窗口较短,复杂分析需要接入 New Relic 云。
9.2 DeepFlow(云杉网络,国产开源)
DeepFlow 是云杉网络开源的面向云原生应用的可观测性产品,2022 年 CNCF 沙箱项目,国产 eBPF 可观测性的代表。架构由代理(agent)和服务端(server)组成:
- Agent:混合数据平面,eBPF + AF_PACKET + DPDK 多种采集方式并存,可以同时抓 L3–L7。
- Server:存储(基于 ClickHouse)、查询、API。
DeepFlow 的核心创新是:
- AutoTagging:自动把 K8s 元数据、Istio 元数据、云厂商标签注入每一条观测数据,打通拓扑。
- 全栈分布式追踪:无需埋点即可生成跨服务的调用链,解决了手工埋点覆盖率低的问题。
- eBPF + cBPF 双模:老内核上退化到 AF_PACKET,保证兼容性。
中文文档完善,社区活跃,国内电信运营商、银行、云厂商的采用案例较多。对内部安全审查严格、偏好自主可控的企业,DeepFlow 是第一优先级选项。
9.3 Grafana Beyla
Beyla 是 Grafana Labs 2023 年推出的 eBPF 自动插桩工具,定位明确:“把任何 HTTP/gRPC 应用变成带 RED 指标(Rate/Errors/Duration)和分布式追踪的服务,无需改代码”。架构极简:单个二进制或容器,挂到目标进程或 K8s 集群上。
核心能力:
- 自动协议识别:HTTP/1、HTTP/2、gRPC、SQL。
- 直接产出 RED 指标:以 OTLP(OpenTelemetry Protocol)或 Prometheus 格式导出,无缝接入 Grafana 栈。
- 分布式追踪:自动生成 span,传播 W3C Trace Context。
- 低代码配置:
print_traces: true
otel_metrics_export:
endpoint: http://otel-collector:4317
discovery:
services:
- name: my-service
open_ports: "8080"Beyla 是目前把 eBPF 零侵入和 OpenTelemetry 生态结合得最好的开源方案之一。
9.4 Parca
Parca 是 Polar Signals 开源的持续性能剖析(continuous
profiling)平台,核心是一个基于 eBPF 的采样剖析器
parca-agent,以 19Hz 或 99Hz
的频率采集所有进程的调用栈,并使用 DWARF 栈回溯(DWARF-based
stack unwinding)在没有帧指针(frame
pointer)的情况下仍能拿到准确栈信息。
这一点非常关键:现代发行版为了代码密度,默认不保留帧指针(-fomit-frame-pointer),传统
perf 采样在这种二进制上栈回溯会断。Parca 通过解析 ELF 的
.eh_frame/.debug_frame 段,在 eBPF
里实现 DWARF
状态机,能在不改编译选项的情况下拿到正确栈。
Parca 的输出格式对齐 pprof,正在与 OpenTelemetry Profiles 信号规范融合,未来会成为”可观测性第四支柱”的标准实现之一。
十、国内落地案例
国内大型互联网公司和云厂商是 eBPF 可观测性的重度使用者:
- 阿里巴巴内核团队:在 Alibaba Cloud Linux(龙蜥 / OpenAnolis 上游贡献者)中维护了大量 eBPF backport,把许多上游 5.x 特性带回 4.19 基线。阿里自研的 SysOM、SysAK 等诊断工具大量使用 eBPF,用于宿主机级别的诊断与性能剖析。
- 字节跳动:KernelShield 系统结合 eBPF 做容器运行时安全与可观测,实现宿主机级别的调用链追踪与异常行为检测;内部还有面向性能诊断的 eBPF 工具链,在数十万台服务器上常态化运行。
- 腾讯云:TKE、EKS 系列产品使用 eBPF 做容器网络可观测,结合 Cilium 和自研组件提供跨可用区、跨集群的网络诊断。
- 百度、美团:内部均有基于 libbpf + CO-RE 的自研 agent,用于持续性能剖析和网络可观测。
- 中国移动、中国电信:DeepFlow 在国内运营商 5G 核心网、IT 系统中有广泛落地,用于云网一体化的可观测性。
这些案例反映了一个事实:在国内生产环境做 eBPF,内核版本兼容性是最大障碍。CentOS 7(3.10)存量巨大,大规模升级不现实,因此国内厂商普遍走两条路:一是在自研内核(如 Alibaba Cloud Linux、TencentOS Server、openEuler)里 backport eBPF 特性;二是在 agent 里做能力降级,高版本内核用 fentry + ring buffer,低版本内核退化到 kprobe + perf buffer。
十一、工程坑点
以下是 eBPF 可观测性在生产里最常踩的坑,按严重程度排序。
11.1 内核版本与 backport 不一致
uname -r 报 4.18,但能否用 fentry/ring
buffer 取决于发行版是否回溯。RHEL 8/9、阿里云
Linux、TencentOS Server 都做了大量 backport。务必在 agent
启动时做能力探测(feature
probe),而不是只看版本号。libbpf 提供了
libbpf_probe_bpf_helper()、libbpf_probe_bpf_prog_type()
等 API。
11.2 验证器复杂度上限
验证器对程序复杂度有硬限制:
- Linux 5.2 之前:4096 条指令上限,任何稍复杂的程序都会卡。
- Linux 5.2+:百万指令上限(1M insns),但分支探索上限仍然存在。
- 程序中有循环时,必须用
bpf_loop辅助或#pragma unroll,否则会被拒。
典型症状是”在开发机上过了,在生产上报
BPF program is too large“,根因通常是内联不同、LLVM
优化级别不同。
11.3 栈回溯与帧指针
上文提到,现代发行版默认关闭帧指针。eBPF 采样拿到的
ustack 在这种进程上会只有 1–2
帧。解决方案:
- 应用侧重新编译时加
-fno-omit-frame-pointer(Ubuntu 24.04+ 已默认开启)。 - 使用支持 DWARF 栈回溯的剖析器(如 Parca、Polar Signals、字节跳动开源的 vArmor)。
- 接受栈不全,用调用关系聚合弥补。
11.4 容器内权限问题
早期内核(< 5.8)必须
CAP_SYS_ADMIN,在多租户集群里这是高风险授权。升级到
5.13+ 后可以用细粒度
CAP_BPF + CAP_PERFMON + CAP_NET_ADMIN
替代。Kubernetes PodSecurityStandard “restricted” profile
下,eBPF agent 必须用独立命名空间或专用 namespace 运行。
11.5 Map 与内存限制
BPF 映射默认受 RLIMIT_MEMLOCK 限制(Linux
5.11 前),生产 agent 通常需要设置:
struct rlimit rlim = { .rlim_cur = RLIM_INFINITY, .rlim_max = RLIM_INFINITY };
setrlimit(RLIMIT_MEMLOCK, &rlim);Linux 5.11 之后改为 cgroup memcg 统计,这个问题自动消失。
11.6 BTF 不可用
CentOS 7 / Ubuntu 18.04 等老内核没有内置 BTF。CO-RE 无法工作。解决方案:
- 使用 BTFHub 或 btfgen 生成”最小 BTF”(只含你程序用到的字段),随 agent 一起分发。
- 或针对这些机器退化到 bcc 模式,牺牲启动速度换兼容性。
11.7 热路径开销
高频函数上挂 eBPF 程序会放大开销。实测案例:
vfs_read上挂 kprobe,NVMe 小 I/O 吞吐下降约 5%–10%。tcp_sendmsg上挂 uprobe(到SSL_write)+ 简单记录,HTTPS QPS 下降约 3%–8%。sched:sched_switch上做 per-CPU 计数(无打印),开销 < 1%。
规则:尽量使用 tracepoint 和 fentry;尽量在内核态过滤;尽量用 per-CPU map 减少竞争。
11.8 ring buffer 与 perf buffer 选择
- Linux < 5.8:只有 perf buffer,需要 per-CPU 分配,内存浪费;排序困难。
- Linux 5.8+:ring buffer 提供全局 MPSC 队列,更省内存,事件天然有序。
- 生产建议:目标内核 ≥ 5.8 时一律用 ring buffer。
11.9 符号解析与 JIT 语言
剖析 Java、Node.js、Python 等 JIT 或动态语言时,eBPF 拿到的地址无法直接解析成符号。解决方案:
- Java:使用
perf-map-agent或async-profiler生成/tmp/perf-<pid>.map文件(agent 应读取 hostPath),然后 agent 解析。 - Node.js:开启
--perf-basic-prof。 - Python:用 USDT 或 py-spy 等工具提供符号。
11.10 与 cgroup v2 的交互
cgroup v2 下,bpf_get_current_cgroup_id()
返回的是统一 cgroup ID;v1 下每个控制器有自己的
ID,易混淆。容器内 ID 解析要用
bpf_get_current_ancestor_cgroup_id(),并用
bpffs 固定住 cgroup 引用。
十二、选型建议与落地清单
12.1 决策矩阵
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 生产 SRE 一次性排障 | bpftrace 单行命令 | 启动快,语义清晰 |
| 开发调试、短期工具 | bcc Python 脚本 | 生态丰富,教程多 |
| agent 产品化、发布到大规模集群 | libbpf + CO-RE | 零依赖、可移植、启动快 |
| Kubernetes 应用可观测 | Pixie / DeepFlow / Beyla | 零埋点、开箱即用 |
| 持续性能剖析 | Parca / Polar Signals | DWARF 栈回溯 |
| 网络深度可观测 | Cilium Hubble / DeepFlow | L3–L7 全栈 |
| 安全与合规观测 | Tetragon / Falco | LSM BPF 可靠 |
| 国内主权可控 | DeepFlow + 自研 libbpf agent | 可审计、可改造 |
12.2 生产就绪清单
在上线一个 eBPF 可观测性方案之前,建议按以下清单检查:
12.3 分阶段采纳路线
推荐的落地节奏分三阶段:
- 体验期(1–2 周):在预发环境装 bpftrace
和 bcc-tools,开始用
execsnoop、biolatency、tcpconnect、profile四个命令解决日常排障。这一步几乎零成本,SRE 能立刻受益。 - 工具化期(1–2 月):选 1–2 个开源产品试点,推荐 DeepFlow 或 Grafana Beyla。覆盖 1 个典型业务集群,对比有无 eBPF 可观测的排障效率。
- 自研或深度集成期(3–6 月):根据业务特点自研 libbpf + CO-RE agent,解决开源产品覆盖不到的特殊场景(如私有协议、特殊内核版本、内部 trace 上下文传播)。同时把数据平面接入统一可观测平台。
每一步都要有明确的成功指标:排障 MTTR 下降、线上 incident 识别提前时间、高价值指标覆盖率。避免一上来就 all-in 自研。
十二附、常见问答与工程补充
这一节汇总在培训、代码评审、线上事故复盘中反复被问到的 eBPF 可观测性问题,给出工程答案。
附.1 eBPF 程序真的可以做到”永不崩内核”吗
几乎可以,但不是绝对。验证器会拒绝所有它能静态证明为不安全的程序。已知的”验证器漏洞导致内核崩溃”CVE 在 Linux 5.x 早期有过几例(如 CVE-2021-3490、CVE-2022-23222),都与操作数边界计算的不完备有关。现代内核(≥ 5.15)加入了更严格的寄存器范围跟踪后基本稳定。风险点在于:
- 使用了非主线内核(backport 来源不明)。
- 使用了 unprivileged BPF(很多发行版已默认禁用)。
- 内核存在未公开漏洞。
生产建议:升级到受支持的 LTS 内核,禁用
kernel.unprivileged_bpf_disabled = 1
之外的放行配置,agent 代码走静态分析与复核流程。
附.2 eBPF 能替代 perf / ftrace 吗
互补而不是替代。
- perf 对硬件性能计数器(PMU)的支持仍然不可替代,CPU 采样的大多数场景都是”perf_event + eBPF”组合。
- ftrace 的函数图形化追踪(function graph tracer)在深度调试内核调用路径时仍然更直观。
- eBPF 的优势在于可编程、可聚合、可结构化输出,对运维 agent 和产品化工具更友好。
日常排障里三者共存,不需要二选一。
附.3 bpftrace 脚本怎么调试
几个实用技巧:
- 加
-d看 AST,-dd看 LLVM IR,-v看加载过程。 - 用
printf+ 简化探针逐步收敛。 - 验证器报错时,看
dmesg | tail,里面有完整的 verifier log。 - 复杂脚本先用
bpftrace -l列出可用探针,再拼装。
附.4 libbpf 的 C 程序怎么做单元测试
BPF 程序很难在传统意义上”单测”,因为它运行在内核里。业界常见做法:
- 用户态函数单测:把事件处理逻辑抽成普通 C 函数,在用户态用 gtest / Unity 测试。
- 集成测试:启动一个容器,在里面加载 BPF
程序,跑一段已知行为的负载(
curl、dd、strace),断言 ring buffer 输出。 - CI 用 KVM 多内核:用 GitHub Actions +
actions-ebpf之类项目,在多个内核版本上跑集成测试。
Cilium、Tetragon 都有这方面成熟的 CI 实践可以参考。
附.5 为什么我的 uprobe 没触发
常见原因:
- 目标函数被内联了(特别是
inline或 LTO 编译的库)。 - 目标函数被链接器去重(
--gc-sections)。 - 符号名被 C++ 修饰(mangled),需要用 mangled 名或
/usr/bin/mybin:_ZN...。 - 目标二进制有 PIE,地址偏移不同,但 libbpf 已处理,不是问题。
- 目标库在容器内路径与挂载路径不一致。
排查工具:readelf -Ws /path/to/lib | grep symbol、objdump -d。
附.6 ring buffer 丢事件怎么办
eBPF ring buffer
是单生产者多消费者(SPMC)结构,生产速度超过消费速度时会直接丢弃并返回
-E2BIG。应对策略:
- 加大 ring buffer 大小(建议 2–16MB)。
- 在内核侧用 per-CPU map 先聚合,减少事件数量。
- 用户态消费要用独立线程,
ring_buffer__poll循环不能被其他工作阻塞。 - 暴露一个”丢弃计数”指标到 Prometheus,线上一旦非零要告警。
附.7 BTF 不可用的降级方案
目标机器上没有 /sys/kernel/btf/vmlinux
时的选择:
- BTFHub:预生成了几乎所有主流发行版的
BTF 文件,直接下载
.btf放到 agent 目录。 - BTFGen:只为你的程序生成”最小 BTF”,文件大小几十 KB,可随 agent 分发。
- 双二进制:分别提供 CO-RE 版本和”老内核
bcc 版本”,根据
uname -r自动选择。
附.8 eBPF 数据如何接入 OpenTelemetry
几条主流路径:
- agent 侧直接产出 OTLP:Beyla 是典型例子。
- agent 产出私有格式 → 本地 Collector 转换 → OTLP 上行:DeepFlow、Pixie 属此类。
- OpenTelemetry eBPF Profiler(OTel 2024 投票通过将 elastic/otel-profiling-agent 并入):第四支柱 profiles 的参考实现。
对新项目,优先选方案 1 或 3,减少数据搬运。
附.9 eBPF 对 ARM64 支持如何
主流 ARM64(aarch64)服务器(鲲鹏、飞腾、Ampere Altra、AWS Graviton)对 eBPF 支持良好。几个要点:
- kprobe、tracepoint、fentry 与 x86 同步可用。
- XDP 对具体网卡驱动依赖较强,需要驱动支持 XDP native mode 才有最佳性能。
- 内核 5.10+ 在 ARM64 上对 BPF JIT 覆盖完整。
- 编译 CO-RE 程序时
-D__TARGET_ARCH_arm64。
国产化替代场景中,ARM64 上的 eBPF 可观测性已完全可用,龙蜥、openEuler 都做了广泛适配。
附.10 eBPF 与 WebAssembly 有什么关系
两者都是”带验证器的虚拟机字节码”,但定位完全不同:
- eBPF 运行在内核态,面向系统层可观测、网络、安全。
- Wasm 运行在用户态沙箱,面向跨平台业务逻辑。
2023 年起有一些尝试把 eBPF 程序编译成 Wasm 让其在用户态运行(如 wasm-bpf),但这属于跨界实验,不要把它当成 eBPF 的替代品。对可观测性工程师而言,只关心 eBPF 本身即可。
十二又、一个完整落地示例:Java 服务的 HTTP 可观测
以一个真实场景贯穿前面所有概念,落地一个”不改 Java 代码的 HTTP 可观测”方案。
场景
- 目标:对所有 Java HTTP 服务收集 RED(Request Rate / Error / Duration)指标,粒度到 URI。
- 约束:不改业务代码、不加 Java agent、不重启服务。
- 集群:Kubernetes 1.28,节点内核 5.15(Ubuntu 22.04 LTS)。
方案选型
候选有三:
- Pixie:覆盖完整,数据留在集群。
- Grafana Beyla:直接产 OTLP,对接现有 Grafana Stack。
- 自研 libbpf agent:完全可控,但开发成本最高。
假设我们已有 Grafana/Prometheus/Tempo,选方案 2。
关键配置
DaemonSet 部署 Beyla:
apiVersion: apps/v1
kind: DaemonSet
metadata: { name: beyla, namespace: obs }
spec:
selector: { matchLabels: { app: beyla } }
template:
metadata: { labels: { app: beyla } }
spec:
hostPID: true
serviceAccountName: beyla
containers:
- name: beyla
image: grafana/beyla:1.6.0
securityContext:
capabilities:
add: ["BPF", "PERFMON", "NET_ADMIN", "SYS_PTRACE", "CHECKPOINT_RESTORE"]
privileged: false
runAsUser: 0
env:
- { name: BEYLA_CONFIG_PATH, value: /config/beyla.yaml }
- { name: BEYLA_KUBE_METADATA_ENABLE, value: "true" }
volumeMounts:
- { name: config, mountPath: /config }
- { name: sys, mountPath: /sys, readOnly: true }
volumes:
- { name: config, configMap: { name: beyla-config } }
- { name: sys, hostPath: { path: /sys } }Beyla 配置:
attributes:
kubernetes:
enable: true
discovery:
services:
- k8s_namespace: prod
k8s_deployment_name: "^(order|payment|checkout)-.*"
otel_metrics_export:
endpoint: http://otel-collector.obs:4317
interval: 10s
otel_traces_export:
endpoint: http://otel-collector.obs:4317
sampler: { name: parentbased_traceidratio, arg: "0.1" }
routes:
unmatched: heuristic
patterns:
- /api/v1/orders/:id
- /api/v1/payments/:id/confirm工作原理
部署后,Beyla 会:
- 扫描节点上所有运行的进程,匹配 K8s 标签和可执行名。
- 识别 Java 进程的 libc
read/write与tcp_sendmsg/tcp_recvmsg,挂 uprobe/kprobe。 - 解析 HTTP 请求行,提取 method、path、status、耗时。
- 按路由归类(通过
routes.patterns归一化),生成http_server_request_duration_seconds_{bucket,count,sum}等 OTLP 指标。 - 通过 K8s API 把 pod/namespace/deployment 注入标签。
- 以 10s 间隔推送到 OTel Collector。
对 HTTPS 的处理
Beyla 会对 JVM 内置 SSL 通过 Java 层定位失败,因此建议 Java 使用 OpenSSL(通过 netty-tcnative 或 Wildfly OpenSSL);或退化到”仅看响应时间+状态码,不看 body”模式。
容量评估
实测数据(参考 Grafana 官方 benchmark,QPS 20k 的 Java 服务):
- Beyla CPU 占用约 0.3 核/节点。
- 内存 约 80MB/节点。
- 对业务 p99 延迟影响 < 3%。
- Prometheus 指标数量:每个 service/route 约 15 条。
上线检查单
这个例子刻画了 eBPF 可观测性”从选型到落地”的完整链路。它不是最强大的方案(自研 libbpf 更可控),也不是最省事的方案(托管 SaaS 更省事),但它在能控制性、部署成本、开源生态、国内可用性上达到了一个良好平衡,是多数企业起步阶段的合理选择。
十三、参考资料
- Linux Kernel Documentation, “BPF Documentation”, https://www.kernel.org/doc/html/latest/bpf/
- Brendan Gregg, BPF Performance Tools, Addison-Wesley, 2019.
- Cilium Project, “BPF and XDP Reference Guide”, https://docs.cilium.io/en/stable/bpf/
- bpftrace Authors, “bpftrace Reference Guide”, https://github.com/bpftrace/bpftrace/blob/master/docs/reference_guide.md
- Andrii Nakryiko, “BPF CO-RE (Compile Once – Run Everywhere)”, https://nakryiko.com/posts/bpf-portability-and-co-re/
- libbpf Project, https://github.com/libbpf/libbpf
- libbpf-bootstrap Project, https://github.com/libbpf/libbpf-bootstrap
- IOVisor BCC Project, https://github.com/iovisor/bcc
- Pixie Documentation, https://docs.px.dev/
- DeepFlow Documentation, https://deepflow.io/docs/zh/
- Grafana Beyla Documentation, https://grafana.com/docs/beyla/latest/
- Parca Documentation, https://www.parca.dev/docs/
- Isovalent Tetragon, https://tetragon.io/
- Falco Security, https://falco.org/
- BTFHub Project, https://github.com/aquasecurity/btfhub
- Alexei Starovoitov, “BPF as a universally applicable engine”, LWN, 2019.
- Arnaldo Carvalho de Melo, “pahole and BTF deduplication”, LPC 2020.
- Yonghong Song, “BPF Type Format (BTF)”, kernel.org, 2018.
- CNCF, “eBPF Landscape”, https://landscape.cncf.io/
- OpenAnolis Community, “Alibaba Cloud Linux eBPF Backports”, https://openanolis.cn/
上一篇:Events 与变更关联:CloudEvents、发布打点、K8s 事件
下一篇:网络可观测性:Cilium Hubble、Pixie、DeepFlow、Tetragon
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【可观测性工程】网络可观测性:Cilium Hubble、Pixie、DeepFlow、Tetragon
从 L3/L4/L7 三层观测视角出发,讲解 eBPF socket filter/tc/XDP 的数据采集机制,深入 Cilium Hubble 流日志与指标体系、Tetragon 安全可观测、Pixie 自动化协议解析、国产 DeepFlow 的架构与实践,以及 TLS 解密、HTTP/2 解析、服务拓扑自动发现等核心工程挑战。
持续性能分析(Continuous Profiling):Parca、Pyroscope、Grafana Beyla
深入剖析持续性能分析(Continuous Profiling)的原理、架构与落地实践,覆盖 Parca、Pyroscope、Grafana Beyla 三大主流方案,包含 eBPF 采样、符号解析、火焰图、差异分析以及字节跳动、美团的生产案例与工程坑点。
可观测性工程
从 Metrics、Logs、Traces 到 Profiling、eBPF、OpenTelemetry 与 SLO 治理,面向中国工程团队的可观测性系统化手册。
【可观测性工程】可观测性全景:Metrics、Logs、Traces、Profiles、Events 五大支柱
从控制论到云原生:拆解可观测性的五大信号支柱,对比监控与可观测性的本质区别,梳理开源/商业/SaaS 分类,以及国内互联网公司三大支柱落地现状与典型工程坑点。