基于eBPF的零开销Agent Harness可观测性:从内核“偷窥”到全栈可视化的革命
一、 引言 (Introduction)
1.1 钩子 (The Hook)
你有没有遇到过这样的场景?
深夜三点,你的公司核心电商应用的订单转化率骤降30%,告警邮件炸了你的手机。你第一时间打开Prometheus查看CPU、内存、磁盘IO的指标——正常得离谱;再打开Jaeger找追踪——核心服务的平均延迟明明标了50ms,但用户端反馈是“支付页面加载30秒还在转圈”;最后你翻遍了ELK里的所有应用日志——没有ERROR,甚至连WARN都少得可怜,只有一堆“INFO: Payment processing initiated”这种没用的流水账。
你挠破了头,重启了应用容器——没用;扩容了K8s节点——没用;升级了中间件版本——还是没用。直到运维同事随手敲了个sar -n DEV 1,发现某个Pod的入站流量居然全是TCP重传包!原来核心支付服务所在的宿主机网卡队列被某个后台日志收集Agent打满了,导致正常业务包根本传不进去。
那你有没有想过,如果我们能在内核层面“悄无声息”地监控网卡队列、CPU调度、网络重传、内存分配这些底层细节,而不是依赖那些消耗大量业务资源的用户态Agent,刚才的问题是不是5分钟就能定位,甚至提前2小时就收到预警?
再换个场景:你是一家金融公司的SRE,根据监管要求,你必须审计所有容器里的进程执行、文件读写、网络连接操作。但传统的审计工具(比如auditd)要么性能损耗太大(开启后业务CPU飙升20%-50%,根本不敢在生产环境全量开),要么只能监控宿主机,无法穿透容器隔离;而云原生可观测性工具(比如Falco)虽然用了eBPF,但功能太聚焦安全,做全栈性能/可用性可观测性时要么数据不全,要么需要部署多个Agent(Falco管安全,Prometheus Node Exporter管指标,SkyWalking Agent管追踪,Filebeat管日志),数据碎片化严重,而且多个Agent加起来的性能损耗可能比一个auditd还大。
那你有没有想过,有没有一种技术,可以让我们只部署一个「内核级的、零业务感知的、几乎没有性能损耗的统一可观测性框架」——这个框架就像Agent的“Harness(马具/ harness/ harness框架)”一样,能把底层内核的全量可观测数据(指标、日志、追踪的原生种子数据**)“套”出来,然后按需喂给上层的Falco、Prometheus、SkyWalking、ELK这些工具,同时上层工具再也不用自己写钩子、埋点、收集数据,只需要专注于分析和展示?**
没错,这就是今天我们要聊的核心主题:基于eBPF的零开销Agent Harness可观测性。
1.2 定义问题/阐述背景 (The “Why”)
1.2.1 传统可观测性的三大“原罪”
在深入eBPF之前,我们必须先搞清楚:为什么我们需要“零开销Agent Harness”?传统可观测性到底哪里出了问题?
根据CNCF 2024年《云原生可观测性调查报告》,全球有超过87%的企业正在使用至少3种云原生可观测性工具,超过62%的企业正在使用5种以上。但与此同时,有高达79%的企业认为“可观测性工具的性能损耗是生产环境最大的痛点”,有68%的企业认为“数据碎片化严重,无法实现全栈根因分析”,还有57%的企业认为“埋点、部署、维护可观测性Agent的成本太高”。
这三大痛点,本质上就是传统可观测性的“三大原罪”:
原罪一:高侵入性、高性能损耗(“偷业务资源的小偷”)
传统可观测性主要依赖用户态埋点(Instrumentation)和用户态采样(Sampling):- 埋点:无论是手动埋点(比如在Java代码里加
Span.current())还是自动埋点(比如用SkyWalking的Java Agent字节码注入),都会修改业务代码的执行流程,增加额外的CPU、内存、网络开销——根据CNCF的测试数据,自动埋点的性能损耗通常在5%-30%之间,手动埋点如果写得不好甚至可能达到50%以上; - 采样:为了降低性能损耗,很多企业会对追踪数据进行采样(比如1%的采样率)——但这又会导致“长尾问题”(比如只在0.5%的请求里出现的支付超时,采样率1%可能刚好漏掉,或者只收集到很少的数据,根本无法定位);
- 用户态钩子:还有一些传统工具(比如Node Exporter的某些插件、Filebeat的某些收集器)会用
ptrace、kprobes的旧版本(非eBPF实现的)、或者读取/proc文件系统来收集数据——ptrace的性能损耗极高(每执行一条指令都要陷入内核,开启后业务CPU可能飙升100%),/proc文件系统本质上是内核导出的“只读快照”,读取频率高了也会消耗大量内核CPU(比如每秒读1000次/proc/[pid]/stat,内核CPU可能增加5%-10%)。
- 埋点:无论是手动埋点(比如在Java代码里加
原罪二:数据碎片化严重(“盲人摸象的困境”)
传统可观测性工具通常是“各司其职”的:- 指标工具(Prometheus、Grafana、InfluxDB):负责收集和展示CPU、内存、磁盘IO、网络IO这些“宏观”指标;
- 追踪工具(Jaeger、Zipkin、SkyWalking):负责收集和展示请求的“微观”调用链;
- 日志工具(ELK、Loki、PLG):负责收集和展示应用的“文本”日志;
- 安全工具(Falco、Trivy、Aqua):负责收集和展示应用的“安全”事件。
这些工具的数据格式、时间戳精度、数据来源都不一样——指标数据通常是1秒/10秒/1分钟的聚合数据,时间戳精度到秒;追踪数据通常是纳秒级的原始数据,但采样率低;日志数据通常是毫秒级的文本数据,但可能没有统一的TraceID;安全工具的数据通常是内核级的原始数据,但只关注安全相关的事件。
这就导致了一个问题:当出现故障时,你需要在多个工具之间来回切换,手动关联数据——比如先用Prometheus找到CPU飙升的时间点,再用Jaeger找到那个时间点附近的请求,再用ELK找到那些请求的日志,再用Falco看看有没有安全事件——整个过程可能需要几十分钟甚至几个小时,而且很容易漏掉关键线索。
原罪三:部署、维护成本高(“养不起的工具链”)
传统可观测性工具链的部署和维护成本非常高:- 部署成本:你需要在每个K8s节点上部署至少3-5个DaemonSet(Node Exporter、Falco、Filebeat、SkyWalking OAP的 sidecar?不对,SkyWalking的Java Agent是sidecar或者静态注入,但OAP是集中式的),在每个应用Pod里部署至少2-3个sidecar(Filebeat收集容器日志、SkyWalking Agent做字节码注入、Envoy做服务网格?不对,Envoy虽然也算可观测性的一部分,但主要是服务治理);
- 维护成本:你需要定期升级这些Agent的版本,修复安全漏洞,调整配置(比如采样率、日志收集路径、指标收集维度),排查Agent自身的故障(比如Agent OOM、Agent卡死、Agent和后端断开连接);
- 学习成本:你需要学习每个工具的使用方法、配置语法、API接口——PromQL、LogQL、Jaeger的查询界面、SkyWalking的拓扑图,这些都是有学习门槛的;
- 成本开销:除了人力成本,还有硬件成本和云服务成本——你需要部署大量的Prometheus Server、Grafana、Elasticsearch、Jaeger Collector、SkyWalking OAP这些后端组件,这些组件需要消耗大量的CPU、内存、磁盘IO;如果用云服务厂商的可观测性服务(比如AWS CloudWatch、GCP Cloud Monitoring、阿里云ARMS),成本可能更高——根据AWS的定价,CloudWatch Logs的存储成本是0.03美元/GB/月,数据摄入成本是0.50美元/GB,指标数据的摄入成本是0.30美元/百万个指标,这对于一个有1000个节点、10000个Pod的企业来说,每月的可观测性成本可能高达数万美元甚至数十万美元。
1.2.2 eBPF:内核级可观测性的“银色子弹”?
就在传统可观测性陷入“三大原罪”的泥潭时,eBPF(extended Berkeley Packet Filter)技术横空出世,被誉为“内核级可观测性的银色子弹”、“云原生可观测性的未来”。
那么,eBPF到底是什么?它为什么能解决传统可观测性的问题?
简单来说,eBPF是一个运行在Linux内核中的“迷你虚拟机”——它允许用户在内核的“安全沙箱”里运行自定义的程序,而不需要修改内核源代码、不需要加载内核模块(LKM)。
eBPF程序可以挂载到内核的各种“钩子点”(Hook Points)上:
- 网络钩子点:XDP(eXpress Data Path,网卡驱动层)、TC(Traffic Control,网络协议栈层)、socket钩子点;
- 内核函数钩子点:kprobe(内核函数入口)、kretprobe(内核函数返回)、fentry/fexit(比kprobe/kretprobe更安全、更高效的内核函数钩子点,Linux 5.5+引入);
- 用户态函数钩子点:uprobe(用户态函数入口)、uretprobe(用户态函数返回);
- 跟踪点钩子点:tracepoint(内核预定义的、稳定的钩子点,比如
sched_switch、netif_receive_skb); - perf事件钩子点:perf_event(硬件性能计数器事件,比如CPU cycles、cache misses)。
当内核执行到这些钩子点时,就会触发对应的eBPF程序执行——eBPF程序可以收集内核的各种数据(比如进程ID、线程ID、时间戳、函数参数、函数返回值、网络包内容、内存分配信息),然后把这些数据存储到eBPF的“映射表”(Maps)里,或者通过“ perf事件缓冲区”(Perf Event Buffer)、“ ring buffer”(Linux 5.8+引入的更高效的数据传输机制)发送给用户态的程序。
eBPF的这些特性,完美地解决了传统可观测性的“三大原罪”:
- 零(极低)侵入性、零(极低)性能损耗:
- eBPF程序运行在内核的“安全沙箱”里,不需要修改业务代码的执行流程,不需要加载内核模块;
- fentry/fexit、tracepoint这些钩子点的性能损耗极低——根据Linux内核社区的测试数据,fentry/fexit的性能损耗只有约0.1%(每执行一次钩子点只需要几十纳秒),tracepoint的性能损耗甚至更低(只有约0.01%);
- ring buffer的性能损耗比perf event buffer低约30%-50%,而且支持多CPU并发写入;
- 不需要采样——因为性能损耗极低,所以可以收集全量的内核级可观测数据。
- 统一的全栈可观测数据来源:
- eBPF程序可以收集从硬件层(CPU cycles、cache misses、网卡队列)、内核层(CPU调度、内存分配、文件系统、网络协议栈)、容器层(cgroup、namespace、容器生命周期)、应用层(通过uprobe/uretprobe收集用户态函数的调用信息,或者通过解析内核的系统调用信息还原应用的行为)的全量可观测数据;
- 这些数据都有统一的纳秒级时间戳、进程ID/线程ID、容器ID/Pod ID/Namespace ID——可以轻松地实现全栈数据的关联。
- 低部署、低维护成本:
- 只需要部署一个基于eBPF的统一可观测性Agent Harness DaemonSet——这个DaemonSet运行在每个K8s节点上,负责加载eBPF程序、收集全量的内核级可观测数据、然后按需喂给上层的可观测性工具;
- 上层的可观测性工具不需要自己写钩子、埋点、收集数据,只需要专注于分析和展示;
- 不需要在每个应用Pod里部署sidecar——除非上层工具需要特殊的功能(比如SkyWalking的Java Agent需要做全链路的TraceID传递,但eBPF也可以通过解析网络包的Header来还原TraceID,比如HTTP的
X-Request-ID、X-B3-TraceId,或者gRPC的grpc-trace-bin)。
1.2.3 Agent Harness:可观测性工具的“通用马具”
虽然eBPF很强大,但它也有一个缺点:eBPF的学习门槛非常高——你需要精通Linux内核、C语言(或者Rust语言,现在有很多eBPF工具链支持Rust)、eBPF的指令集、eBPF的验证器(Verifier)规则、eBPF的映射表、eBPF的数据传输机制。
而且,现在的eBPF可观测性工具通常是“单一功能”的:
- Cilium Hubble:专注于网络可观测性;
- Pixie:专注于应用层可观测性(但现在已经被CNCF归档了);
- BCC:一个eBPF工具集,包含很多单一功能的工具(比如
execsnoop、opensnoop、tcpconnlat); - bpftrace:一个高级的eBPF跟踪语言,类似于awk和DTrace,但功能还是比较单一;
- Falco:专注于安全可观测性;
- Parca:专注于持续性能分析(Continuous Profiling)。
这就导致了一个问题:如果你想实现全栈可观测性,你还是需要部署多个eBPF工具——Cilium Hubble管网络,Falco管安全,Parca管性能,BCC/bpftrace管临时排查——这些工具的数据格式、数据传输机制都不一样,还是存在数据碎片化的问题,而且多个eBPF程序挂载到同一个钩子点上会不会有性能损耗?
答案是肯定的——虽然单个eBPF程序的性能损耗很低,但如果有10个eBPF程序挂载到同一个sched_switch钩子点上,每个程序都要执行一遍,性能损耗就会叠加到1%左右,虽然还是比传统工具低,但还是有优化空间的。
这时候,Agent Harness的概念就应运而生了。
那么,什么是Agent Harness?
简单来说,Agent Harness是一个基于eBPF的统一可观测性框架——它的核心思想是:
- 统一加载eBPF程序:Agent Harness只加载一组“通用的、模块化的、可配置的”eBPF程序,这些程序挂载到内核的所有关键钩子点上,收集全量的内核级可观测数据;
- 统一存储和过滤数据:Agent Harness把收集到的全量数据存储到本地的环形缓冲区或者内存数据库里,然后根据上层工具的“订阅规则”(比如“只订阅容器ID为abc123的Pod的网络连接事件”、“只订阅CPU使用率超过80%的进程的内存分配信息”),过滤出上层工具需要的数据;
- 统一传输数据:Agent Harness把过滤后的数据转换成上层工具需要的格式(比如Prometheus的OpenMetrics格式、Jaeger的OpenTelemetry格式、Falco的JSON格式、ELK的JSON格式),然后通过统一的API接口或者消息队列发送给上层工具;
- 统一管理eBPF程序和订阅规则:Agent Harness提供了一个统一的控制平面,可以动态地加载/卸载eBPF程序、调整订阅规则、查看Agent Harness自身的状态(比如CPU使用率、内存使用率、数据收集速率、数据传输速率)。
可以把Agent Harness想象成一个**“可观测性数据的自来水厂”**:
- 内核的各种钩子点是**“水源”**;
- Agent Harness加载的通用eBPF程序是**“抽水泵”**——负责把水源里的水(全量可观测数据)抽出来;
- Agent Harness的本地存储和过滤模块是**“蓄水池和净水器”**——负责把水存储起来,然后根据用户的需求过滤出干净的水(上层工具需要的数据);
- Agent Harness的数据转换和传输模块是**“水管和水龙头”**——负责把过滤后的水转换成用户需要的格式(比如自来水、热水、纯净水),然后通过水管送到用户家里(上层工具);
- Agent Harness的控制平面是**“自来水厂的中控室”**——负责控制抽水泵的开关、调整净水器的过滤规则、查看蓄水池的水位、查看水管的流量。
1.3 亮明观点/文章目标 (The “What” & “How”)
1.3.1 亮明观点
本文的核心观点是:基于eBPF的零开销Agent Harness可观测性是云原生可观测性的未来——它可以彻底解决传统可观测性的“三大原罪”,实现全栈、统一、零开销、低维护的可观测性。
1.3.2 文章目标
读完这篇文章,你将能够:
- 深入理解eBPF的核心概念、工作原理、优势和局限性;
- 深入理解Agent Harness的核心概念、架构设计、关键技术;
- 从零开始,用Rust和Aya(一个Rust语言的eBPF工具链)构建一个简单的基于eBPF的零开销Agent Harness可观测性框架;
- 把这个简单的Agent Harness框架和Prometheus、Falco、OpenTelemetry这三个主流的可观测性工具集成起来;
- 了解基于eBPF的零开销Agent Harness可观测性的最佳实践、常见陷阱、行业发展和未来趋势。
1.4 文章结构预告
本文将按照以下结构展开:
- 引言:介绍传统可观测性的痛点、eBPF的优势、Agent Harness的概念,亮明观点和文章目标;
- 基础知识/背景铺垫:深入讲解eBPF的核心概念、工作原理、工具链、优势和局限性,深入讲解可观测性的三大支柱(指标、日志、追踪)的核心概念,深入讲解云原生环境下的可观测性挑战;
- Agent Harness的核心概念与架构设计:深入讲解Agent Harness的核心概念、设计目标、架构设计(数据平面、控制平面、管理平面)、关键技术(模块化eBPF程序、高效数据存储和过滤、统一数据转换和传输、动态管理);
- 核心内容/实战演练:从零开始构建基于eBPF的零开销Agent Harness:用Rust和Aya构建一个简单的Agent Harness框架,包括模块化eBPF程序的开发、数据平面的开发、控制平面的开发、管理平面的开发;
- 进阶探讨/最佳实践:Agent Harness与主流可观测性工具的集成:把我们构建的简单Agent Harness框架和Prometheus、Falco、OpenTelemetry集成起来,实现全栈可观测性;
- 常见陷阱与避坑指南:介绍基于eBPF的零开销Agent Harness可观测性的常见陷阱(比如eBPF验证器的限制、数据传输的性能瓶颈、容器隔离的穿透、安全问题)以及如何避免这些陷阱;
- 行业发展与未来趋势:介绍基于eBPF的零开销Agent Harness可观测性的行业发展历史、现状、未来趋势(比如eBPF的CO-RE(Compile Once - Run Everywhere)技术、eBPF的WASM集成、eBPF的AI/ML集成、可观测性的“自动驾驶”);
- 结论:总结文章的核心要点,展望未来,发出行动号召。
二、 基础知识/背景铺垫 (Foundational Concepts)
2.1 前言
在深入讲解基于eBPF的零开销Agent Harness可观测性之前,我们必须先掌握一些必备的基础知识——这些知识就像盖房子的“地基”一样,没有地基,房子就盖不起来;地基不牢,房子就会倒塌。
本章将分为三个部分:
- eBPF深度解析:深入讲解eBPF的核心概念、工作原理、工具链、优势和局限性;
- 可观测性三大支柱深度解析:深入讲解指标(Metrics)、日志(Logs)、追踪(Traces)的核心概念、数据模型、主流工具;
- 云原生环境下的可观测性挑战:深入讲解云原生环境(容器、Kubernetes、微服务、服务网格)给可观测性带来的新挑战。
2.2 eBPF深度解析
2.2.1 eBPF的起源:从BPF到eBPF
要理解eBPF,我们必须先了解它的“前身”——BPF(Berkeley Packet Filter)。
2.2.1.1 BPF的诞生:1992年
1992年,加州大学伯克利分校的Steven McCanne和Van Jacobson发表了一篇名为《The BSD Packet Filter: A New Architecture for User-level Packet Capture》的论文——这篇论文标志着BPF的诞生。
在BPF诞生之前,用户态的数据包捕获工具(比如最早的tcpdump的前身snoop)通常采用的是“拷贝所有数据包到用户态,然后在用户态过滤”的策略——这种策略的性能损耗非常大,因为每个数据包都要从内核态拷贝到用户态,即使这个数据包最终会被过滤掉。
而BPF采用的是“在内核态过滤数据包,只拷贝符合过滤条件的数据包到用户态”的策略——这种策略的性能损耗非常小,因为只有少数符合过滤条件的数据包才会被拷贝到用户态。
BPF的核心组件是:
- BPF虚拟机:一个运行在内核态的“迷你虚拟机”,有自己的指令集(类似RISC指令集,只有约30条指令);
- BPF过滤器:用BPF虚拟机的指令集编写的程序,负责在内核态过滤数据包;
- BPF映射表(Maps):后来才加入的组件,用于存储BPF程序的数据;
- BPF数据传输机制:用于把符合过滤条件的数据包从内核态拷贝到用户态。
2.2.1.2 BPF的发展:2011年之前
从1992年到2011年,BPF的发展非常缓慢——它主要被用于数据包捕获工具(比如tcpdump、Wireshark)和防火墙工具(比如iptables的-m bpf模块)。
在这期间,BPF只做了一些小的改进:
- 2003年:Linux内核2.5.75版本加入了BPF映射表(Maps)的雏形——
sock_filter的fd字段; - 2008年:Linux内核2.6.27版本加入了
TPACKET_V3,提高了BPF数据传输的性能; - 2010年:Linux内核2.6.37版本加入了
seccomp-bpf,允许用BPF程序过滤系统调用。
2.2.1.3 eBPF的诞生:2014年
2014年,Linux内核3.18版本加入了eBPF(extended Berkeley Packet Filter)——这标志着BPF进入了一个全新的时代。
eBPF的核心贡献者是Daniel Borkmann、Alexei Starovoitov、Ingo Molnar等Linux内核社区的顶级开发者。
eBPF对BPF进行了革命性的扩展:
- 扩展了指令集:从原来的约30条RISC指令集扩展到了约100条指令集,包括64位算术运算指令、跳转指令、函数调用指令、内存访问指令等;
- 扩展了寄存器:从原来的2个32位寄存器扩展到了11个64位寄存器(
r0-r10,其中r10是只读的栈指针寄存器); - 扩展了映射表(Maps):提供了多种类型的映射表(比如哈希表、数组、环形缓冲区、LRU哈希表、栈、队列等),可以存储大量的数据,支持用户态程序和eBPF程序并发访问;
- 扩展了钩子点(Hook Points):从原来的只有网络钩子点(XDP、TC、socket)扩展到了内核函数钩子点(kprobe、kretprobe、fentry/fexit)、用户态函数钩子点(uprobe、uretprobe)、跟踪点钩子点(tracepoint)、perf事件钩子点(perf_event)、cgroup钩子点、LSM(Linux Security Module)钩子点等;
- 加入了eBPF验证器(Verifier):这是eBPF最重要的组件之一——它负责在加载eBPF程序之前,检查eBPF程序是否安全(比如是否会访问非法内存、是否会陷入无限循环、是否会修改内核的关键数据结构),只有通过验证的eBPF程序才能被加载到内核中运行;
- 加入了eBPF JIT(Just-In-Time)编译器:这是eBPF提高性能的关键组件之一——它负责把eBPF程序的字节码编译成主机的原生机器码(比如x86_64、ARM64、RISC-V的机器码),这样eBPF程序的执行速度就和原生内核代码差不多了;
- 加入了CO-RE(Compile Once - Run Everywhere)技术:这是eBPF在云原生环境下普及的关键技术之一——它允许我们把eBPF程序编译成一次字节码,然后在不同版本的Linux内核上运行,而不需要重新编译(之前的eBPF程序需要针对每个版本的Linux内核重新编译,因为内核的数据结构定义可能会变化)。
2.2.2 eBPF的核心概念
要理解eBPF的工作原理,我们必须先掌握eBPF的核心概念:
- eBPF程序(eBPF Program);
- eBPF映射表(eBPF Map);
- eBPF钩子点(eBPF Hook Point);
- eBPF验证器(eBPF Verifier);
- eBPF JIT编译器(eBPF JIT Compiler);
- eBPF辅助函数(eBPF Helper Functions);
- eBPF CO-RE技术;
- eBPF数据传输机制。
下面我们将逐一深入讲解这些核心概念。
2.2.2.1 eBPF程序(eBPF Program)
eBPF程序是用C语言、Rust语言或者bpftrace语言编写的、运行在Linux内核中的“迷你程序”。
eBPF程序的编写流程通常是:
- 用高级语言编写eBPF程序的源代码:比如用C语言、Rust语言或者bpftrace语言;
- 把高级语言的源代码编译成eBPF字节码:比如用
clang(C语言)、rustc+aya-ebpf(Rust语言)、bpftrace编译器(bpftrace语言); - 把eBPF字节码加载到内核中:比如用
bpf()系统调用、或者用libbpf库、或者用aya库; - 把eBPF程序挂载到内核的钩子点上;
- 当内核执行到钩子点时,触发对应的eBPF程序执行;
- eBPF程序执行完毕后,返回钩子点继续执行内核的原生代码。
eBPF程序的类型通常由它挂载的钩子点决定——不同类型的eBPF程序有不同的输入参数、不同的返回值、不同的可用辅助函数。
根据Linux内核的官方文档,eBPF程序的类型主要有以下几种:
| eBPF程序类型(enum bpf_prog_type) | 挂载的钩子点 | 主要用途 | 可用的Linux内核版本 |
|---|---|---|---|
BPF_PROG_TYPE_SOCKET_FILTER | socket | 数据包过滤(传统BPF的用途) | 3.18+ |
BPF_PROG_TYPE_KPROBE | kprobe/kretprobe | 内核函数跟踪 | 3.18+ |
BPF_PROG_TYPE_SCHED_CLS | TC ingress | 网络流量分类 | 3.18+ |
BPF_PROG_TYPE_SCHED_ACT | TC egress | 网络流量控制 | 3.18+ |
BPF_PROG_TYPE_TRACEPOINT | tracepoint | 内核预定义事件跟踪 | 4.7+ |
BPF_PROG_TYPE_XDP | XDP | 高性能数据包处理(网卡驱动层) | 4.8+ |
BPF_PROG_TYPE_PERF_EVENT | perf_event | 硬件性能计数器事件跟踪 | 4.9+ |
BPF_PROG_TYPE_CGROUP_SKB | cgroup skb | cgroup级别的数据包过滤 | 4.10+ |
BPF_PROG_TYPE_CGROUP_SOCK | cgroup sock | cgroup级别的socket操作控制 | 4.10+ |
BPF_PROG_TYPE_LWT_IN | LWT ingress | 轻量级隧道入口处理 | 4.10+ |
BPF_PROG_TYPE_LWT_OUT | LWT egress | 轻量级隧道出口处理 | 4.10+ |
BPF_PROG_TYPE_LWT_XMIT | LWT xmit | 轻量级隧道发送处理 | 4.10+ |
BPF_PROG_TYPE_SOCK_OPS | cgroup sock_ops | cgroup级别的socket选项控制 | 4.13+ |
BPF_PROG_TYPE_SK_SKB | socket skb | socket级别的数据包处理 | 4.14+ |
BPF_PROG_TYPE_CGROUP_DEVICE | cgroup device | cgroup级别的设备访问控制 | 4.15+ |
BPF_PROG_TYPE_SK_MSG | socket msg | socket级别的消息处理 | 4.17+ |
BPF_PROG_TYPE_RAW_TRACEPOINT | raw_tracepoint | 内核原始跟踪点跟踪(比tracepoint更高效,但更不稳定) | 4.17+ |
BPF_PROG_TYPE_CGROUP_SOCK_ADDR | cgroup sock_addr | cgroup级别的socket地址控制 | 4.17+ |
BPF_PROG_TYPE_LSM | LSM hook | 安全模块钩子点(比如文件访问控制、进程执行控制) | 5.7+ |
BPF_PROG_TYPE_SK_REUSEPORT | socket reuseport | 端口复用的socket选择 | 5.7+ |
BPF_PROG_TYPE_FLOW_DISSECTOR | flow dissector | 网络流量解析 | 5.8+ |
BPF_PROG_TYPE_CGROUP_SYSCTL | cgroup sysctl | cgroup级别的sysctl参数控制 | 5.10+ |
BPF_PROG_TYPE_EXT | 扩展eBPF程序 | 扩展已有的eBPF程序 | 5.10+ |
BPF_PROG_TYPE_LIRC_MODE2 | LIRC mode2 | 红外遥控器数据处理 | 5.11+ |
BPF_PROG_TYPE_SK_LOOKUP | socket lookup | socket查找拦截 | 5.13+ |
BPF_PROG_TYPE_SYSCALL | syscall | 系统调用拦截(比seccomp-bpf更强大) | 5.14+ |
BPF_PROG_TYPE_NETFILTER | netfilter | netfilter钩子点 | 5.16+ |
BPF_PROG_TYPE_KPROBE_MULTI | kprobe_multi | 批量内核函数跟踪 | 5.18+ |
BPF_PROG_TYPE_UPROBE_MULTI | uprobe_multi | 批量用户态函数跟踪 | 5.18+ |
(表1:eBPF程序类型及主要用途)
下面我们来看一个最简单的eBPF程序——这个程序是用C语言编写的,挂载到sched_switch跟踪点上,当内核切换进程时,打印出旧进程的PID和新进程的PID。
// sched_switch.bpf.c#include"vmlinux.h"// 包含Linux内核的数据结构定义,CO-RE技术需要#include<bpf/bpf_helpers.h>// 包含eBPF辅助函数的声明#include<bpf/bpf_tracing.h>// 包含eBPF跟踪点的宏定义#include<bpf/bpf_core_read.h>// 包含CO-RE技术的宏定义// 定义一个eBPF程序,挂载到sched_switch跟踪点上SEC("tracepoint/sched/sched_switch")intsched_switch_trace(structtrace_event_raw_sched_switch*ctx){// 获取旧进程的PIDu32 old_pid=BPF_CORE_READ(ctx,prev_pid);// 获取新进程的PIDu32 new_pid=BPF_CORE_READ(ctx,next_pid);// 获取旧进程的名称charold_comm[16];bpf_core_read_str(old_comm,sizeof(old_comm),BPF_CORE_READ(ctx,prev_comm));// 获取新进程的名称charnew_comm[16];bpf_core_read_str(new_comm,sizeof(new_comm),BPF_CORE_READ(ctx,next_comm));// 打印日志到内核的trace buffer(用户态可以用dmesg或者bpftool trace查看)bpf_printk("sched_switch: old_pid=%d, old_comm=%s, new_pid=%d, new_comm=%s\n",old_pid,old_comm,new_pid,new_comm);return0;}// eBPF许可证(必须是GPL或者BSD,否则无法使用某些辅助函数)charLICENSE[]SEC("license")="GPL";(代码1:最简单的eBPF程序——sched_switch跟踪)
这个eBPF程序虽然简单,但它已经包含了eBPF程序的所有核心要素:
- 包含头文件:
vmlinux.h(CO-RE技术需要)、bpf/bpf_helpers.h(辅助函数声明)、bpf/bpf_tracing.h(跟踪点宏定义)、bpf/bpf_core_read.h(CO-RE宏定义); - 定义eBPF程序:用
SEC()宏定义eBPF程序的类型和挂载点(这里是tracepoint/sched/sched_switch); - 获取输入参数:eBPF程序的输入参数
ctx是一个指向sched_switch跟踪点的原始数据结构的指针; - 使用CO-RE宏读取内核数据:用
BPF_CORE_READ()宏读取内核数据结构的字段(这样可以在不同版本的Linux内核上运行); - 使用辅助函数:用
bpf_printk()辅助函数打印日志到内核的trace buffer; - 定义许可证:用
SEC("license")宏定义eBPF程序的许可证(必须是GPL或者BSD,否则无法使用bpf_printk()等辅助函数)。
2.2.2.2 eBPF映射表(eBPF Map)
eBPF映射表是一个用于存储eBPF程序数据的“内核态数据结构”——它可以被用户态程序和eBPF程序并发访问,是用户态程序和eBPF程序之间通信的“桥梁”。
eBPF映射表的类型非常多,每种类型都有自己的特点和适用场景——下面我们来看一下eBPF映射表的主要类型:
| eBPF映射表类型(enum bpf_map_type) | 数据结构 | 特点 | 适用场景 | 可用的Linux内核版本 |
|---|---|---|---|---|
BPF_MAP_TYPE_HASH | 哈希表 | 支持任意类型的key和value,查找、插入、删除的时间复杂度是O(1) | 存储键值对数据(比如存储进程的PID和进程的名称、存储网络连接的五元组和网络连接的延迟) | 3.18+ |
BPF_MAP_TYPE_ARRAY | 数组 | 支持整数类型的key(索引),查找、插入、删除的时间复杂度是O(1),比哈希表更高效 | 存储固定大小的索引数据(比如存储每个CPU的统计数据、存储每个网络接口的统计数据) | 3.18+ |
BPF_MAP_TYPE_PROG_ARRAY | 程序数组 | 存储eBPF程序的文件描述符(fd),支持通过索引调用对应的eBPF程序 | 实现eBPF程序的“跳转表”(比如XDP程序根据数据包的协议类型调用不同的处理程序) | 4.2+ |
BPF_MAP_TYPE_PERF_EVENT_ARRAY | perf事件数组 | 存储perf事件的文件描述符(fd),支持通过索引把数据发送到对应的perf事件缓冲区 | eBPF程序和用户态程序之间的高效数据传输(之前的主要数据传输机制) | 4.3+ |
BPF_MAP_TYPE_PERCPU_HASH | 每CPU哈希表 | 每个CPU都有一个独立的哈希表,查找、插入、删除的时间复杂度是O(1),不需要锁(因为每个CPU只访问自己的哈希表),比普通哈希表更高效 | 存储需要高并发访问的键值对数据(比如存储每个CPU的网络连接统计数据) | 4.6+ |
BPF_MAP_TYPE_PERCPU_ARRAY | 每CPU数组 | 每个CPU都有一个独立的数组,查找、插入、删除的时间复杂度是O(1),不需要锁,比普通数组更高效 | 存储需要高并发访问的固定大小的索引数据(比如存储每个CPU的CPU cycles统计数据) | 4.6+ |
BPF_MAP_TYPE_STACK_TRACE | 栈跟踪表 | 存储栈跟踪信息 | 存储程序的调用栈(比如性能分析、故障排查) | 4.6+ |
BPF_MAP_TYPE_CGROUP_ARRAY | cgroup数组 | 存储cgroup的文件描述符(fd),支持通过索引访问对应的cgroup | cgroup级别的网络流量控制、资源统计 | 4.8+ |
BPF_MAP_TYPE_LRU_HASH | LRU哈希表 | 带LRU(Least Recently Used,最近最少使用)淘汰策略的哈希表 | 存储需要淘汰旧数据的键值对数据(比如存储最近10000个网络连接的信息) | 4.10+ |
BPF_MAP_TYPE_LRU_PERCPU_HASH | 每CPU LRU哈希表 | 每个CPU都有一个独立的带LRU淘汰策略的哈希表 | 存储需要高并发访问且需要淘汰旧数据的键值对数据 | 4.10+ |
BPF_MAP_TYPE_LPM_TRIE | LPM前缀树 | 带LPM(Longest Prefix Match,最长前缀匹配)策略的前缀树 | 存储IP地址前缀数据(比如防火墙规则、路由规则) | 4.11+ |
BPF_MAP_TYPE_ARRAY_OF_MAPS | 映射表数组 | 存储其他eBPF映射表的文件描述符(fd) | 实现多层映射表(比如先根据网络接口的索引找到对应的哈希表,再根据网络连接的五元组找到对应的信息) | 4.12+ |
BPF_MAP_TYPE_HASH_OF_MAPS | 映射表哈希表 | 存储其他eBPF映射表的文件描述符(fd) | 实现多层映射表(比如先根据容器的ID找到对应的哈希表,再根据进程的PID找到对应的信息) | 4.12+ |
BPF_MAP_TYPE_DEVMAP | 设备映射表 | 存储网络设备的索引 | XDP程序的数据包重定向 | 4.14+ |
BPF_MAP_TYPE_SOCKMAP | socket映射表 | 存储socket的文件描述符(fd) | socket级别的消息重定向 | 4.17+ |
BPF_MAP_TYPE_CPUMAP | CPU映射表 | 存储CPU的索引 | XDP程序的数据包重定向到其他CPU | 4.15+ |
BPF_MAP_TYPE_XSKMAP | AF_XDP socket映射表 | 存储AF_XDP socket的文件描述符(fd) | XDP程序的数据包重定向到AF_XDP socket | 4.18+ |
BPF_MAP_TYPE_SOCKHASH | socket哈希表 | 存储socket的文件描述符(fd),支持任意类型的key | socket级别的消息重定向 | 4.18+ |
BPF_MAP_TYPE_CGROUP_STORAGE | cgroup存储表 | 存储cgroup级别的数据 | cgroup级别的资源统计 | 4.19+ |
BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE | 每CPU cgroup存储表 | 每个CPU都有一个独立的cgroup存储表 | cgroup级别的高并发资源统计 | 4.19+ |
BPF_MAP_TYPE_QUEUE | 队列 | FIFO(先进先出)数据结构 | 存储需要按顺序处理的数据 | 4.20+ |
BPF_MAP_TYPE_STACK | 栈 | LIFO(后进先出)数据结构 | 存储需要按逆序处理的数据 | 4.20+ |
BPF_MAP_TYPE_RINGBUF | 环形缓冲区 | 多CPU并发写入、单CPU或多CPU并发读取的环形缓冲区,比BPF_MAP_TYPE_PERF_EVENT_ARRAY更高效 | eBPF程序和用户态程序之间的高效数据传输(现在的主要数据传输机制) | 5.8+ |
BPF_MAP_TYPE_INODE_STORAGE | inode存储表 | 存储inode级别的数据 | 文件级别的资源统计 | 5.10+ |
BPF_MAP_TYPE_TASK_STORAGE | 任务存储表 | 存储task_struct级别的数据(每个进程/线程都有一个task_struct) | 进程/线程级别的资源统计 | 5.11+ |
| `BPF_MAP_TYPE_BLO |