资讯动态

eBPF内核可观测性实战:从bpftrace到生产环境排障

发布时间:2026/10/5 17:10:36 来源:尧图企业网站定制
最近几年只要聊到 Linux 内核可观测性几乎绕不开 eBPF 这个词。无论你是搞网络、搞安全、搞性能调优还是纯粹在做云原生基础设施都会发现 eBPF 正在悄悄改变我们和内核打交道的方式。我自己第一次真正被 eBPF 震撼到是在一次生产环境故障排查里——那个问题用传统的 strace、perf、tcpdump 轮番上阵都看不到根因最后是靠一个几十行的 bpftrace 脚本直接在内核态抓到了关键事件。那种感觉就像一直在门外透过猫眼往里看突然拿到了钥匙直接走进房间。这篇文章我想从一个实际使用者的角度把 eBPF 的核心机制、工具链选型、真实应用场景和踩过的坑讲清楚。不会堆砌太多内核源码尽量用说人话的方式带你把 eBPF 这块硬骨头嚼碎。适合谁看如果你对 Linux 内核有一定基础但一直没搞懂 eBPF 到底是什么或者你已经在用 bcc / bpftrace 但想知道底层怎么工作的又或者你正打算在自己的项目里引入 eBPF 做可观测性这篇文章都能给你一个从原理到实战的完整视角。1. 传统内核观测手段的短板为什么 eBPF 会出现1.1 还在用 /proc、strace、tcpdump 的痛在 eBPF 出现之前我们做内核态观测基本就三板斧读 /proc 文件、用 strace 跟踪系统调用、用 tcpdump 抓网络包。这三样东西单独看都很好用但放到真实的生产排障场景里各自的局限就非常明显了。/proc 这种方式本质上是内核主动把一些统计信息暴露给用户态你看到的是内核想让你看到的东西而不是你想看的东西。比如你想知道某个进程为什么频繁唤醒/proc 里没有这种细粒度信息。strace 看着强大但它走的是 ptrace 机制每一次系统调用都要经过用户态和内核态的来回切换性能开销大得吓人。在高并发的生产环境上跑 strace那个进程的性能会直接被拖垮相当于给赛车的轮子捆上沙袋。tcpdump 的问题更隐蔽。它底层用的是 libpcap在传统实现里数据包需要先从网卡复制到内核缓冲区再从内核缓冲区复制到用户态缓冲区中间还有可能触发软中断。在万兆甚至更高带宽的链路上这种复制带来的 CPU 开销和延迟是很多人不能接受的。更关键的是你只能看到包进包出看不到包为什么被丢、谁改写了包头、socket 缓冲区里到底发生了什么。1.2 想改内核又没有胆量那有人问了既然用户态工具不够用我直接把代码写进内核不就行了理论上这是最彻底的方案——在内核里加一个 proc 文件、加一个 tracepoint、甚至直接改动某个子系统想要什么观测能力就自己写。但现实是内核代码的合入门槛非常高。Linux 内核的 review 机制极其严格哪怕是一个小小的 tracepoint 改动也要过 maintainer 的层层把关。一次提交从发送 patch 到最终合入主线短则几个星期长则几个月甚至一两年。就算你不想合入主线只想在自己的服务器上改那也意味着你要维护一个私有内核分支。内核的编译时间动不动就是几十分钟遇到安全补丁还要重新 merge、重新编译、重新滚动升级运维成本直接翻倍。更麻烦的是改出来的行为如果没经过充分测试可能就是一场灾难。我记得业内有个笑谈在内核里加代码就像在飞机飞行途中换引擎——不是不行但你要有必死的觉悟。1.3 内核模块看着自由实则戴着镣铐在内核模块Loadable Kernel Module, LKM出现之后很多人觉得找到了捷径不需要改主线内核只需要加载一个 .ko 文件就行。但内核模块的问题在于权限太大了。模块代码运行在内核态拥有最高权限任何一个指针错误直接就是 kernel panic连 oops 都可能是轻的。而且模块和具体内核版本强绑定换个内核版本就要重新编译一遍模块维护成本一点没少。更麻烦的是内核模块几乎不受任何安全约束。一个写得不严谨的模块可能覆盖了别人的数据结构、劫持了函数指针、甚至是污染了内核的全局状态。这也是为什么很多生产环境明确禁止加载非官方内核模块——审计风险和安全风险都太高了。传统手段各有各的不足那有没有一种方式既能深入内核内部看细节又不需要改动内核源码还能保证运行时的安全性这正是 eBPF 登场的核心原因。它不是简单地改良了某个工具而是从机制层面提供了一套安全地把代码送进内核执行的通用框架。你写的观测逻辑可以直接挂在内核的任意探针点上但每一个指令都必须先通过内核验证器的安全检查就像给每个进内核的访客都装上了强制安检门。2. eBPF 核心机制拆解虚拟机、验证器与钩子2.1 从 BPF 到 eBPF 的演进eBPF 的全称是 Extended Berkeley Packet Filter它的前身 BPF 最初只是为了高效过滤网络数据包设计目标非常朴素在内核态做一个简单的指令虚拟机对每一个到达的数据包跑一段过滤程序决定放行还是丢弃。经典的 tcpdump 过滤表达式最终都会被编译成 BPF 指令。2014 年前后内核社区对 BPF 做了一次全面升级这就是 eBPF。和老的 cBPF 相比eBPF 把指令集从 2 条扩展到了 11 条 64 位指令引入了寄存器数量更多、更接近真实机器的虚拟机模型还加入了 BPF Map 这种和用户态共享数据的机制。最重要的一点是eBPF 程序不再局限于网络包过滤它可以挂载到内核里几乎任何感兴趣的事件点上函数入口、函数返回、tracepoint、kprobe、perf event甚至网络报文经过的 TC 钩子和 XDP 钩子。用一句话概括eBPF 把内核变成了一台可以安全执行用户自定义字节码的可编程机器而可观测性只是它的第一个大规模落地场景。2.2 钩子机制决定你从哪里切入内核eBPF 程序要发挥作用第一步是选择一个挂载点也就是钩子。钩子的选择很大程度上决定了你看到的是什么维度的数据。kprobe / kretprobe动态探针可以挂在内核函数的入口和返回处。比如你想知道每次调用tcp_v4_connect时传入的目标地址用 kprobe 挂上就行。kprobe 的灵活度最高但它依赖内核函数符号内核版本升级后函数名变了程序就失效了。tracepoint静态探针由内核开发者主动埋点。它比 kprobe 稳定得多因为 tracepoint 是一等公民不会随便改。缺点是覆盖范围有限内核没埋点的地方你无处下手。perf_event用于性能事件比如 CPU 周期、缓存命中率、内存访问延迟等适合做 profiling。XDP / TC挂在网络数据路径上XDP 在网卡驱动收到的包的最早阶段执行适合做高性能包处理TC 挂在协议栈的入口和出口适合做流量整形和观测。选钩子的原则其实就一条优先用 tracepoint因为稳定tracepoint 覆盖不到再用 kprobe如果是网络场景考虑 XDP 和 TC 这种专项钩子。我见过不少新手一上来就全用 kprobe结果升了个内核版本脚本全挂这就是没想清楚钩子稳定性的代价。2.3 验证器eBPF 安全的守门人eBPF 允许普通用户加载代码进内核这听起来非常危险。凭什么信任这段代码答案是验证器verifier。验证器是整个 eBPF 机制里最复杂的组件它会对每一个加载进来的字节码做静态分析和模拟执行确保它不会做任何危险的事。验证器主要检查这几类问题指令是否合法操作码、寄存器使用是否符合规范是否有越界访问对栈、Map、数据包缓冲区的访问必须边界可控是否会死循环eBPF 程序不允许无限循环早期的验证器甚至要求程序必须有确定的退出路径空指针、未初始化变量、类型不匹配这些在内核模块里可能导致崩溃的问题在 eBPF 里一律禁止验证器的检查机制直观理解就是它真的会模拟走一遍你的程序把每一步的寄存器状态、指针类型、可能的值范围都记录下来。比如你从数据包里读取了一个字段作为数组下标验证器会尝试所有可能的值任何一个分支越界程序直接被拒。所以 eBPF 程序的编写风格和普通 C 程序很不一样。你不能依赖运行时检查兜底必须在代码里给验证器提供足够的边界信息。写过 eBPF 的人都有体会代码逻辑本身不难难的是怎么让验证器满意。对 Map 的大小做动态判断、对指针做复杂的条件分支都容易撞上验证器的限制这时候往往需要通过调整代码逻辑来哄好它。2.4 BPF Map用户态与内核态的数据桥梁eBPF 程序跑在内核态但它产生的数据最终要给用户态的程序用。这个数据交换的通道就是 BPF Map。Map 本质上是一组在内核内存里维护的键值存储结构支持哈希表、数组、环形缓冲区、栈等不同形态。Map 的设计很有意思。它不只是简单地把数据从内核复制到用户态更像是一块双方都能读写、通过文件描述符访问的共享内存。内核态的 eBPF 程序可以往 Map 里写数据用户态的程序可以通过bpf_map_lookup_elem这类系统调用去读。两者之间还可以建立事件通知机制比如 map 有新数据时用户态通过perf_event或者 ring buffer 立刻收到消息。在实际的可观测性程序里常见的玩法是eBPF 程序采集事件把聚合结果写进 Map用户态程序周期性地从 Map 里读取统计值做展示或告警。还有更高级的玩法Map 可以把多个 eBPF 程序串联起来实现类似程序协作的效果比如一个程序负责过滤另一个程序负责处理。2.5 一个生活化的类比如果你想快速给身边的朋友解释 eBPF 是什么可以这样讲传统的内核就像一栋管理森严的大楼你只能在楼外透过窗户往里面看/proc或者在门口登记进出记录strace想知道楼里某个房间具体发生了什么几乎不可能。eBPF 相当于给你发了张临时通行证你可以在大楼里装各种传感器但每一个传感器的安装位置、工作方式、数据采集范围都必须经过严格审批验证器并且传感器只能通过固定的接口Map往外传数据。这个类比虽然简单但能抓住三个要点深入性能进内核内部、安全性审批机制、可编程性传感器由你自定义。3. 可观测性实战从工具链选型到 Hello World3.1 bcc、bpftrace、libbpf 怎么选搞清楚原理之后下一步就是上手。目前主流的 eBPF 开发方式有三条路bcc、bpftrace 和 libbpf。很多初学者在这三者的选择上会很纠结我的建议是先甭管底层差异按场景来选。bpftrace 是写一次性观测脚本的首选。它的语法像 awk还支持类似 printf 的输出格式把所有 eBPF 的复杂性封装得干干净净。几十行脚本就能跟踪进程系统调用、统计函数耗时、聚合 IO 延迟简直是为排查问题量身定制。bcc 是一个 Python / C 框架它把 eBPF 程序的加载、编译、数据采集封装成了 Python 接口。如果你想做稍微复杂一点的观测工具比如写一个持续运行的后台监控程序bcc 会比 bpftrace 灵活得多。bcc 的问题在于运行时依赖 Python 环境而且每个目标机上都要安装完整的 bcc 工具集部署起来比较笨重。libbpf 是最底层的方式适合做生产级工具的开发。配合 CO-RECompile Once, Run Everywhere机制可以在开发机上编译出的 eBPF 程序直接拿到其他机器上跑不依赖目标机的内核头文件和编译工具链。这解决了 eBPF 早期版本最头疼的可移植性问题。代价是你需要手写 C 代码并且自己处理加载、Map、perf event 这些细节。三条路可以同时学。日常排障用 bpftrace 提效率做小工具用 bcc 快开发做严肃的基础设施再用 libbpf。3.2 环境检查与依赖准备上手之前先确认你的内核支持 eBPF。最直接的方式就是跑一个 bpftrace 示例脚本能跑起来就说明环境基本没问题。如果不想先装可以用一条命令检查内核编译选项zcat /proc/config.gz | grep BPF如果输出里有CONFIG_BPFy、CONFIG_BPF_SYSCALLy、CONFIG_BPF_JITy那基础支持就有了。如果想用 CO-RE 和 BTF还要确认/sys/kernel/btf/vmlinux这个文件存在。内核版本方面4.x 能跑基本的 eBPF但想要完整的可观测性和 BTF 支持我建议至少 5.x 以上越新体验越好。安装工具看你用的发行版。Ubuntu 和 Debian 系sudo apt install bpftraceRHEL / CentOS 系sudo dnf install bpftracebcc 的安装也类似sudo apt install bpfcc-tools装完之后直接跑一下 bpftrace --version确认命令有效就行。3.3 用 bpftrace 数分钟跑通第一个脚本我个人非常推荐用 bpftrace 作为第一个 eBPF 上手脚本。比如统计系统中每秒的系统调用次数sudo bpftrace -e tracepoint:raw_syscalls:sys_enter { [comm] count(); }这个脚本的意思是在每次进入系统调用的 tracepoint 上触发一段程序按进程名计数bpf 运行时每秒输出一次计数结果。你会在终端上看到类似这样的输出Attaching 1 probe... [chrome]: 1203 [node]: 88 [sshd]: 12看到这个输出的那一刻你实际已经在用 eBPF 做内核级观测了。它底层做的事是将一个极小的 eBPF 程序挂载到内核的 syscall 入口 tracepoint每次系统调用都执行一次哈希表计数数据通过 BPF Map 传到用户态并打印出来。整个过程没有加载任何内核模块没有任何代码进入内核主线你只是利用内核提供的安全钩子做了一次观测。再举一个更实用的例子统计每个进程打开文件描述符的分布sudo bpftrace -e tracepoint:syscalls:sys_enter_openat { [pid, comm] count(); }指出的是bpftrace 的符号代表一个全局 map 变量count()是聚合函数语法虽然像 awk但背后的语义完全是 eBPF 的写法和执行流程。如果你能理解这个脚本的执行流程就已经跨过了 eBPF 入门的门槛。3.4 一个完整的 eBPF C 程序结构示例如果你打算走 libbpf 路线这里的入门例子可以保存下来。下面是一个标准的hello world级 eBPF 程序用来统计进程的 exec 调用#include linux/bpf.h #include bpf/bpf_helpers.h struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 1024); __type(key, u32); __type(value, u64); } counts SEC(.maps); SEC(tracepoint/syscalls/sys_enter_execve) int on_execve(void *ctx) { u32 pid bpf_get_current_pid_tgid() 32; u64 *val bpf_map_lookup_elem(counts, pid); if (val) __sync_fetch_and_add(val, 1); return 0; } char _license[] SEC(license) GPL;这段代码干了三件事定义一个名为 counts 的哈希 Map定义一个挂载在 execve tracepoint 上对处理器在处理器中获取当前进程 pid 并给对应计数加一。这里有几个写 eBPF 程序时必须养成的习惯严格使用bpf_helpers.h里的辅助函数不要直接访问任意内存所有全局变量和函数都要用SEC宏声明所在的 section加载器靠 section 名识别程序类型Map 的定义是用户态和内核态共享的契约字段类型必须两边一致返回值语义要清楚对 tracepoint 程序来说返回 0 表示正常这段代码编译成字节码之后由加载器比如 libbpf 的 skeleton 机制加载进内核验证器在加载时会对它做完整的检查。整个加载流程可以用下面的伪代码理解open BPF object - load programs into kernel - attach to hooks - 读取 Map 数据4. 现实世界的 eBPF 应用版图4.1 网络领域Cilium 与云原生数据路径eBPF 在云原生领域的最大成功案例之一就是 Cilium。Cilium 把 eBPF 用在 Kubernetes 集群的网络数据路径上实现了网络策略、负载均衡、可观测性的一体化。传统 K8s 网络方案里Service 的负载均衡大多依赖 kube-proxy 用 iptables 规则做转发。iptables 的处理模型是线性匹配规则规则一多延迟和更新开销都会上升。Cilium 的做法是把负载均衡和网络策略直接编译成 eBPF 程序挂在 TC 或 XDP 钩子上在包进入协议栈之前就完成决策。数据路径短了性能自然好策略更新也变成了直接更新 Map不再需要像 iptables 那样重新生成一大串规则链。我自己的经验是在集群流量比较大的场景下Cilium 替代 kube-proxy 之后长连接的转发延迟和 NAT 的性能损耗都有肉眼可见的改善。这种改善不是来自某个花哨的配置而是因为整个数据处理路径变得更短了。4.2 安全领域Falco 与运行时安全监控可观测性和安全监控在 eBPF 时代几乎是同一件事——只不过安全监控关注的是异常行为。Falco 是 CNCF 项目里典型的基于 eBPF 的运行时安全工具。Falco 的思路是用 eBPF 持续采集系统调用和其他内核事件然后对照一套规则引擎做实时判断。比如某容器突然执行了chmod x /etc/passwd或者某个进程想连接一个非常规端口这些行为会被 eBPF 探针捕捉到规则引擎再根据上下文决定是否告警。传统安全监控方案很多依赖 agent 在用户态读取各种日志时效性差、容易被绕过Falco 这类 eBPF 方案直接在事件产生的源头做判断因为探针就在内核事件点上恶意行为基本无处躲。4.3 性能剖析与排障profile、runqlat 这一票工具BCC 工具集里有一堆现成的性能剖析工具名字很有识别度。比如profile可以做 CPU 火焰图采样runqlat统计任务在运行队列里的等待延迟biolatency看块设备的 IO 延迟分布。runqlat之于 CPU 调度问题几乎等同于抓包之于网络问题。它能告诉你在一个统计周期内有多少任务等待了 0-1ms、1-2ms、2-4ms…… 如果发现大量任务在运行队列里等待超过 10ms基本可以判断 CPU 资源吃紧或者调度策略有问题。真实排障中我常用的组合拳是先用runqlat判断是否调度延迟再用profile采样看热点函数最后用trace或者funclatency跟踪某个可疑函数的调用耗时。这一套下来大概率能找到根因。它们都是 bpftrace 或 bcc 里现成的脚本不需要从零写 eBPF 代码开箱即用。4.4 云原生基础设施的隐形底座除了 Cilium 和 Falco还有很多基础设施工具选择 eBPF 作为底座。比如服务网格领域的链路追踪数据采集、网络策略执行的 sidecar 卸载再比如大规模集群里的 Node 级指标采集都有 eBPF 方案在背后做数据采集层。这背后的原因很实际eBPF 不需要修改业务代码不需要注入 sidecar 进程也不需要在宿主机上装各种内核模块。它对业务进程的干扰最小数据采集的精度却能到内核函数级别。对于追求零侵入和高精度的基础设施团队来说eBPF 几乎是唯一能同时满足这两个条件的方案。如果你平时在做 Kubernetes 方面的运维可以留意一下环境中是否已经有容器以 privileged 模式运行、但又不是业务容器的观测类 Pod——那很可能就是个 eBPF 采集器。这种部署模式现在越来越常见以后也会成为基础设施的标准形态。5. 我踩过的坑内核版本、BTF 与验证器限制5.1 内核版本兼容性问题eBPF 发展速度极快但这也带来一个非常现实的问题新特性往往只在较新的内核里可用。比如早期的 eBPF 不支持循环、不支持读取全局变量很多现代特性在 5.2、5.8、5.10、5.15 这些版本上都有不同的支持程度。我早期用 kprobe 写过一个追踪脚本在开发机的 5.4 内核上跑得很正常部署到生产环境的 4.19 内核上直接加载失败。原因就是某个辅助函数在 4.19 里还没有。排查过程很简单把错误信息里的函数名去include/uapi/linux/bpf.h里查一下版本注释就行但这种坑第一次踩到的人都容易懵。解决这类问题的标准思路是如果工具涉及 kprobe 和 tracepoint 混用优先用 tracepoint如果必须用 kprobe程序里要处理符号找不到的情况并且确认目标机的内核版本符合 API 支持表。长期维护的工具还是建议上 CO-RE从机制上规避版本耦合。5.2 BTF 与 CO-RE 的坑CO-RE 代表一次编译到处运行听起来很美但实际用起来有不少细节。CO-RE 的核心依赖是 BTFBPF Type Format它描述了内核数据结构的布局信息。eBPF 程序在访问内核结构体字段时不再写死偏移量而是通过 BTF 在运行时解析。但 BTF 不是天然就有的它需要内核开启CONFIG_DEBUG_INFO_BTF。很多发行版在较老的内核上没开这个选项。你自己编译内核的时候也得注意开启 BTF 会增加内核镜像的体积和编译时间不少人为了省那一点编译时间就把它关了结果后来跑 CO-RE 程序时满头问号。另外 BTF 也是有版本差异的。如果目标机的内核太老、BTF 信息不完整某些字段访问还是会失败。我的经验是维护一个面向多版本内核的观测工具必须先在各个目标版本上做一轮冒烟测试别在开发机上跑通了就觉得万事大吉。5.3 验证器的限制与指令复杂度验证器最让人痛苦的限制就是指令复杂度。eBPF 程序能够处理的指令条数有限特别是早期版本限制很严格超过就加载失败。哪怕现在的版本放宽了很多你仍然会在写复杂逻辑时撞上这个上限。我写过一次对网络数据包做多层字段解析的程序一开始信心满满地按常规 C 语言风格写结果验证器直接报complexity limit exceeded。解决办法无外乎几种精简代码逻辑、把复杂的状态拆成多个 eBPF 程序并用 Map 串联、或者把部分逻辑挪到用户态处理。这算是对思维方式的一种改造——在内核态只做最核心的观测动作别的都交给用户态。还有一点值得注意验证器对数组索引、指针边界的要求非常苛刻。如果你在内核态代码里写了一个通过变量做数组下标的操作验证器会尝试枚举所有可能的取值范围确保任何情况下都不会越界。所以写 eBPF 时尽量用常量或者非常明确的边界推导不要写那种理论上没问题但验证器看不出来的代码。5.4 权限与容器环境限制加载 eBPF 程序需要一定的权限。普通用户默认是不能加载的要么 root要么具有CAP_BPF和CAP_PERFMON权限。在容器环境里还要注意容器是否允许加载 eBPF默认的 seccomp 或 AppArmor 配置可能会拦截bpf()系统调用。常见的表现是容器里跑 bpftrace 脚本报 Operation not permitted。这时候不要一开始就去搞什么特权模式先确认宿主机能不能跑、容器内是不是被 seccomp 挡了。如果有权限要求可以在容器 spec 里加CAP_BPF或者直接跟安全团队确认观察容器的安全策略。最终如果跑不了一个很实用的退路是在宿主机上跑观测工具把数据输出共享给容器使用。虽然不如直接在容器里跑灵活但比什么都看不到强多了。6. eBPF 的能力边界与未来趋势6.1 什么场景不适合用 eBPFeBPF 虽然强大但它不是万能钥匙。我见过不少团队刚接触 eBPF 的时候特别兴奋什么需求都想用它来解结果绕了一大圈又回到传统方案。eBPF 不适合的场景主要有这么几类。第一需要随机访问大量磁盘数据的场景。eBPF 程序运行在内核态你不可能在里面做复杂的文件 IO 或者数据库查询它更适合做轻量级的数据处理。第二状态非常复杂的业务逻辑。eBPF 程序要过验证器逻辑复杂了指令数就爆炸最后还是得把逻辑拆出去。第三需要跨主机架构保证字节码兼容的场景虽然可以借助可移植性方案缓解但实际落地还是有不小的适配工作量。直白地说eBPF 的定位是观测和轻量处理而不是在内核里写业务系统。把复杂逻辑放用户态、把高频决策放内核态才是合理的设计思路。6.2 值得关注的技术演进方向eBPF 还在高速演进几个方向值得跟踪。第一是 BPF 指令集的持续扩展比如有符号整数操作、更灵活的内存模型这些都会让 eBPF 程序的表达能力更强。第二是 BPF 在内核生态里的渗透越来越多的内核子系统开始把 eBPF 作为官方可扩展方式而不是社区自己拼出来的方案。第三是工具链的成熟libbpf 和 CO-RE 已经让写一个生产级观测工具的门槛大幅降低未来会有更多开箱即用的 eBPF 组件出现。如果是做基础设施方向的读者我建议尽早把 eBPF 纳入技术雷达。即便现阶段只是用 bpftrace 排查问题积累的思维方式也会在将来用上。变化是常态但内核底层的这套安全可编程思路大概率会成为未来十年操作系统基础设施的核心底座之一。我自己在一次次排障中对 eBPF 最大的感受就是它给了普通开发者和运维人员一种看见内核真相的能力而这种能力在过去几乎只有内核开发者才能拥有。与其等着别人做好现成工具不如先动手跑通一两个脚本在真实的场景里体会一下探针挂上去那一刻的掌控感。踩过几次坑之后你会慢慢形成自己的判断知道什么场景该上 eBPF什么场景不能硬上。这就是从会用工具到理解工具的过程也是这篇文章最想帮你迈过的那道坎。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