5.12 eBPF 基础——现代 Linux 可观测性

预计阅读时间:14 分钟

📖 目录

学习目标

学完本章后,你将能够:

  • 理解 eBPF 的核心架构(JIT 编译、Verifier 验证、BPF maps)
  • 区分 kprobe、uprobe、tracepoint、USDT 四种探针类型与适用场景
  • 使用 bcc 工具集(execsnoop、biolatency、tcplife)做日常排障
  • 使用 bpftrace 编写一行流 eBPF 程序做深度诊断
  • 了解 Cilium 和 Tetragon 在企业级网络与安全中的角色

核心知识

  • eBPF(extended Berkeley Packet Filter)——在内核中运行沙箱化程序的框架,无需修改内核源码或加载内核模块
  • JIT(Just-In-Time)编译器——将 eBPF 字节码转换为本地机器码,运行速度接近原生内核代码
  • Verifier(验证器)——静态分析 eBPF 程序,拒绝无限循环、越界访问、未初始化变量等不安全行为
  • BPF maps——eBPF 程序与用户空间共享数据的键值存储结构,支持数组、哈希表、LRU 等类型
  • 探针类型——kprobe(内核函数入口/返回)、uprobe(用户态函数)、tracepoint(内核静态跟踪点)、USDT(应用预埋跟踪点)
  • bcc(BPF Compiler Collection)——将 C 写的 eBPF 代码嵌入 Python/Lua 脚本的编译加载框架
  • bpftrace——类似 awk 语法的高阶 eBPF 脚本语言,一行即可写出探针程序
  • Cilium / Tetragon——基于 eBPF 的 K8s CNI 网络插件与运行时安全监控平台

知识关联

  • 前置知识2.4:进程管理 进程管理(理解进程与系统调用关系)、4.5:网络故障排查 网络故障排查(理解网络栈层次)
  • 后续影响:eBPF 是容器网络(6.2:Kubernetes 入门 K8s 网络)、运行时安全、深度性能调优(4.6:Linux 性能调优)的基础技术
  • 配套技术:系统调用跟踪(strace)、性能分析(perf)、容器网络(CNI)
  • 内核版本要求:eBPF 基础功能需 Linux 4.1+;BPF maps 需 4.4+;CO-RE/BTF 需 5.4+;fentry/fexit 需 5.5+。生产环境建议内核 ≥ 5.4

原理讲解

eBPF 程序生命周期

一个 eBPF 程序从编写到加载挂载的流程如下:

  1. 编写——用 C 语言编写 eBPF 内核态代码(限制:无循环、无 libc、有限栈大小)
  2. 编译——由 LLVM/Clang 编译为 eBPF 字节码(ELF 目标文件)
  3. 验证——加载时内核 Verifier 静态分析:控制流图确保有穷终止、模拟执行检查越界、类型检查参数
  4. JIT 编译——通过通过验证后,内核 JIT 编译器将字节码转为当前 CPU 架构的本地机器码
  5. 挂载——将 eBPF 程序附加到指定事件源(kprobe、tracepoint、XDP 等)
  6. 交互——eBPF 程序通过 BPF maps 与用户空间通信(读/写/更新键值数据)
  7. 卸载——程序不再使用时从内核卸载,释放资源

Verifier 的工作机制

Verifier 是 eBPF 安全性的核心保障。它以 DAG(有向无环图)方式遍历程序的每条执行路径,确保:

  • 有穷终止——拒绝任何含有向后跳转的循环(除非有界循环被 LLVM 展开)
  • 内存安全——每个内存访问都模拟检查指针范围和类型,禁止越界
  • 栈深度限制——最大栈空间 512 字节,不允许递归调用
  • 辅助函数白名单——eBPF 程序只能调用内核预定义的 bpf_* 辅助函数

探针类型对比

探针类型语法前缀跟踪目标稳定性典型用例
kprobekprobe:func内核函数入口调试内核函数调用参数
kretprobekretprobe:func内核函数返回获取函数返回值、测量内核函数耗时
uprobeuprobe:bin:func用户态函数入口跟踪应用层函数如 malloc、SSL_read
tracepointtracepoint:syscalls:*内核静态跟踪点跟踪系统调用、调度器事件(生产环境首选)
USDTusdt:bin:provider:name应用预埋跟踪点跟踪 PostgreSQL/Node.js 等应用内建事件

BPF maps 数据交互

BPF maps 是 eBPF 程序与用户空间之间的桥梁。内核态的 eBPF 程序写入 maps,用户态程序通过 bpf() 系统调用读取。常用 map 类型:

