Skip to content

eBPF 可观测性原理:无侵入监控实战,从 kernel trace 到生产落地

去年给公司线上 k8s 集群加可观测性时,遇到了一个诡异问题:Java 服务偶发性 GC 停顿超过 500ms,Prometheus 抓不到,Grafana 看不到根因。最后用 eBPF 的 funcslower 抓到了——是某个 JNI 调用阻塞了 JVM 线程,但 Prometheus 的 GC metric 根本覆盖不到这个场景。

这个问题如果用传统方式排查,至少要装 APM、改代码、重启服务。eBPF 的好处是零侵入——不需要改代码、不需要重启、不需要插桩,直接在内核层面看发生了什么。

eBPF 是什么:一句话解释清楚

eBPF(extended Berkeley Packet Filter)原本是用来过滤网络包的内核功能。后来被扩展成一个可以在内核里安全执行用户态编写的小程序的框架。

核心思想:在 Linux 内核里跑"安全沙箱",允许用户态程序在内核上下文里执行,但经过验证器(verifier)检查,确保不会崩溃内核或访问非法内存。

用户态程序(BPF 字节码)
    ↓  ebpf verifier 检查安全
内核态执行(挂载到钩子点)
    ↓  产出数据
ring buffer / perf event / map
    ↓  用户态读取
监控工具(BCC / bpftrace / 自研 agent)

关键点:verifier 是核心——它保证你的 eBPF 程序不会无限循环、不会访问内核未授权内存、不会导致内核崩溃。如果 verifier 过不了,程序根本加载不进去。

三种 Attach 方式:kprobe / uprobe / tracepoint

这是 eBPF 监控的三大入口,选对类型决定你能看到什么。

kprobe:内核函数级别

挂在内核函数入口或出口,能看到任何内核函数的调用。

# bpftrace 一行命令:跟踪 mkdir 系统调用的延迟分布
bpftrace -e 'kprobe:do_mkdir { @start[tid] = nsecs; }
             kretprobe:do_mkdir /@start[tid]/ {
  @latency = lshift(nsecs - @start[tid], 0);
  delete(@start[tid]);
}'

# 输出示例
# @latency:
# [1000, 2000)          3
# [2000, 4000)         15
# [4000, 8000)          8

kprobe 的限制:kernel symbol 可能被 kabi 改名,跨内核版本兼容性差。比如 do_mkdir 在 5.15+ 变成了 vfs_mkdir,脚本要跟着改。

uprobe:用户空间函数级别

挂在用户态进程的函数上,能看到应用程序内部行为。

# 挂到 Java 进程的某个函数(需要 PID)
bpftrace -e 'uprobe:/usr/bin/java:_ZNK6google7protobuf7Message10InitializationErrorStringEv {
  printf("Java function called by pid %d\n", pid);
}'

# 跟踪 Python 函数的执行时间
bpftrace -e 'uprobe:/usr/bin/python3.11:main { @start[pid] = nsecs; }
             uretprobe:/usr/bin/python3.11:main /@start[pid]/ {
  printf("python3 pid %d took %lld ns\n", pid, nsecs - @start[pid]);
  delete(@start[pid]);
}'

uprobe 的限制:需要知道目标二进制路径,且函数符号可能 stripped。生产环境常用 readelf 或 nm 先确认符号名。

tracepoint:预定义内核事件

tracepoint 是内核预先定义好的钩子,比 kprobe 稳定(不会因为内部实现改变而失效)。

# 列出所有网络相关的 tracepoint
sudo bpftrace -l 'tracepoint:*net*' | head -20

# 跟踪 TCP 连接建立
sudo bpftrace -e 'tracepoint:syscalls:sys_exit_connect {
  @estab[tid] = args->err;
}'

决策树: - 想看系统调用 → tracepoint:syscalls 优先,不行再 kprobe - 想看内核函数 → kprobe - 想看用户态函数 → uprobe - 不确定用哪种 → 先用 bpftrace -l 列出可用的 tracepoint,再决定

生产级工具链:BCC / bpftrace / Telegraf / Cilium

BCC:Python 脚本集合

BCC 提供几十种现成的监控脚本,是入门 eBPF 最快的路径。

# 安装 BCC(Ubuntu 22.04)
sudo apt install bpfcc-tools linux-headers-$(uname -r)

# 一键查看所有可用脚本
bcu list | head -30

# 经典脚本:tcplife - 展示 TCP 连接的生存时间和字节数
sudo tcplife -l

# 经典脚本:execsnoop - 跟踪新进程启动
sudo execsnoop -T

# 经典脚本:biosnoop - 块设备 I/O 延迟
sudo biosnoop -T

个人判断:BCC 脚本适合快速排查问题,但生产监控不建议直接用 BCC 脚本——它们是调试工具,不是监控系统。生产环境应该用 Telegraf 或自建 agent。

bpftrace:一行命令的瑞士军刀

bpftrace 语法类似 awk + C,学习曲线比 BCC 脚本陡,但灵活性高得多。

# 安装
sudo apt install bpftrace

# 监控 open 系统调用的文件路径分布
sudo bpftrace -e 'tracepoint:syscalls:sys_exit_openat {
  printf("%s %s\n", comm, str(args->filename));
}'

# 监控 TCP 重传(网络问题的神器)
sudo bpftrace -e 'kprobe:tcp_retransmit_skb {
  @retrans[key(arg0, arg0->sk)] = count();
}'

