
基于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但功能太聚焦安全做全栈性能/可用性可观测性时要么数据不全要么需要部署多个AgentFalco管安全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%。原罪二数据碎片化严重“盲人摸象的困境”传统可观测性工具通常是“各司其职”的指标工具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个DaemonSetNode Exporter、Falco、Filebeat、SkyWalking OAP的 sidecar不对SkyWalking的Java Agent是sidecar或者静态注入但OAP是集中式的在每个应用Pod里部署至少2-3个sidecarFilebeat收集容器日志、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内核级可观测性的“银色子弹”就在传统可观测性陷入“三大原罪”的泥潭时eBPFextended Berkeley Packet Filter技术横空出世被誉为“内核级可观测性的银色子弹”、“云原生可观测性的未来”。那么eBPF到底是什么它为什么能解决传统可观测性的问题简单来说eBPF是一个运行在Linux内核中的“迷你虚拟机”——它允许用户在内核的“安全沙箱”里运行自定义的程序而不需要修改内核源代码、不需要加载内核模块LKM。eBPF程序可以挂载到内核的各种“钩子点”Hook Points上网络钩子点XDPeXpress Data Path网卡驱动层、TCTraffic Control网络协议栈层、socket钩子点内核函数钩子点kprobe内核函数入口、kretprobe内核函数返回、fentry/fexit比kprobe/kretprobe更安全、更高效的内核函数钩子点Linux 5.5引入用户态函数钩子点uprobe用户态函数入口、uretprobe用户态函数返回跟踪点钩子点tracepoint内核预定义的、稳定的钩子点比如sched_switch、netif_receive_skbperf事件钩子点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、tcpconnlatbpftrace一个高级的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-RECompile 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我们必须先了解它的“前身”——BPFBerkeley 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版本加入了eBPFextended 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钩子点、LSMLinux Security Module钩子点等加入了eBPF验证器Verifier这是eBPF最重要的组件之一——它负责在加载eBPF程序之前检查eBPF程序是否安全比如是否会访问非法内存、是否会陷入无限循环、是否会修改内核的关键数据结构只有通过验证的eBPF程序才能被加载到内核中运行加入了eBPF JITJust-In-Time编译器这是eBPF提高性能的关键组件之一——它负责把eBPF程序的字节码编译成主机的原生机器码比如x86_64、ARM64、RISC-V的机器码这样eBPF程序的执行速度就和原生内核代码差不多了加入了CO-RECompile Once - Run Everywhere技术这是eBPF在云原生环境下普及的关键技术之一——它允许我们把eBPF程序编译成一次字节码然后在不同版本的Linux内核上运行而不需要重新编译之前的eBPF程序需要针对每个版本的Linux内核重新编译因为内核的数据结构定义可能会变化。2.2.2 eBPF的核心概念要理解eBPF的工作原理我们必须先掌握eBPF的核心概念eBPF程序eBPF ProgrameBPF映射表eBPF MapeBPF钩子点eBPF Hook PointeBPF验证器eBPF VerifiereBPF JIT编译器eBPF JIT CompilereBPF辅助函数eBPF Helper FunctionseBPF CO-RE技术eBPF数据传输机制。下面我们将逐一深入讲解这些核心概念。2.2.2.1 eBPF程序eBPF ProgrameBPF程序是用C语言、Rust语言或者bpftrace语言编写的、运行在Linux内核中的“迷你程序”。eBPF程序的编写流程通常是用高级语言编写eBPF程序的源代码比如用C语言、Rust语言或者bpftrace语言把高级语言的源代码编译成eBPF字节码比如用clangC语言、rustcaya-ebpfRust语言、bpftrace编译器bpftrace语言把eBPF字节码加载到内核中比如用bpf()系统调用、或者用libbpf库、或者用aya库把eBPF程序挂载到内核的钩子点上当内核执行到钩子点时触发对应的eBPF程序执行eBPF程序执行完毕后返回钩子点继续执行内核的原生代码。eBPF程序的类型通常由它挂载的钩子点决定——不同类型的eBPF程序有不同的输入参数、不同的返回值、不同的可用辅助函数。根据Linux内核的官方文档eBPF程序的类型主要有以下几种eBPF程序类型enum bpf_prog_type挂载的钩子点主要用途可用的Linux内核版本BPF_PROG_TYPE_SOCKET_FILTERsocket数据包过滤传统BPF的用途3.18BPF_PROG_TYPE_KPROBEkprobe/kretprobe内核函数跟踪3.18BPF_PROG_TYPE_SCHED_CLSTC ingress网络流量分类3.18BPF_PROG_TYPE_SCHED_ACTTC egress网络流量控制3.18BPF_PROG_TYPE_TRACEPOINTtracepoint内核预定义事件跟踪4.7BPF_PROG_TYPE_XDPXDP高性能数据包处理网卡驱动层4.8BPF_PROG_TYPE_PERF_EVENTperf_event硬件性能计数器事件跟踪4.9BPF_PROG_TYPE_CGROUP_SKBcgroup skbcgroup级别的数据包过滤4.10BPF_PROG_TYPE_CGROUP_SOCKcgroup sockcgroup级别的socket操作控制4.10BPF_PROG_TYPE_LWT_INLWT ingress轻量级隧道入口处理4.10BPF_PROG_TYPE_LWT_OUTLWT egress轻量级隧道出口处理4.10BPF_PROG_TYPE_LWT_XMITLWT xmit轻量级隧道发送处理4.10BPF_PROG_TYPE_SOCK_OPScgroup sock_opscgroup级别的socket选项控制4.13BPF_PROG_TYPE_SK_SKBsocket skbsocket级别的数据包处理4.14BPF_PROG_TYPE_CGROUP_DEVICEcgroup devicecgroup级别的设备访问控制4.15BPF_PROG_TYPE_SK_MSGsocket msgsocket级别的消息处理4.17BPF_PROG_TYPE_RAW_TRACEPOINTraw_tracepoint内核原始跟踪点跟踪比tracepoint更高效但更不稳定4.17BPF_PROG_TYPE_CGROUP_SOCK_ADDRcgroup sock_addrcgroup级别的socket地址控制4.17BPF_PROG_TYPE_LSMLSM hook安全模块钩子点比如文件访问控制、进程执行控制5.7BPF_PROG_TYPE_SK_REUSEPORTsocket reuseport端口复用的socket选择5.7BPF_PROG_TYPE_FLOW_DISSECTORflow dissector网络流量解析5.8BPF_PROG_TYPE_CGROUP_SYSCTLcgroup sysctlcgroup级别的sysctl参数控制5.10BPF_PROG_TYPE_EXT扩展eBPF程序扩展已有的eBPF程序5.10BPF_PROG_TYPE_LIRC_MODE2LIRC mode2红外遥控器数据处理5.11BPF_PROG_TYPE_SK_LOOKUPsocket lookupsocket查找拦截5.13BPF_PROG_TYPE_SYSCALLsyscall系统调用拦截比seccomp-bpf更强大5.14BPF_PROG_TYPE_NETFILTERnetfilternetfilter钩子点5.16BPF_PROG_TYPE_KPROBE_MULTIkprobe_multi批量内核函数跟踪5.18BPF_PROG_TYPE_UPROBE_MULTIuprobe_multi批量用户态函数跟踪5.18表1eBPF程序类型及主要用途下面我们来看一个最简单的eBPF程序——这个程序是用C语言编写的挂载到sched_switch跟踪点上当内核切换进程时打印出旧进程的PID和新进程的PID。// sched_switch.bpf.c#includevmlinux.h// 包含Linux内核的数据结构定义CO-RE技术需要#includebpf/bpf_helpers.h// 包含eBPF辅助函数的声明#includebpf/bpf_tracing.h// 包含eBPF跟踪点的宏定义#includebpf/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_pidBPF_CORE_READ(ctx,prev_pid);// 获取新进程的PIDu32 new_pidBPF_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.hCO-RE技术需要、bpf/bpf_helpers.h辅助函数声明、bpf/bpf_tracing.h跟踪点宏定义、bpf/bpf_core_read.hCO-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 MapeBPF映射表是一个用于存储eBPF程序数据的“内核态数据结构”——它可以被用户态程序和eBPF程序并发访问是用户态程序和eBPF程序之间通信的“桥梁”。eBPF映射表的类型非常多每种类型都有自己的特点和适用场景——下面我们来看一下eBPF映射表的主要类型eBPF映射表类型enum bpf_map_type数据结构特点适用场景可用的Linux内核版本BPF_MAP_TYPE_HASH哈希表支持任意类型的key和value查找、插入、删除的时间复杂度是O(1)存储键值对数据比如存储进程的PID和进程的名称、存储网络连接的五元组和网络连接的延迟3.18BPF_MAP_TYPE_ARRAY数组支持整数类型的key索引查找、插入、删除的时间复杂度是O(1)比哈希表更高效存储固定大小的索引数据比如存储每个CPU的统计数据、存储每个网络接口的统计数据3.18BPF_MAP_TYPE_PROG_ARRAY程序数组存储eBPF程序的文件描述符fd支持通过索引调用对应的eBPF程序实现eBPF程序的“跳转表”比如XDP程序根据数据包的协议类型调用不同的处理程序4.2BPF_MAP_TYPE_PERF_EVENT_ARRAYperf事件数组存储perf事件的文件描述符fd支持通过索引把数据发送到对应的perf事件缓冲区eBPF程序和用户态程序之间的高效数据传输之前的主要数据传输机制4.3BPF_MAP_TYPE_PERCPU_HASH每CPU哈希表每个CPU都有一个独立的哈希表查找、插入、删除的时间复杂度是O(1)不需要锁因为每个CPU只访问自己的哈希表比普通哈希表更高效存储需要高并发访问的键值对数据比如存储每个CPU的网络连接统计数据4.6BPF_MAP_TYPE_PERCPU_ARRAY每CPU数组每个CPU都有一个独立的数组查找、插入、删除的时间复杂度是O(1)不需要锁比普通数组更高效存储需要高并发访问的固定大小的索引数据比如存储每个CPU的CPU cycles统计数据4.6BPF_MAP_TYPE_STACK_TRACE栈跟踪表存储栈跟踪信息存储程序的调用栈比如性能分析、故障排查4.6BPF_MAP_TYPE_CGROUP_ARRAYcgroup数组存储cgroup的文件描述符fd支持通过索引访问对应的cgroupcgroup级别的网络流量控制、资源统计4.8BPF_MAP_TYPE_LRU_HASHLRU哈希表带LRULeast Recently Used最近最少使用淘汰策略的哈希表存储需要淘汰旧数据的键值对数据比如存储最近10000个网络连接的信息4.10BPF_MAP_TYPE_LRU_PERCPU_HASH每CPU LRU哈希表每个CPU都有一个独立的带LRU淘汰策略的哈希表存储需要高并发访问且需要淘汰旧数据的键值对数据4.10BPF_MAP_TYPE_LPM_TRIELPM前缀树带LPMLongest Prefix Match最长前缀匹配策略的前缀树存储IP地址前缀数据比如防火墙规则、路由规则4.11BPF_MAP_TYPE_ARRAY_OF_MAPS映射表数组存储其他eBPF映射表的文件描述符fd实现多层映射表比如先根据网络接口的索引找到对应的哈希表再根据网络连接的五元组找到对应的信息4.12BPF_MAP_TYPE_HASH_OF_MAPS映射表哈希表存储其他eBPF映射表的文件描述符fd实现多层映射表比如先根据容器的ID找到对应的哈希表再根据进程的PID找到对应的信息4.12BPF_MAP_TYPE_DEVMAP设备映射表存储网络设备的索引XDP程序的数据包重定向4.14BPF_MAP_TYPE_SOCKMAPsocket映射表存储socket的文件描述符fdsocket级别的消息重定向4.17BPF_MAP_TYPE_CPUMAPCPU映射表存储CPU的索引XDP程序的数据包重定向到其他CPU4.15BPF_MAP_TYPE_XSKMAPAF_XDP socket映射表存储AF_XDP socket的文件描述符fdXDP程序的数据包重定向到AF_XDP socket4.18BPF_MAP_TYPE_SOCKHASHsocket哈希表存储socket的文件描述符fd支持任意类型的keysocket级别的消息重定向4.18BPF_MAP_TYPE_CGROUP_STORAGEcgroup存储表存储cgroup级别的数据cgroup级别的资源统计4.19BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE每CPU cgroup存储表每个CPU都有一个独立的cgroup存储表cgroup级别的高并发资源统计4.19BPF_MAP_TYPE_QUEUE队列FIFO先进先出数据结构存储需要按顺序处理的数据4.20BPF_MAP_TYPE_STACK栈LIFO后进先出数据结构存储需要按逆序处理的数据4.20BPF_MAP_TYPE_RINGBUF环形缓冲区多CPU并发写入、单CPU或多CPU并发读取的环形缓冲区比BPF_MAP_TYPE_PERF_EVENT_ARRAY更高效eBPF程序和用户态程序之间的高效数据传输现在的主要数据传输机制5.8BPF_MAP_TYPE_INODE_STORAGEinode存储表存储inode级别的数据文件级别的资源统计5.10BPF_MAP_TYPE_TASK_STORAGE任务存储表存储task_struct级别的数据每个进程/线程都有一个task_struct进程/线程级别的资源统计5.11BPF_MAP_TYPE_BLO