Map 类型描述典型用法
BPF_MAP_TYPE_HASH哈希表(键值对)统计计数、跟踪连接状态
BPF_MAP_TYPE_ARRAY固定大小数组CPU 计数器、per-CPU 统计
BPF_MAP_TYPE_PERF_EVENT_ARRAY性能事件环形缓冲区将事件逐条推送到用户空间
BPF_MAP_TYPE_LRU_HASHLRU 淘汰哈希表跟踪活跃连接、自动淘汰旧条目
BPF_MAP_TYPE_STACK_TRACE栈回溯采集调用栈(配合 perf 事件)

eBPF 程序类型与挂载点

除了探针(probe),eBPF 程序还能挂载到网络和追踪的关键位置。程序类型决定了它能调用哪些辅助函数、访问哪些上下文:

程序类型挂载点典型用途
BPF_PROG_TYPE_XDP网卡驱动最早入口DDoS 过滤、负载均衡(性能最高的包处理位置)
BPF_PROG_TYPE_SCHED_CLSTC(流量控制)钩子容器网络策略、限速
BPF_PROG_TYPE_KPROBE内核函数入口/返回函数级追踪
BPF_PROG_TYPE_TRACEPOINT内核静态跟踪点生产可观测性(稳定 ABI)
BPF_PROG_TYPE_CGROUP_SKBcgroup 网络钩子容器级网络隔离/统计
BPF_PROG_TYPE_LSMLSM 安全钩子内核安全策略(如阻止特定 syscall)
BPF_PROG_TYPE_TRACINGfentry/fexit低开销函数级追踪(内核 5.5+)

网络路径上 XDP 在收包最早期执行(驱动层、没有 skb 分配),性能比 TC 高一个数量级;XDP 放行/丢弃后包不进内核协议栈,常用于抗 DDoS 的丢包场景。Cilium 正是把 eBPF 挂在 TC 钩子上实现 Pod 网络策略,替代 iptables 链式遍历。

BTF 与 CO-RE

早期 eBPF 程序依赖内核头文件编译,换个内核版本就得重新编译(BPF 程序与内核强耦合)。BTF(BPF Type Format)把内核的类型信息以紧凑格式暴露给用户空间,CO-RE(Compile Once, Run Everywhere)让同一份编译产物在不同内核版本上直接加载——这是 eBPF 能被大规模分发到异构集群的前提。内核 5.4+ 默认开启 CONFIG_DEBUG_INFO_BTF,检查方法:

# 确认 BTF 支持
test -f /sys/kernel/btf/vmlinux && echo "BTF enabled" || echo "BTF missing"
# 输出: BTF enabled

# bpftool 查看 BTF 信息
bpftool btf dump file /sys/kernel/btf/vmlinux | head -5

# 为当前内核生成 vmlinux.h(写 CO-RE 程序时使用)
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

示例代码

1. 检查内核 eBPF 支持

# 查看内核是否开启了 eBPF 相关配置
cat /boot/config-$(uname -r) | grep BPF
# 输出:
# CONFIG_BPF=y
# CONFIG_BPF_SYSCALL=y
# CONFIG_BPF_JIT=y

# 查看已加载的 BPF 程序
bpftool prog list

# 查看 BPF 网络程序(XDP/TC)
bpftool net list

2. bcc 常用工具实战

# 安装 bcc 工具集
apt install -y bpfcc-tools
# 所有工具以 *-bpfcc 命名

# 追踪新进程创建(排查周期性高 CPU)
execsnoop-bpfcc
# 输出:
# PID   COMM             RET ARGS
# 1234  logrotate         0  /usr/sbin/logrotate /etc/logrotate.conf

# 追踪磁盘 I/O 延迟分布
biolatency-bpfcc
# 输出: 直方图显示 I/O 延迟分布区间

# 追踪 TCP 连接生命周期
tcplife-bpfcc
# 输出: 进程、源/目标 IP、端口、持续时间、收发字节数

# 追踪 OOM Killer
oomkill-bpfcc
# 输出: 谁触发了 OOM、kill 了哪个进程、分数多少

# 追踪慢文件 I/O(>10ms)
fileslower-bpfcc 10

3. bpftrace 一行流

# 安装 bpftrace
apt install -y bpftrace

# 统计每个进程的 read() 系统调用次数
bpftrace -e 'kprobe:sys_read { @[comm] = count(); }'

# 追踪 open 操作并打印文件名
bpftrace -e 'kprobe:do_sys_open { printf("%s: %s\n", comm, str(arg1)); }'