实操经验:bpftrace -e 'hist(args->size)' 可以画出 open 系统调用参数大小的分布——这是我发现一个大文件拷贝导致磁盘 I/O 尖峰的快捷方式。

Telegraf with eBPF:生产级方案

Telegraf 有官方 eBPF plugin,支持开箱即用的系统指标采集。

# telegraf.conf
[[outputs.prometheus_client]]
  listen = ":9273"

[[inputs.ebpf]]
  programs = [
    { name = "syscall", paths = ["tracepoint:syscalls:sys_exit_read", "tracepoint:syscalls:sys_exit_write"] },
    { name = "network", paths = ["tracepoint:net:net_dev_rx"] },
  ]
telegraf --config telegraf.conf --test

我的建议:Telegraf eBPF plugin 适合基础指标(system call 计数、网络统计),但复杂场景(如追踪特定应用的调用链)还是 bpftrace 更灵活。

Cilium Hubble:K8s 网络可观测性的天花板

Cilium 自带 Hubble,用 eBPF 做 K8s 网络层的完全可观测性——流量矩阵、DNS 查询、连接状态,都不需要改业务代码。

# 查看 Hubble 拓扑
hubble observe --follow

# 查看 DNS 查询
hubble observe --type dns

# 查看连接状态变化
hubble events --type connection

Hubble 的核心价值:在 K8s 里看东西南北向流量,不需要 service mesh,不需要 sidecar,eBPF 直接搞定。如果你的集群用了 Cilium,这是必装组件。

性能影响:eBPF 不是零开销

很多人误以为 eBPF 零开销,这是个误区。高频 tracepoint 会烧 CPU,必须配合采样策略。

三个真实踩坑案例

案例一:采样率设太高,CPU 飙到 40%

# ❌ 错误:每个系统调用都打印,CPU 直接爆
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat {
  printf("%s open %s\n", comm, str(args->filename));
}'

# ✅ 正确:每 100 次采样一次
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat {
  @[comm] = count();
  if (@[comm] % 100 == 0) {
    printf("%s open %s\n", comm, str(args->filename));
  }
}'

教训:生产环境不要直接跑 bpftrace -e 'print(...)',一定要配采样或聚合。

案例二:map 溢出导致程序被 verifier 拒绝

# ❌ 错误:map 大小设太小,频繁报错
bpftrace -e 'kprobe:do_sys_open {
  @a[tid] = args->dfd;  # 每个 tid 一条,map 爆了
}'

# ✅ 正确:用 LRU map 限制条目数
bpftrace -e 'kprobe:do_sys_open {
  @a[lru] = args->dfd;  # 自动淘汰最旧条目
}'

教训:eBPF map 有大小限制(通常 4MB),高频场景要用 LRU hash 或 percpu array。

案例三:BTF 缺失导致 kfunc 加载失败

# 报错信息
Error: failed to apply BTF map: Invalid argument
Error: no BTF found for kernel
# 检查内核是否支持 BTF
ls /sys/kernel/btf/vmlinux && echo "BTF OK" || echo "BTF missing"

# 修复(Ubuntu 22.04 通常已有)
sudo apt install linux-modules-extra-$(uname -r)
sudo modprobe btf

教训:某些定制内核(如云厂商的优化内核)会去掉 BTF 支持,此时只能用 tracepoint,不能用 kprobe/kfunc。生产前务必先检查。

权限与部署:容器里怎么用 eBPF

eBPF 加载需要 CAP_SYS_ADMIN,这在容器里默认没有。三种方案:

# 方案一:privileged 容器(最简单但最危险)
securityContext:
  privileged: true

# 方案二:只加需要的能力(推荐)
securityContext:
  capabilities:
    add: ["SYS_ADMIN", "BPF", "PERFMON"]
    drop: ["ALL"]
seccompProfile:
  type: Unconfined

# 方案三:hostNetwork + hostPID(Cilium Hubble 推荐)
hostNetwork: true
hostPID: true

我的判断:能跑在节点上就别放容器里。eBPF 的核心优势是"看到容器看不到的东西",如果把 eBPF 本身关在容器里,反而丧失了最大价值。

FAQ

Q:eBPF 能监控什么?有什么限制?

能监控的范围非常广:系统调用、网络包、函数调用、磁盘 I/O、内存分配、信号、定时器……几乎内核做的任何事。

主要限制: - 内核版本 ≥ 4.9(XDP 等高级功能需要 4.18+) - 需要 BTF(5.4+ 默认开启,旧内核需要手动安装) - 不能监控已经被内核移除的功能(如某些旧的 tracepoint) - XDP 等高级功能需要网卡驱动支持

Q:eBPF 和 APM 工具(如 Jaeger、SkyWalking)有什么区别?

APM 工具需要在代码里插桩(SDK、agent),要改代码或至少部署 sidecar。eBPF 完全零侵入,内核层面就能看到所有进程的系统调用和网络行为。

适用场景:APM 适合追踪业务逻辑链路(请求从哪个入口来、走了哪些服务);eBPF 适合系统层诊断(为什么慢、哪里卡了、资源瓶颈在哪)。两者互补,不冲突。

Q:eBPF 程序出问题了会搞挂内核吗?

不会。eBPF verifier 在加载前会静态分析程序,确保: - 程序会在有限步数内终止(无无限循环) - 不会访问未授权内存 - 不会修改不属于自己的数据结构 - map 操作在边界内

如果 verifier 过不了,程序根本加载不进去。历史上出现过极少数 verifier bypass 漏洞(CVE-2021-3490 等),但都有补丁修复。生产环境保持内核更新即可。