去年有次线上抖动排查,把我和同事折腾了一整晚。现象很典型:接口P99延迟从50ms飙到800ms,CPU使用率不高,内存充足,容器平台所有常规指标全部正常。用tcpdump抓包,流量一大,抓包本身先把CPU打满了;用perf采样,全在采样用户态代码,内核里到底发生了什么完全看不见;翻内核日志,干干净净什么都没有。后来靠一个eBPF程序才定位到根因——某个节点上TCP重传率异常,是底层交换机链路抖动导致数据包丢失,而业务侧完全感知不到。
这次经历让我下决心把eBPF(extended Berkeley Packet Filter)这套东西彻底吃透。在云原生架构里,服务拆得越来越细、网络路径越来越长,传统观测手段的盲区越来越多,而eBPF恰好能把这些盲区一个一个点亮。这篇内容我围绕网络监控和性能优化两条主线,把从原理到实战、从工具选型到常见坑位的东西都整理出来,适合正在做SRE、平台工程、后端基础架构,或者对内核观测感兴趣的开发者参考。
1. 先弄懂eBPF到底解决了什么问题
1.1 传统观测手段卡在哪
在说eBPF之前,先看看过去我们要观测内核状态有多难。
最朴素的方式是读/proc和/sys,比如用ss看连接状态、用free看内存、用mpstat看CPU。这些数据是内核预先把指标算好、通过procfs导出的,能看到一些全局统计,但看不到细节。比如CPU使用率高,是哪个进程、哪段代码、在内核什么路径上消耗的?ss能看到连接状态,但看不到重传发生在哪一秒、丢包发生在哪一层。这些工具本质上都是"采样",拿到的只是结果,不是过程。
稍微深入一点,可以用tcpdump抓包。tcpdump依赖libpcap,在内核里注册AF_PACKET套接字,把数据包从协议栈复制到用户态再做过滤分析。问题在于这个过程涉及数据拷贝和上下文切换,流量稍微一大,抓包本身的CPU开销就上来了。我在压测环境做过测试,万兆网卡跑满时开tcpdump,吞吐量直接掉20%以上。这不是tcpdump不行,是它的架构决定了必须把包从内核搬到用户态才能处理。
再进一步,可以用perf。perf是性能分析的经典工具,基于perf_event子系统,可以采样CPU周期、缓存未命中、内核函数调用栈等。但perf有个天然盲区:它采集的是正在CPU上运行的进程,如果线程因为等待IO、等待锁、等待网络数据而休眠,perf就完全看不到了。这在实际排查中非常致命——很多性能问题恰恰不是CPU不够,而是大家都在等。
传统内核模块也能做深度观测,比如写一个内核模块挂到某个内核函数上做埋点。但内核模块的风险太大,一不留神就能把整个系统搞崩溃,而且每个内核版本API都可能变,维护成本极高。生产环境敢用内核模块做观测的团队,我见过的真不多。
这个局面的核心矛盾是:应用越来越复杂,内核越来越复杂,但我们观测内核的手段还停留在"从外面看"的阶段,谁都不敢轻易进到内核里面去看,因为太危险了。
1.2 eBPF的核心机制:给内核装一个安全的探针
eBPF从根本上改变了这个局面。它允许你把一段代码加载到内核里,在特定的hook点执行,从而观测甚至干预内核的行为,同时通过验证器和沙箱机制保证这段代码不会把内核搞崩。
理解eBPF最直观的方式,是想象给内核装了一批"探测器"。这些探测器不是从外部看,而是直接安装在内核内部的关键位置。每个探测器上还配了一个"安全员"——验证器,专门检查你放进去的代码,访问范围有没有越界、循环有没有退出条件、指针操作是否合法,任何有风险的代码在加载阶段就会被拒绝。验证器检查通过之后,JIT编译器会把你的代码编译成原生指令,接近直接运行在CPU上的执行效率。
eBPF程序本身不能直接访问内核内存,所有数据读取都必须通过bpf_probe_read这类辅助函数安全地拷贝。内核态和用户态之间通过map数据结构交换数据。My理解里这就像探测器在内核里采集数据,用篮筐(map)把数据送到外面,用户态程序再从篮筐里拿数据分析展示。整个过程,内核完全没有暴露给不安全的代码。
关于hook点,eBPF支持的类型非常多。kprobe可以挂钩到任意内核函数入口,kretprobe挂钩到函数返回;tracepoint是内核开发者预留的稳定事件点,比如系统调用、调度器、网络栈都有对应的tracepoint;uprobe可以挂钩到用户态程序的函数,方便分析应用层行为;XDP挂钩在网卡驱动层,处理报文最早的位置;TC挂钩在流量控制层,可以管控流量转发路径。选择哪个hook点,直接影响你看到什么、能做什么、开销有多大,这个后面详细说。
1.3 为什么eBPF在云原生时代成了刚需
eBPF其实从2014年就开始进入内核主线,但过去几年才真正爆发,根本原因在于云原生架构把传统观测手段的短板放到了极致。
容器网络本身就是一个大黑洞。一个Pod里的流量要经过veth pair、Linux bridge、iptables规则、overlay网络等多层转发,链路长到一旦出问题根本不知道从哪查起。以前在物理机上用tcpdump加ifconfig就能解决的问题,在K8s集群里变得异常复杂。eBPF可以挂载到TC层直接看到容器网卡入口和出口的完整报文,甚至能看到overlay封装前后的变化,这才让容器网络的排障真正变得可操作。
服务编排的弹性也带来了新的观测需求。Pod动态创建删除、服务多副本漂移、流量从A迁移到B,这些变化是常态。传统静态的监控系统很难在这种情况下拼出完整视图。eBPF基于内核事件,和进程生命周期天然绑定——进程死了,采集自然跟着消失,不存在"僵尸采集器"的问题。连接级别的数据、进程级别的数据从内核实时出来,天然适配这种动态环境。
另外一个关键推手是性能开销。云原生组件多了,如果每个组件都做一个agent来做观测,节点资源根本不够用。eBPF程序跑在内核态,不需要复制数据到用户态,不需要反复切换上下文,性能开销极低。我见过一个部署在K8s集群的eBPF网络指标采集器,整体CPU占用不到1%,这放在传统方案里是不可想象的。低开销意味着它可以常驻生产环境,随时想看数据就看,而不是出事时才临时抱佛脚。
内核本来就该被观测,只是过去没有一个安全又高效的方式。eBPF把这两点同时做到了,所以被云原生生态选中也就不奇怪了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手准备:环境、工具链和第一个程序
2.1 内核版本与权限要求
eBPF功能是逐步演进内核对,不同版本能力差异很大。做实验前先确认你的内核版本,用uname -r看一眼,低于4.4就别玩了,很多关键特性不可用。
简单整理一下常用版本对应的eBPF能力:
| 内核版本 | eBPF相关能力 |
|---|---|
| 4.4 | 内核主线首次集成eBPF,基础map、kprobe可用 |
| 4.9 | 大幅增强,增加更多辅助函数 |
| 4.14 | 支持BPF到BPF调用,可用bpf_tail_call |
| 4.18 | 增加bpf_skb_load_bytes、bpf_probe_read_str等常见辅助函数 |