# 只追踪 >4KB 的大文件读取
bpftrace -e 'kprobe:vfs_read /arg2 > 4096/ { @[comm] = sum(arg2); }'

# 追踪新进程创建
bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s -> %s\n", comm, str(args->filename)); }'

# 实时统计磁盘 I/O 按进程
bpftrace -e 'kprobe:submit_bio { @[comm] = count(); }'

# 查看可用探针列表
bpftrace -l 'tracepoint:syscalls:*'
bpftrace -l 'kprobe:tcp_*'

# 查看探针参数字段
bpftrace -lv 'tracepoint:syscalls:sys_enter_openat'
# 输出: filename, flags, mode

4. Cilium 与 Tetragon 部署

# Cilium — 基于 eBPF 的 Kubernetes CNI
# 替代 kube-proxy iptables,性能提升明显
helm repo add cilium https://helm.cilium.io/
helm install cilium cilium/cilium --namespace kube-system

# Tetragon — 基于 eBPF 的运行时安全监控
# 检测容器内新二进制执行、敏感文件读取、异常网络连接
# 无需修改应用代码即可获得安全可见性

5. bpftrace 生产排查示例集

# 场景一:CPU 突然飙高,找出谁在疯狂 fork 进程
bpftrace -e 'tracepoint:syscalls:sys_enter_fork { @[comm] = count(); }'
# 输出: @[java]: 12031, @[sh]: 342, ...

# 场景二:磁盘 IO 暴涨,定位读写进程与文件
bpftrace -e 'kprobe:blk_start_plug { @[comm] = count(); }'
bpftrace -e 'kprobe:vfs_read { @[comm, str(arg1)] = count(); }'

# 场景三:文件被谁删了(open 时打印调用栈)
bpftrace -e 'kprobe:do_unlinkat { printf("%s unlink %s\n", comm, str(arg1)); }'

# 场景四:网络重传排查(TCP 重传触发源)
bpftrace -e 'kprobe:tcp_retransmit_skb { @[comm, ntop(2, arg2)] = count(); }'

# 场景五:统计进程创建耗时分布(exec 延迟)
bpftrace -e 'kprobe:execve { @start[tid] = nsecs; } kretprobe:execve /@start[tid]/ { @ms = hist((nsecs - @start[tid]) / 1000000); delete(@start[tid]); }'

# 场景六:系统调用延迟直方图(按进程名分组)
bpftrace -e 'kprobe:do_sys_openat { @start[tid] = nsecs; } kretprobe:do_sys_openat { @ns[comm] = hist(nsecs - @start[tid]); delete(@start[tid]); }'

# 场景七:信号追踪(谁给谁发了什么信号)
bpftrace -e 'tracepoint:signal:signal_generate { printf("%s -> %s signal %d\n", comm, args->target, args->sig); }'

6. 用 Python + bcc 编写自定义工具

#!/usr/bin/env python3
# trace-opens.py —— 追踪指定进程的 openat 系统调用
from bcc import BPF

bpf_text = """
#include <uapi/linux/ptrace.h>
int trace_openat(struct pt_regs *ctx, const char __user *filename) {
    bpf_trace_printk("open: %s\\n", filename);
    return 0;
}
"""

bpf = BPF(text=bpf_text)
bpf.attach_kprobe(event="do_sys_openat", fn_name="trace_openat")
print("追踪 openat 调用,Ctrl+C 退出...")
bpf.trace_print()

# 运行:
# python3 trace-opens.py
# 输出: bash-1234 [000] .... 123.456789: open: /etc/passwd
#!/usr/bin/env python3
# vfs-latency.py —— 按进程统计 VFS 读写耗时
from bcc import BPF

bpf_text = """
#include <uapi/linux/ptrace.h>
BPF_HASH(start, u32);
BPF_HISTOGRAM(lat, u32);

int entry(struct pt_regs *ctx) {
    u32 pid = bpf_get_current_pid_tgid();
    start.update(&pid, &bpf_ktime_get_ns());
    return 0;
}

int exit(struct pt_regs *ctx) {
    u32 pid = bpf_get_current_pid_tgid();
    u64 *t = start.lookup(&pid);
    if (t) {
        u64 delta = bpf_ktime_get_ns() - *t;
        lat.increment(bpf_log2l(delta / 1000));
        start.delete(&pid);
    }
    return 0;
}
"""

bpf = BPF(text=bpf_text)
bpf.attach_kprobe(event="vfs_read", fn_name="entry")
bpf.attach_kretprobe(event="vfs_read", fn_name="exit")
bpf["lat"].print_log2_hist("VFS read latency (us)")

# 输出: 直方图显示各延迟区间的调用次数

常见错误

错误表现根因正确做法
Failed to load BTF / libbpf: BTF is required内核未启用 BTF(BPF Type Format),缺少类型信息升级内核到 5.4+ 并确认 CONFIG_DEBUG_INFO_BTF=y
permission denied 加载 BPF 程序非 root 用户,或 cgroup BPF 权限不足以 root 执行,或为容器设置 CAP_BPFCAP_SYS_ADMIN
invalid func unknown#1 verifier 报错eBPF 程序调用了不被允许的辅助函数只使用 bpf_* 官方辅助函数,检查程序类型是否匹配
BPF program is too largeeBPF 指令数超过内核上限(4096 或 100 万条)简化逻辑,拆分程序,或升级到 5.2+ 内核(放宽到 100 万条)
unreachable instruction verifier 报错代码中存在不可达路径(如死循环后的指令)确保程序是无环 DAG,有界循环用 #pragma unroll 展开
func #1 ptr R1=... access off=... size=... 越界BPF maps 或栈访问越界检查数组索引范围、栈空间不超过 512 字节
bpftrace 输出全是 lost events事件速率超过 perf ring buffer 消费能力减少打印内容、在内核态预聚合(map 计数替代 printf),或增大 buffer
kprobe 挂载后报 symbol not found函数被内联或在不同内核版本中改名/删除bpftrace -l 'kprobe:*func*' 查找实际符号名,或改用 tracepoint/fentry
容器内无法运行 bcc/bpftrace容器缺少 CAP_BPF/CAP_SYS_ADMIN 或访问不了 /sys/kernel/btfprivileged 模式或给容器添加 capability;BPF 工具一般建议在宿主机运行
bcc 工具输出乱码/数字地址缺少内核符号表(kallsyms 未导出)或 kptr_restrict 开启确认 /proc/kallsyms 可读、sysctl kernel.kptr_restrict=0(安全环境慎用)

最佳实践

实践原理示例
生产环境优先用 tracepointtracepoint 是内核承诺稳定的 ABI,kprobe 的函数名可能因版本变化tracepoint:syscalls:* 替代 kprobe:sys_read
使用 bcc 工具替代手写 eBPFbcc 封装了编译+加载+map 交互样板代码,开箱即用execsnoop-bpfcc 替代手写 eBPF + C 加载器
bpftrace 用于快速诊断一行流脚本适合 ad-hoc 排查,不适合生产长期运行bpftrace -e 'kprobe:do_sys_open { printf("%s\n", str(arg1)); }'
限制 eBPF 程序复杂度Verifier 对指令数和栈空间有严格限制,复杂逻辑应放用户空间内核态只做采集和过滤,聚合计算在用户态完成
利用 BPF maps 做数据降噪在内核态做预聚合可以减少 perf event 环形缓冲区压力用 per-CPU hash map 做计数器,避免逐条上报
内核 >= 5.4 以获取完整特性BPF CO-RE(一次编译到处运行)从 5.4 开始稳定使用 bpftool gen skeleton 生成 BTF 驱动的加载骨架
先 bpftrace -l 再写探针内核版本不同,符号名与参数会有差异写一行式前先 bpftrace -l / -lv 确认探针存在与字段名
追踪脚本设置明确的过滤条件全量追踪(所有 PID、所有文件)日志量巨大追加过滤器:/comm == "nginx"//pid == 1234//arg2 > 4096/
长期监控用 bcc 程序而非 bpftrace 一行流一行流无持久化与告警能力,适合 ad-hoc 排查生产监控落地为 bcc/Python 工具 + Prometheus 指标或定期任务
关注 Verifier 报错的第一个条目Verifier 报错是路径敏感的,只修后面报错会原地打转按报错顺序逐个修复;复杂逻辑尽量移入用户空间

练习题

  1. (概念)eBPF Verifier 主要检查程序的哪三类安全性问题?
  2. (概念)tracepoint 和 kprobe 的区别是什么?生产环境为什么更推荐 tracepoint?
  3. (实操)安装 bcc 工具集,使用 execsnoop-bpfcc 观察当你运行 ls 命令时触发的事件。
  4. (实操)用 bpftrace 写一行命令,统计系统上所有进程调用 write 系统调用的次数(按进程名分组)。
  5. (实操)使用 tcplife-bpfcc 观察当前系统的 TCP 连接,找到一条从本机到外部服务的连接记录并解释各字段。
  6. (🔍 挑战)在一个 K8s 集群上部署 Cilium 替换 CNI,用 cilium connectivity test 验证网络连通性,对比替换前后 kubectl exec 跨节点通信延迟的变化。
点击查看答案
  1. 三类安全性检查:① 有穷终止(拒绝无限循环);② 内存安全(指针范围验证、禁止越界);③ 栈深度限制(512 字节,禁止递归)。
  2. tracepoint 是内核预定义稳定接口,参数和含义随内核版本契约固定,适合生产环境。kprobe 可挂载绝大多数内核函数(未被内联且未列入黑名单的函数),但无稳定 ABI,内核升级可能断裂。
  3. execsnoop-bpfcc 在另一个终端执行 ls 后,输出显示 ls 进程的 PID、PPID、命令行参数和返回码。
  4. bpftrace -e 'kprobe:sys_write { @[comm] = count(); }' 按进程名统计 write 调用次数,按 Ctrl+C 输出汇总表。
  5. tcplife-bpfcc 输出每行包含 LADDR/LPORT/RADDR/RPORT/SENT/RECV 等字段。可通过对比 ss -tnp 确认连接存在。
  6. Cilium 安装后 cilium connectivity test 跑通过 pod-to-pod 和 pod-to-service 测试。eBPF 替代 iptables 后延迟通常降低 30-50%。

学习检查点

学完本章后,请检验自己是否掌握以下内容:

检查项自测问题验证方法
概念理解能用自己的话解释 eBPF 的沙箱执行模型和 BPF maps 机制尝试向他人讲解
命令操作能不查文档完成 bpftrace 脚本编写和 bpftool 使用在终端实际执行
原理掌握能说出 eBPF 程序的加载、验证和挂载流程画出流程图
故障排查能独立排查 eBPF 程序加载失败或 verifier 拒绝的问题模拟故障并修复
最佳实践能说明为什么 eBPF 是可观测性和网络性能调优的理想选择对比不同方案

本章总结

eBPF 是 Linux 内核近十年最重要的创新之一,它通过安全的内核沙箱化执行框架,重新定义了操作系统可观测性。核心流程为:C 编写 → LLVM 编译为字节码 → Verifier 安全验证 → JIT 编译为本地码 → 挂载到事件源 → 通过 BPF maps 与用户空间交互。

速查路线

  • 日常排错 → bcc 工具(execsnoop、biolatency、tcplife)
  • 深度诊断 → bpftrace 一行流脚本
  • 企业部署 → Cilium(网络)+ Tetragon(安全)

速查表

命令/工具用途
execsnoop-bpfcc追踪新进程创建
biolatency-bpfcc磁盘 I/O 延迟分布直方图
tcplife-bpfccTCP 连接生命周期追踪
oomkill-bpfccOOM Killer 触发追踪
fileslower-bpfcc <ms>追踪慢文件 I/O
bpftrace -e '...'一行流 eBPF 程序
bpftrace -l 'tracepoint:*'列出可用 tracepoint
bpftrace -lv '<probe>'查看探针参数字段
bpftool prog list列出已加载的 BPF 程序

学习路径建议

延伸阅读

常见问题

eBPF 和内核模块有什么区别?
eBPF(extended Berkeley Packet Filter)在安全的内核虚拟机中运行用户提供的代码,通过验证器(verifier)严格检查确保不崩溃内核、不无限循环。内核模块是加载到内核的二进制代码,任何 bug 都可能导致内核崩溃。eBPF 可以动态加载和卸载,无需重启系统。性能工具首选 eBPF 而非内核模块。
bcc 和 bpftrace 分别适合什么场景?
bcc(BPF Compiler Collection)提供 Python/Lua 库编写 eBPF 程序,需要编译工具链,适合编写复杂工具。bpftrace 是高层 CLI 工具(类似 awk for eBPF),一行命令做动态追踪。快速排障用 bpftrace:bpftrace -e "kprobe:do_sys_open { printf(output); }"。写持久化监控工具用 bcc。
eBPF 在 Cilium 中扮演什么角色?
Cilium 利用 eBPF 实现 Kubernetes 网络、安全和可观测性。eBPF 替换了传统的 kube-proxy(iptables 模式),大幅提升服务路由性能。Cilium NetworkPolicy 使用 eBPF 在 L3-L7 做细粒度策略执行。Hubble(Cilium 的可观测层)通过 eBPF 采集网络流量和 DNS 数据。eBPF + Cilium 正在成为 K8s 网络的新标准。
↑ 回到顶部