惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
V2EX
爱范儿
爱范儿
Martin Fowler
Martin Fowler
T
The Blog of Author Tim Ferriss
B
Blog RSS Feed
博客园 - 聂微东
G
GRAHAM CLULEY
Engineering at Meta
Engineering at Meta
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
WordPress大学
WordPress大学
Scott Helme
Scott Helme
AI
AI
S
Security Affairs
T
Threat Research - Cisco Blogs
M
MIT News - Artificial intelligence
T
Troy Hunt's Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
人人都是产品经理
人人都是产品经理
AWS News Blog
AWS News Blog
T
Threatpost
Cyberwarzone
Cyberwarzone
www.infosecurity-magazine.com
www.infosecurity-magazine.com
U
Unit 42
V
Vulnerabilities – Threatpost
J
Java Code Geeks
博客园 - Franky
月光博客
月光博客
Blog — PlanetScale
Blog — PlanetScale
NISL@THU
NISL@THU
D
Docker
小众软件
小众软件
N
News and Events Feed by Topic
Microsoft Security Blog
Microsoft Security Blog
Y
Y Combinator Blog
A
Arctic Wolf
D
DataBreaches.Net
云风的 BLOG
云风的 BLOG
Forbes - Security
Forbes - Security
量子位
PCI Perspectives
PCI Perspectives
美团技术团队
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
I
InfoQ
Security Archives - TechRepublic
Security Archives - TechRepublic
有赞技术团队
有赞技术团队
腾讯CDC
P
Proofpoint News Feed
S
Security @ Cisco Blogs
G
Google Developers Blog
C
Cisco Blogs

土法炼钢兴趣小组的算法知识备份

国密算法与国密 TLS 系列索引 【系统架构设计】架构质量属性:不只是"高可用高性能" 【系统架构设计百科】告警策略:如何避免"狼来了" 【系统架构设计】CQRS:读写分离的架构哲学 【系统架构设计】空间架构:极端扩展场景的解法 【系统架构设计】微服务架构深度审视:优势、代价与适用边界 【系统架构设计】扩展性原理:水平、垂直与对角扩展 【系统架构设计】无状态设计:扩展的第一步也是最难的一步 【系统架构设计】缓存架构:从本地到分布式的多级缓存体系 【系统架构设计】管道与过滤器:Unix 哲学的架构表达 【系统架构设计】复杂性管理:架构的核心战场 【系统架构设计】消息队列架构:异步解耦的设计与陷阱 【系统架构设计】CDN 架构:全球加速的设计原理 【系统架构设计】连接池设计:被忽视的性能杀手 【系统架构设计】弹性设计模式:熔断器、舱壁与超时 【系统架构设计】高可用设计模式:冗余、故障转移与仲裁 【系统架构设计】容量规划:从拍脑袋到数据驱动 【系统架构设计】数据库扩展:分库分表的工程实践与替代方案 【系统架构设计】SLO 工程:可靠性的量化管理 【系统架构设计】性能建模:用数学思维分析系统瓶颈 【系统架构设计】混沌工程:主动验证系统的韧性 【系统架构设计】零拷贝与内存映射:数据搬运的极致优化 【系统架构设计】线程模型:从 thread-per-request 到协程 【系统架构设计】容灾架构:多活与灾备设计 【系统架构设计】数据库性能模式:索引、查询与连接管理 【系统架构设计】数据建模:从关系范式到文档模型的真实权衡 【系统架构设计】吞吐量优化:批处理、流水线与并发模型 【系统架构设计】流处理架构:从批处理到实时的范式迁移 【系统架构设计】搜索引擎架构:倒排索引之上的系统设计 【系统架构设计】时序数据架构:监控与 IoT 的存储设计 【系统架构设计】数据迁移与版本化:在线不停机的数据演进 【系统架构设计】数据湖与数据仓库:分析架构的演进路线 【系统架构设计】API 网关设计:入口层的职责边界 【系统架构设计】应用层数据一致性模式:在正确性与性能之间走钢丝 【系统架构设计】多模数据库选型:Polyglot Persistence 的工程实践 【系统架构设计】服务发现与注册:动态拓扑的基础设施 【系统架构设计】配置管理架构:从配置文件到配置中心 【系统架构设计】全链路压测:大规模系统的性能验证 【系统架构设计】幂等性设计:分布式环境下的安全重试 【系统架构设计】契约测试与 Schema 演进:服务间的信任协议 【系统架构设计】长连接与推送架构:WebSocket、SSE 与 MQTT 【系统架构设计】延迟分析:从 P50 到 P999 的全链路追踪 【系统架构设计百科】DDD 战术模式:聚合、实体与值对象 【系统架构设计百科】防腐层与开放主机服务:系统集成的 DDD 方案 【系统架构设计百科】领域事件与事件风暴:从业务到架构的桥梁 【系统架构设计百科】CQRS + Event Sourcing 完整实战:从领域建模到部署 【系统架构设计百科】DDD 与微服务:用领域模型划分服务边界 【系统架构设计】DDD 战略设计:限界上下文与上下文映射 【系统架构设计百科】认证架构:从 Session 到 JWT 到 OIDC 【系统架构设计】API 设计哲学:REST vs GraphQL vs gRPC 的真实权衡 排序算法专题:从 TimSort 到并行排序 【密码学百科】国密算法体系:SM2/SM3/SM4/SM9 全景解读 【密码学百科】承诺方案:Pedersen 承诺、向量承诺与多项式承诺 【密码学百科】不经意传输与隐私信息检索:OT、OT 扩展与 PIR 【密码学百科】门限密码学:门限签名、门限解密与分布式密钥生成 完美哈希:从理论到 gperf 实践 【密码学百科】安全多方计算:从 Yao 的混淆电路到实用 MPC 【密码学百科】同态加密:从 Paillier 到全同态加密(FHE) 【密码学百科】零知识证明系统:zk-SNARKs、zk-STARKs 与 Bulletproofs 【密码学百科】概率论与密码分析:生日攻击、差分分析与线性分析 【密码学百科】计算复杂性与归约:密码安全性证明的基石 【密码学百科】秘密共享:Shamir 方案、VSS 与安全多方计算入口 【密码学百科】椭圆曲线代数:Weierstrass 方程、点群运算与曲线选择 【密码学百科】离散对数与配对密码学:从 DLP 到 BLS 签名 【密码学百科】格密码数学基础:SVP、LWE 与格基约化 【密码学百科】抽象代数:群、环、域的密码学视角 【密码学百科】有限域算术:GF(2^n) 运算与在 AES/ECC 中的应用 【密码学百科】数论进阶:二次剩余、椭圆曲线上的 Weil 配对 【密码学百科】密码学简史:从凯撒密码到量子时代 【密码学百科】威胁模型与安全目标:CIA 三要素之外 【密码学百科】Kerckhoffs 原则与现代密码设计哲学 【密码学百科】随机性:密码学的基石 【密码学百科】信息论入门:熵、完美保密与 Shannon 定理 【密码学百科】分组密码原理:Feistel 网络与 SPN 结构 【密码学百科】AES 逐步拆解:SubBytes 到 MixColumns 的数学 【密码学百科】分组密码工作模式全览:ECB/CBC/CTR/OFB/CFB 【密码学百科】流密码:RC4 的兴衰与 ChaCha20 的崛起 【密码学百科】密码学哈希函数:MD5→SHA-2→SHA-3 的进化之路 【密码学百科】MAC 与 HMAC:消息认证的正确姿势 【密码学百科】认证加密(AEAD):GCM、ChaCha20-Poly1305 与 OCB 【密码学百科】密钥派生函数:HKDF、PBKDF2、Argon2 与密码存储 【密码学百科】公钥密码的数论基础:模运算、群、原根 【密码学百科】RSA 从原理到攻击:教科书 RSA 为什么不安全 【密码学百科】Diffie-Hellman 密钥交换与离散对数问题 【密码学百科】椭圆曲线密码学(ECC):从几何直觉到点群运算 【密码学百科】数字签名:ECDSA、EdDSA 与 Schnorr 签名 【密码学百科】现代密钥交换:X25519、ECDHE 与前向保密 【密码学百科】混合加密与 KEM/DEM 范式:ECIES 与 HPKE 【密码学百科】填充方案:PKCS#1 v1.5、OAEP 与 PSS 【密码学百科】TLS 协议全解析:从握手到 0-RTT 【密码学百科】PKI 与数字证书:信任链的构建与崩塌 【密码学百科】密码认证协议:从 SRP 到 OPAQUE 【密码学百科】零知识证明入门:如何证明你知道而不泄露 【密码学百科】安全信道构造:Noise 协议框架与 Signal 协议 【密码学百科】密钥管理工程:HSM、KMS 与密钥生命周期 【密码学百科】侧信道攻击:从时序攻击到功耗分析 【密码学百科】密码学实现陷阱:三层漏洞分类、审计工具链与系统性预防 密码敏捷性:如何设计可升级的密码系统 【密码学百科】OpenSSL/BoringSSL 架构剖析:ENGINE、Provider 与 FIPS 模块 排序基准测试:用数据说话
【可观测性工程】eBPF 可观测性全景:bcc、bpftrace、libbpf 的工程路径
2026-04-22 · via 土法炼钢兴趣小组的算法知识备份
eBPF 可观测性技术栈

在可观测性的演进史上,扩展的伯克利包过滤器(eBPF,extended Berkeley Packet Filter)是近十年最具颠覆性的内核技术之一。它让我们可以在不修改一行业务代码、不重启任何进程的前提下,将自定义的、经过安全校验的小程序动态挂载到内核与用户态进程的执行路径上,采集系统调用、网络报文、调度事件、文件 I/O、块设备延迟、加密流量等几乎一切可被观察的事件。这种能力对传统监控体系是一次根本性的改写:应用程序不需要主动暴露埋点,运维和 SRE 也不需要打包内核模块,只要一条合适的 eBPF 程序就能让一台在生产环境跑了五年的机器回答”谁打开了这个文件”、“哪个容器的哪条 SQL 慢了”、“TLS 握手卡在哪一步”这类问题。

本篇是可观测性工程系列的第 14 篇,目标是从钩子机制出发,系统梳理 eBPF 可观测性的三条主流工程路径:字节码编译器集合(bcc,BPF Compiler Collection)、bpftrace 脚本语言、以及 libbpf 配合一次编译到处运行(CO-RE,Compile Once – Run Everywhere)的现代工程方案。在此之上再穿过 BPF 类型格式(BTF,BPF Type Format)、BPF 骨架(skeleton)、内核版本兼容矩阵、生产部署中的权能(capability)与验证器(verifier)坑点,直到 Pixie、DeepFlow、Grafana Beyla、Parca 等商业或开源产品的落地路径,最后给出面向国内生产环境的选型建议。阅读完本篇后,你应当能够独立判断:一个具体的可观测性需求,到底应该用 bpftrace 写两行一把梭、用 bcc 拉起一个 Python 脚本、还是沉到 libbpf + CO-RE 做一个可发布的二进制。

在 eBPF 普及之前,Linux 生态里做深度可观测性的方法大致分三类,每一类都有显著的工程代价。

第一类是用户态插桩,典型做法包括动态链接预加载(LD_PRELOAD)、应用层 SDK 埋点、代理(Agent)注入字节码。这类方案的优势是语义清晰,可以精确拿到业务上下文;缺点是必须修改部署,重启进程,语言和运行时绑定严重,且看不到内核发生了什么。比如一个请求在内核 TCP 层丢包,在 sidecar 里什么也抓不到。

第二类是内核跟踪工具,如 strace、ltrace、perf、ftrace,甚至更早年的 SystemTap 和 DTrace。strace 基于进程跟踪(ptrace),对目标进程的性能影响是数量级的,典型场景下会让系统调用慢十倍以上;perf 和 ftrace 能力强,但输出需要后处理,不便直接做生产级别的聚合;SystemTap 要编译内核模块,意味着每次升级、每个发行版都要适配,在生产里几乎没人敢用。DTrace 在 Solaris 上一骑绝尘,但在 Linux 上始终因为许可和工程代价没有进入主流。

第三类是内核模块,直接在内核态写 C 代码。能力无上限,代价是风险无上限:一行空指针解引用就会导致宿主机崩溃,一个不小心的持锁会让整台机器软锁死(softlockup)。没有哪个 SRE 团队会允许把一个未经严格评审的第三方内核模块装到线上机器上。

eBPF 的出现,同时解决了这三条路线的核心痛点:

  • 零侵入(Zero-instrumentation):绝大多数 eBPF 可观测性场景下,被观测的应用完全无感,不需要重新编译、不需要链接任何 SDK、不需要重启。这对已经跑在生产的遗留系统(legacy system)尤其重要。
  • 内核级可见性(Kernel-level visibility):eBPF 程序可以挂载到内核函数、静态跟踪点(tracepoint)、系统调用、调度事件、网络驱动收发包、块设备 I/O 等路径。它看到的是内核视角的第一手数据,不需要从用户态反推。
  • 低开销(Low overhead):eBPF 程序以字节码形式加载,经过即时编译(JIT,Just-In-Time compilation)翻译成本地指令执行,一次典型的 kprobe 命中开销在纳秒级(约 50–200ns),比 ptrace 路径低两到三个数量级。
  • 安全(Safety):所有 eBPF 程序在加载时都要通过内核验证器(verifier)的静态分析,确保不会访问非法内存、不会死循环、不会越界。这让”在内核里跑第三方代码”第一次成为工程上可以接受的事情。
  • 可编程性(Programmability):相比 ftrace、perf 这类”开关式”工具,eBPF 允许你在数据源端直接做过滤、聚合、哈希,而不是把所有原始事件吐到用户态再处理,这在高吞吐场景下是质变。

从历史上看,eBPF 的前身是 1992 年提出的经典 BPF(cBPF,classic BPF),最初仅用于 tcpdump 的过滤表达式编译。2014 年,Alexei Starovoitov 将其扩展为 eBPF 并合入 Linux 3.18,自此 BPF 从”内核包过滤器”演化为”内核可编程平面”。2016 年 XDP(eXpress Data Path)合入,BPF 开始承担高性能网络;2018 年 BTF 合入,为 CO-RE 铺路;2020 年前后 fentry/fexit、BPF 蹦床(trampoline)、LSM BPF 陆续进入主线。到本文成稿时(对应 Linux 6.x 主线),eBPF 已覆盖跟踪、网络、安全、调度等几乎所有内核子系统。

对可观测性工程而言,eBPF 的意义不仅是”多了一种工具”,而是让内核第一次真正成为可观测性数据平面的一等公民。过去我们习惯把应用层指标、日志、追踪视作三大支柱,但只要内核是黑盒,所有排障路径都会在某个节点遇到”不知道”。eBPF 把这层黑盒打开,且代价可控。

二、eBPF 的钩子类型与发展路线

eBPF 程序本身并不神奇,真正决定它能观察到什么的是挂载点(attach point / hook)。不同挂载点对应不同的程序类型(program type),每种程序类型有不同的上下文结构、能调用的内核辅助函数(helper)集合、稳定性保证和性能特征。对于可观测性场景,最常用的挂载点可以分为七大类。

2.1 kprobe / kretprobe

内核探针(kprobe)是 eBPF 可观测性中最朴素也最强大的机制。原理是在目标内核函数的入口处动态替换一条指令为 int3 断点(x86 上),命中断点时陷入异常处理程序,执行挂载的 eBPF 程序,然后返回原指令继续执行。kretprobe 类似,但挂在函数返回路径上,用于拿到返回值。

kprobe 的杀手级优势是几乎可以挂到任何内核符号上tcp_connectvfs_readdo_sys_openat2ext4_file_write_iter 等等。只要这个符号没有被内联(inlined),kprobe 就能生效。缺点是:

  • 不稳定:内核函数签名不是稳定应用程序二进制接口(ABI,Application Binary Interface),版本变了字段偏移就变了,需要 CO-RE 或手工适配。
  • 开销相对较大:int3 断点涉及中断处理和上下文切换,单次命中约 100–300ns,高频函数上挂 kprobe 会显著拖慢系统。
  • 无法挂到内联函数:现代内核在优化下内联了大量小函数,kprobe 会报”找不到符号”。

一个用 bpftrace 写的示例,跟踪所有 TCP 主动连接:

bpftrace -e 'kprobe:tcp_connect { printf("%s -> pid=%d\n", comm, pid); }'

2.2 uprobe / uretprobe

用户态探针(uprobe)是 kprobe 在用户态的镜像,原理同样是 int3 替换。uprobe 可以挂到任何用户态二进制或共享库的任意符号、甚至任意偏移上。这使得我们可以在不重编应用的情况下,跟踪应用自己的函数、跟踪标准库函数、跟踪加密库函数。

一个典型用例是加密流量可观测性:HTTPS 流量在网卡上是密文,传统抓包无法看到 HTTP 请求内容,但如果我们在 OpenSSL 的 SSL_read / SSL_write 上挂 uprobe,就能在加密前和解密后的明文缓冲区拿到完整 HTTP 报文。这正是 Pixie、DeepFlow 等商业化 eBPF 产品实现”零侵入 HTTPS 可观测”的底层机制。

bpftrace -e '
uprobe:/usr/lib/x86_64-linux-gnu/libssl.so.3:SSL_write {
    printf("pid=%d comm=%s wrote %d bytes\n", pid, comm, arg2);
}'

uprobe 的开销比 kprobe 更高(每次命中约 1–3μs),因为涉及用户态到内核态的往返,因此在热路径(如每次调用都命中的小函数)上要谨慎使用。

2.3 静态跟踪点(Tracepoints)

静态跟踪点是内核开发者预先在关键路径上埋下的稳定探针,格式为 subsystem:event_name,例如 sched:sched_switch(进程切换)、syscalls:sys_enter_read(read 系统调用入口)、block:block_rq_issue(块设备请求下发)、net:netif_receive_skb(网卡收包)。

相比 kprobe,tracepoint 的优势是:

  • 稳定 ABI:tracepoint 是内核对外承诺的接口,跨版本字段布局基本不变,CO-RE 场景下可移植性最好。
  • 语义清晰:每个 tracepoint 带有明确字段,不需要了解函数实现细节。
  • 开销略低:没有 int3 断点,静态注册,开销略小于 kprobe。

查看当前内核支持的所有 tracepoint:

sudo ls /sys/kernel/debug/tracing/events/

生产环境可观测性程序应优先选 tracepoint,只有在 tracepoint 无法覆盖时才退化到 kprobe。

2.4 USDT:用户态静态跟踪点

USDT(User Statically-Defined Tracing)是用户态程序在源码里预先埋好的静态探针,最早由 DTrace 引入。应用通过 DTRACE_PROBE 宏(或 systemtap 的 STAP_PROBE)在关键路径上放置一条空指令(nop),eBPF 可以把这条 nop 替换为 int3,从而实现类似 tracepoint 的用户态稳定探针。

大量主流软件内置 USDT,包括:

  • Pythonpython:function__entrypython:function__returnpython:gc__start
  • Node.jsnode:http__server__requestnode:http__server__response
  • MySQL / PostgreSQLmysql:query__startpostgresql:query__start
  • OpenJDKhotspot:method__entryhotspot:gc__begin

相比 uprobe 挂到 SSL_write 这种”第三方函数名”,USDT 是应用作者主动暴露的”稳定可观测接口”,可移植性好得多。

2.5 fentry / fexit(BPF 蹦床)

从 Linux 5.5 开始,内核引入了 BPF 蹦床(BPF trampoline)机制,对应的程序类型是 fentry(function entry)和 fexit(function exit)。与 kprobe 相比,fentry/fexit 的实现方式完全不同:它不依赖 int3 断点,而是通过 ftrace 的函数跟踪器(ftrace function tracer)直接在函数入口处跳转到一段生成的蹦床代码,蹦床调用 BPF 程序后再跳回原函数。

实测数据(Brendan Gregg 等人的基准测试)显示,fentry 的单次命中开销约为 kprobe 的 1/2 到 1/3(在约 30–80ns 量级),特别是在多个 BPF 程序挂到同一函数时,fentry 可以批量调用,效率进一步提升。此外,fexit 与 kretprobe 的一个关键区别是,fexit 能同时拿到入参和返回值,而不需要像 kretprobe 那样用 map 存入参、在返回时取。

SEC("fentry/tcp_connect")
int BPF_PROG(trace_tcp_connect, struct sock *sk) {
    bpf_printk("tcp_connect: sk=%p\n", sk);
    return 0;
}

SEC("fexit/tcp_connect")
int BPF_PROG(trace_tcp_connect_ret, struct sock *sk, int ret) {
    bpf_printk("tcp_connect returned %d\n", ret);
    return 0;
}

建议:目标内核 ≥ 5.5 时,优先使用 fentry/fexit;需要兼容老内核时才用 kprobe/kretprobe。

2.6 XDP / TC / Socket Filter

网络方向的 eBPF 钩子不是本篇重点,但可观测性视角需要略作介绍。

  • 快速数据路径(XDP,eXpress Data Path):挂载在网卡驱动层,收到数据包的瞬间就执行,用于超低延迟丢包、负载均衡、DDoS 缓解。对可观测性的价值是能在最早路径上采样或计数。
  • 流量控制(TC,Traffic Control):挂在协议栈的 TC 子系统,介于驱动和协议栈之间,能看到完整的 sk_buff,适合做连接级别的统计。
  • 套接字过滤器(Socket Filter):挂在单个 socket 上,能力较弱但对容器场景很友好。

性能上,XDP 处理 64 字节小包可以达到每秒千万级(10Mpps+),TC 次之,socket filter 最慢。可观测性用例里,XDP 通常用于全网抓包采样,TC 用于 L4 连接追踪,socket filter 用于按进程过滤。

2.7 LSM BPF 钩子

从 Linux 5.7 开始,eBPF 可以挂载到 Linux 安全模块(LSM,Linux Security Module)钩子上,如 file_opensocket_connectbprm_check_security。这条路径主要面向安全场景(如运行时威胁检测),但对可观测性的意义是:LSM 钩子位于权限检查路径,能稳定地观察到”谁在访问什么资源”,比 kprobe 挂到 VFS 函数更规范。

Tetragon(Isovalent 开源)和 Falco(CNCF 毕业项目)都大量使用 LSM BPF 做运行时观测。

三、bcc(BPF Compiler Collection)

bcc 是 IO Visor 项目于 2015 年开源的 BPF 工具集,是 eBPF 在可观测性领域进入大众视野的第一代工程方案,也是 Brendan Gregg 著作《BPF Performance Tools》中绝大部分示例的基础。

3.1 bcc 的架构与工作流程

bcc 的核心设计是”Python 前端 + Clang/LLVM 后端 + 运行时内核头文件”:

  1. 用户编写一个 Python 脚本,其中内嵌 C 语言的 BPF 程序字符串。
  2. Python 侧的 bcc 库在运行时调用 Clang/LLVM,把 C 字符串编译为 BPF 字节码。
  3. 字节码通过 bpf() 系统调用加载进内核,挂载到指定钩子。
  4. Python 侧通过 BPF 映射(BPF map)读取内核态采集的数据,做展示或聚合。

这个架构的核心问题是:Clang/LLVM 和内核头文件必须在目标机器上可用。在最初几年的 bcc 部署里,运维要在每台被观测机器上装几百兆的 LLVM 工具链和 kernel-devel / linux-headers 包,并且每次内核升级都要同步更新。这在大规模生产集群里是巨大的工程负担,也是后来 libbpf + CO-RE 要解决的核心痛点。

3.2 bcc 工具集:bcc-tools 开箱即用清单

bcc 项目附带数十个现成的诊断工具,位于 /usr/share/bcc/tools/ 目录。以下是最常用的子集:

工具 功能 典型用法
execsnoop 跟踪进程创建(exec) 谁在偷偷跑 shell 脚本
opensnoop 跟踪文件打开 哪个进程在读敏感文件
tcpconnect 跟踪 TCP 主动连接 容器在连哪些外部地址
tcpaccept 跟踪 TCP 被动接受 服务端连接情况
tcpretrans 跟踪 TCP 重传 网络质量诊断
biolatency 块设备 I/O 延迟直方图 磁盘慢请求定位
biosnoop 块设备 I/O 逐事件跟踪 哪个进程在大量写盘
runqlat 调度器运行队列延迟 CPU 竞争分析
offcputime 进程离 CPU 时间堆栈 阻塞在哪一步
profile 基于采样的 CPU 性能剖析 火焰图原始数据
cachestat 页缓存命中率 内存压力分析
funclatency 任意函数耗时分布 热点函数定位
filetop 按进程统计文件 I/O I/O 大户排查
sslsniff OpenSSL/GnuTLS 明文抓取 加密流量可观测

这些工具绝大多数只需 root 权限即可运行,不需要重启任何服务,也不需要修改被观察进程。

3.3 一个真实的 bcc Python 脚本

下面是一个简化版的 HTTP 请求跟踪器,挂 kprobe 到 tcp_sendmsg,在 payload 里识别 HTTP 请求行:

#!/usr/bin/env python3
from bcc import BPF
from time import strftime

bpf_text = r"""
#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <bcc/proto.h>

struct event_t {
    u32 pid;
    u32 size;
    char comm[16];
    char payload[64];
};

BPF_PERF_OUTPUT(events);

int trace_tcp_sendmsg(struct pt_regs *ctx, struct sock *sk,
                      struct msghdr *msg, size_t size) {
    if (size < 4) return 0;
    struct event_t ev = {};
    ev.pid = bpf_get_current_pid_tgid() >> 32;
    ev.size = size;
    bpf_get_current_comm(&ev.comm, sizeof(ev.comm));

    struct iov_iter *iter = &msg->msg_iter;
    const struct iovec *iov = iter->iov;
    void *base = 0;
    bpf_probe_read_kernel(&base, sizeof(base), &iov->iov_base);
    bpf_probe_read_user(&ev.payload, sizeof(ev.payload), base);

    // 简单判断是否 HTTP 请求
    if (ev.payload[0] == 'G' && ev.payload[1] == 'E' && ev.payload[2] == 'T') {
        events.perf_submit(ctx, &ev, sizeof(ev));
    }
    return 0;
}
"""

b = BPF(text=bpf_text)
b.attach_kprobe(event="tcp_sendmsg", fn_name="trace_tcp_sendmsg")

def handle(cpu, data, size):
    ev = b["events"].event(data)
    print("%s pid=%d comm=%s size=%d payload=%s" % (
        strftime("%H:%M:%S"), ev.pid, ev.comm.decode(), ev.size,
        bytes(ev.payload).split(b'\r\n')[0].decode('utf-8', 'replace')))

b["events"].open_perf_buffer(handle)
while True:
    try: b.perf_buffer_poll()
    except KeyboardInterrupt: break

这个脚本展示了 bcc 的典型工作流:内嵌 C、Python 加载、perf buffer 回传、用户态解析。开发体验很不错,但部署到没有 LLVM 的机器上会直接报错。

3.4 bcc 的局限

  • 运行时依赖 LLVM/Clang(数百 MB)。
  • 运行时依赖 linux-headers 或等价包。
  • 启动慢:每次运行要花 1–3 秒编译。
  • 内存占用高:LLVM 动辄吃 100MB+。
  • 跨内核版本仍需内嵌版本判断,可移植性差。

这些问题在笔记本上做一次性排障时完全可以接受,但要把 bcc 工具嵌入生产 agent 就捉襟见肘了。libbpf + CO-RE 正是为此诞生。

四、bpftrace 脚本语言

如果说 bcc 是”给工程师用的 eBPF 开发框架”,bpftrace 就是”给 SRE 用的 eBPF 命令行”。bpftrace 由 Alastair Robertson 发起,现已成为 IO Visor 下与 bcc 并列的顶级项目,灵感直接来自 DTrace 的 D 语言。

4.1 bpftrace 的架构

bpftrace 的内部架构是”领域特定语言(DSL,Domain-Specific Language)→ LLVM IR → BPF 字节码”:

  1. 用户写一段 bpftrace 脚本或一行命令。
  2. bpftrace 前端解析脚本,输出 LLVM 中间表示(IR)。
  3. LLVM 后端把 IR 编译为 BPF 字节码并加载。
  4. bpftrace 运行时负责读 map、格式化输出、退出时自动打印聚合结果。

bpftrace 同样依赖 LLVM,但它对用户隐藏了所有 C 语言细节,上手门槛极低。

4.2 语法速览

bpftrace 的基本单元是探针块(probe block),格式为:

probe-type:target /predicate/ { action; }

例如:

bpftrace -e 'tracepoint:syscalls:sys_enter_openat /comm == "cat"/ {
    printf("%s opened %s\n", comm, str(args->filename));
}'
  • 探针类型(probe type)kprobekretprobeuprobetracepointusdtprofileintervalsoftwarehardware 等。
  • 谓词(predicate):可选的过滤条件,仅当为真时执行动作。
  • 动作(action):C 风格表达式序列,以分号分隔。

4.3 内建变量

bpftrace 提供一组开箱即用的上下文变量:

变量 含义
pid 当前进程 PID
tid 当前线程 TID
uid 当前用户 UID
comm 当前进程名(16 字节截断)
cpu 当前 CPU 编号
nsecs 当前纳秒时间戳
kstack 内核态调用栈
ustack 用户态调用栈
args 探针参数结构体(tracepoint 专用)
arg0..arg9 探针位置参数(kprobe/uprobe)
retval 返回值(kretprobe/uretprobe/fexit)

4.4 内建函数与聚合

bpftrace 的聚合语法是它的杀手锏:

聚合函数 作用
count() 计数
sum(x) 求和
avg(x) 平均
min(x) / max(x) 极值
hist(x) 指数直方图(2 的幂)
lhist(x, min, max, step) 线性直方图
stats(x) 聚合统计

聚合结果保存在映射(map)里,脚本退出时自动打印。

4.5 经典单行命令

文件打开跟踪(opensnoop)

bpftrace -e 'tracepoint:syscalls:sys_enter_openat {
    printf("%-6d %-16s %s\n", pid, comm, str(args->filename));
}'

调度器运行队列延迟(runqlat)

bpftrace -e '
tracepoint:sched:sched_wakeup    { @qt[args->pid] = nsecs; }
tracepoint:sched:sched_wakeup_new{ @qt[args->pid] = nsecs; }
tracepoint:sched:sched_switch {
    if (@qt[args->next_pid]) {
        @us = hist((nsecs - @qt[args->next_pid]) / 1000);
        delete(@qt[args->next_pid]);
    }
}'

块设备 I/O 追踪(biosnoop)

bpftrace -e '
tracepoint:block:block_rq_issue  { @start[args->dev, args->sector] = nsecs; }
tracepoint:block:block_rq_complete {
    $s = @start[args->dev, args->sector];
    if ($s) {
        printf("%-8d %-16s %d us\n", pid, comm, (nsecs - $s) / 1000);
        delete(@start[args->dev, args->sector]);
    }
}'

TCP 连接跟踪(tcpconnect)

bpftrace -e '
kprobe:tcp_v4_connect {
    @sk[tid] = arg0;
}
kretprobe:tcp_v4_connect /@sk[tid]/ {
    $sk = (struct sock *)@sk[tid];
    $dport = ((uint16)$sk->__sk_common.skc_dport);
    printf("%s pid=%d dport=%d\n", comm, pid, ($dport >> 8) | (($dport & 0xff) << 8));
    delete(@sk[tid]);
}'

基于采样的 CPU 剖析(profile)

bpftrace -e 'profile:hz:99 { @[kstack] = count(); }'

这一行就能以 99Hz 的频率采集内核栈,停止时自动输出每条栈路径的次数,直接可以扔进 FlameGraph 画火焰图。

4.6 bpftrace 与 bcc 的选择

经验法则:

  • 一次性排障、Ad-hoc 问题定位:bpftrace。一行命令解决的事不要写脚本。
  • 需要复杂逻辑、需要对接外部系统、要做成产品化工具:bcc 或 libbpf。
  • 需要打包成 agent 分发到上万台机器:libbpf + CO-RE,不要选 bpftrace 也不要选 bcc。

五、libbpf + CO-RE(Compile Once – Run Everywhere)

libbpf 是内核团队维护的 C 语言 BPF 用户态库,位于 tools/lib/bpf/,也以独立项目 libbpf 发布。它是当前 eBPF 工程化的事实标准路径。

5.1 问题:bcc 的部署痛点

回顾 bcc 的架构:运行时编译依赖 Clang/LLVM 和内核头文件。这意味着:

  1. 发布包必须附带 LLVM,体积膨胀。
  2. 每台目标机器必须安装匹配的 linux-headers-$(uname -r)
  3. 启动延迟高,不能做”加载后即用”的轻量 agent。
  4. 同一份 C 源码在不同内核版本下,因 struct 字段偏移变化可能读错内存。

5.2 CO-RE 的核心思路

CO-RE 的目标是:一次在开发机上编译出 BPF 字节码,在任意内核版本上直接加载运行。它由三部分协同完成:

  1. BTF:内核把自身所有类型信息以紧凑格式嵌入 /sys/kernel/btf/vmlinux
  2. CO-RE 重定位(relocation):Clang 编译 BPF 程序时,对字段访问生成特殊的重定位记录(.BTF.ext 段),记录”我要读 struct task_structpid 字段”这种语义信息,而不是写死的偏移。
  3. libbpf 运行时修正:加载时,libbpf 读取目标内核的 BTF,查到 task_struct.pid 在当前内核的真实偏移,patch 进字节码,再提交给验证器。

这样即使内核版本变了、字段偏移变了、甚至字段换了位置,同一份字节码依然能正确工作。

5.3 BTF 简介

BTF 是一种极简的类型描述格式,比 DWARF 小两个数量级,专为 BPF 设计。一个现代 Linux 发行版(Ubuntu 20.10+、Fedora 31+、CentOS Stream 9+、RHEL 8.6+、openEuler 22.03+)都会默认开启 CONFIG_DEBUG_INFO_BTF=y,把 BTF 塞进内核镜像,运行时可以直接读:

ls -lh /sys/kernel/btf/vmlinux

对于老内核(如 CentOS 7 的 3.10),需要借助 BTFGen 或第三方维护的 BTF 包(BTFHub 项目维护了绝大部分主流发行版的 BTF 生成结果),把 BTF 单独带进去。

5.4 libbpf API 速览

libbpf 的核心 API 围绕”对象(object)→ 程序(program)→ 链接(link)→ 映射(map)“四个概念:

struct bpf_object *obj = bpf_object__open_file("prog.bpf.o", NULL);
bpf_object__load(obj);
struct bpf_program *prog = bpf_object__find_program_by_name(obj, "handle_exec");
struct bpf_link *link = bpf_program__attach(prog);
struct bpf_map *map = bpf_object__find_map_by_name(obj, "events");

5.5 BPF 骨架(skeleton)

手写这些 API 调用很繁琐。bpftool gen skeleton 能根据 .bpf.o 自动生成一个 C 头文件(.skel.h),把加载、挂载、读 map 全部封装成结构体成员调用:

#include "execsnoop.skel.h"

int main() {
    struct execsnoop_bpf *skel = execsnoop_bpf__open_and_load();
    execsnoop_bpf__attach(skel);
    // 读 skel->maps.events
    execsnoop_bpf__destroy(skel);
}

5.6 一个完整的 libbpf + CO-RE 程序

内核侧(execsnoop.bpf.c):

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>

char LICENSE[] SEC("license") = "GPL";

struct event {
    u32 pid;
    u32 ppid;
    char comm[16];
    char filename[128];
};

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} events SEC(".maps");

SEC("tracepoint/syscalls/sys_enter_execve")
int handle_execve(struct trace_event_raw_sys_enter *ctx) {
    struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) return 0;

    struct task_struct *task = (struct task_struct *)bpf_get_current_task();
    e->pid = bpf_get_current_pid_tgid() >> 32;
    e->ppid = BPF_CORE_READ(task, real_parent, tgid);
    bpf_get_current_comm(&e->comm, sizeof(e->comm));
    bpf_probe_read_user_str(&e->filename, sizeof(e->filename),
                            (const char *)ctx->args[0]);

    bpf_ringbuf_submit(e, 0);
    return 0;
}

用户态侧(execsnoop.c):

#include <stdio.h>
#include <signal.h>
#include <bpf/libbpf.h>
#include "execsnoop.skel.h"

static volatile bool exiting;
static void sig_handler(int sig) { exiting = true; }

static int handle_event(void *ctx, void *data, size_t sz) {
    const struct event *e = data;
    printf("%-6d %-6d %-16s %s\n", e->pid, e->ppid, e->comm, e->filename);
    return 0;
}

int main(int argc, char **argv) {
    struct ring_buffer *rb = NULL;
    struct execsnoop_bpf *skel;
    int err;

    signal(SIGINT, sig_handler);
    skel = execsnoop_bpf__open_and_load();
    if (!skel) return 1;

    err = execsnoop_bpf__attach(skel);
    if (err) goto cleanup;

    rb = ring_buffer__new(bpf_map__fd(skel->maps.events),
                          handle_event, NULL, NULL);
    if (!rb) goto cleanup;

    printf("%-6s %-6s %-16s %s\n", "PID", "PPID", "COMM", "FILENAME");
    while (!exiting) {
        err = ring_buffer__poll(rb, 100 /* ms */);
        if (err == -EINTR) { err = 0; break; }
        if (err < 0) break;
    }

cleanup:
    ring_buffer__free(rb);
    execsnoop_bpf__destroy(skel);
    return 0;
}

编译命令(在任意带 Clang 的开发机上执行一次即可):

clang -g -O2 -target bpf -D__TARGET_ARCH_x86 \
    -I. -c execsnoop.bpf.c -o execsnoop.bpf.o
bpftool gen skeleton execsnoop.bpf.o > execsnoop.skel.h
clang -O2 execsnoop.c -lbpf -o execsnoop

得到的 execsnoop 是一个约 1MB 的静态二进制,拷到任意支持 BTF 的内核上都能直接运行,不需要 Clang,不需要内核头文件,启动时间毫秒级。

5.7 vmlinux.h 生成

vmlinux.h 是从 BTF 反生成的”整合版内核头文件”,包含了你能用到的所有内核类型定义:

bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

开发时用它代替一堆 #include <linux/...>,既方便又稳定。

六、BTF 与 BPF 骨架

上一节介绍了 CO-RE 的思想,这一节展开 BTF 的工程细节。

6.1 BTF 格式

BTF 是一种类型描述格式,由以下若干”类型条目”组成:

  • BTF_KIND_INT:基本整型。
  • BTF_KIND_STRUCT / BTF_KIND_UNION:结构体、联合体。
  • BTF_KIND_ARRAY:数组。
  • BTF_KIND_ENUM:枚举。
  • BTF_KIND_FUNC_PROTO / BTF_KIND_FUNC:函数签名。
  • BTF_KIND_TYPEDEF:类型别名。

与 DWARF 相比,BTF 省略了所有行号、变量位置、表达式等调试信息,只保留”类型布局”,体积只有 DWARF 的 1%–3%。

6.2 BTF 生成链

  1. 内核编译时,pahole(dwarves 项目)从 DWARF 调试信息抽取类型并转为 BTF。
  2. BTF 被嵌入 vmlinux 的 .BTF 段。
  3. 运行时通过 /sys/kernel/btf/vmlinux 暴露给用户态。
  4. 内核模块的 BTF 以 /sys/kernel/btf/<modname> 暴露。

6.3 查看 BTF

bpftool btf dump file /sys/kernel/btf/vmlinux | grep 'STRUCT task_struct'
bpftool btf dump file /sys/kernel/btf/vmlinux format c | less

6.4 骨架生成的内部细节

bpftool gen skeleton 输出的 .skel.h 文件本质上是:

  1. .bpf.o 的字节内容用 __attribute__((section)) 内嵌到 C 文件里。
  2. 为每个 map、每个 prog 生成对应的成员指针。
  3. 提供 openloadattachdestroy 四个封装函数。

这让用户态代码不再需要 bpf_object__open_file 这类 API,加载目标 BPF 对象就像使用一个普通的 C 库。

七、生产部署:内核版本兼容性

eBPF 生态的工程现实是:哪个特性能用,取决于你集群里最老的那台机器的内核版本。下表列出了可观测性相关特性在主要内核版本中的可用性:

内核版本 发布时间 关键特性 生产定位
3.18 2014-12 eBPF 初版,kprobe 不推荐
4.1 2015-06 BPF_PROG_TYPE_KPROBE 不推荐
4.4 2016-01 perf event output 老系统最低线
4.14 2017-11 LTS,basic BPF maps、socket filter 兼容底线
4.18 2018-08 BTF 最早版本 过渡
4.19 2018-10 LTS,XDP 稳定 网络场景
5.1 2019-05 BPF_PROG_TYPE_TRACING fentry 基础
5.2 2019-07 百万指令上限(1M insns) 复杂程序
5.4 2019-11 LTS,bpf_d_path,fentry 部分 生产可用
5.5 2020-01 fentry/fexit 完整 推荐起点
5.7 2020-05 LSM BPF、ring buffer 推荐
5.8 2020-08 BTF-enabled maps、CAP_BPF 雏形 推荐
5.10 2020-12 LTS,BPF 特性大量成熟 生产主流
5.11 2021-02 task local storage 可选
5.13 2021-06 CAP_BPF 完整拆分 容器友好
5.15 2021-10 LTS,BPF 生产最佳实践 强烈推荐
6.1 2022-12 LTS,kfunc 广泛可用 未来
6.6 2023-10 LTS,BPF open-coded iterators 未来

7.1 推荐的最低内核版本

  • 排障工具场景(bcc/bpftrace 随手跑):4.15+ 可用,4.18+ 体验好。
  • agent 产品化(libbpf + CO-RE):强烈建议 ≥ 5.4,最好 ≥ 5.10。
  • 新特性开发(fentry、ring buffer、LSM BPF):≥ 5.8,最好 ≥ 5.15。
  • 安全观测场景(Tetragon 类):≥ 5.10。

7.2 CAP_BPF vs CAP_SYS_ADMIN

Linux 5.8 引入了独立的 CAP_BPF 权能(capability),5.13 完成拆分。在此之前,加载 BPF 程序需要 CAP_SYS_ADMIN,这是一种”近 root”权限,给容器授权风险极大。

5.13+ 之后,典型的 eBPF agent 容器只需要:

  • CAP_BPF:加载 BPF 程序、创建 map。
  • CAP_PERFMON:读取 perf 事件,做 profile 和 tracing。
  • CAP_NET_ADMIN:挂载网络类程序(XDP/TC)。

这远比 privileged: true 安全。

7.3 容器部署

在 Kubernetes 里部署 eBPF agent,典型的 DaemonSet 配置:

apiVersion: apps/v1
kind: DaemonSet
metadata: { name: ebpf-agent, namespace: obs }
spec:
  selector: { matchLabels: { app: ebpf-agent } }
  template:
    metadata: { labels: { app: ebpf-agent } }
    spec:
      hostPID: true
      hostNetwork: true
      containers:
      - name: agent
        image: registry.example.com/ebpf-agent:v1.2.0
        securityContext:
          capabilities:
            add: ["BPF", "PERFMON", "NET_ADMIN", "SYS_RESOURCE"]
          privileged: false
        volumeMounts:
        - { name: sys, mountPath: /sys, readOnly: true }
        - { name: debugfs, mountPath: /sys/kernel/debug }
        - { name: btf, mountPath: /sys/kernel/btf, readOnly: true }
      volumes:
      - { name: sys, hostPath: { path: /sys } }
      - { name: debugfs, hostPath: { path: /sys/kernel/debug } }
      - { name: btf, hostPath: { path: /sys/kernel/btf } }

几点关键:

  • hostPID: true:让 agent 能看到宿主机全部 PID,便于 resolve。
  • hostNetwork: true:XDP/TC 程序需要挂在宿主机网络命名空间。
  • 挂载 /sys/kernel/btf:让 libbpf 能读 BTF 做 CO-RE 重定位。
  • 避免 privileged: true,用细粒度 capability 替代。

7.4 红帽系(RHEL/CentOS)的 backport

红帽对 RHEL 有强烈的 ABI 稳定承诺,导致它会把大量新内核特性回溯(backport)到老版本号。比如 RHEL 8.6 标称 4.18,但实际 BPF 能力接近上游 5.4;RHEL 9.0 标称 5.14,能力接近上游 5.15。判断能力时不能只看 uname -r,应该直接测试某个 helper 或程序类型是否可用。

八、常用 eBPF Observability 程序

下表是生产环境最常用的 eBPF 可观测性程序清单,覆盖进程、文件、网络、存储、调度、CPU 六大方向:

程序 功能 最低内核 典型用法
execsnoop 跟踪新进程 exec 4.8 定位”谁在跑 shell”
exitsnoop 跟踪进程退出 4.9 诊断进程频繁重启
opensnoop 跟踪文件打开 4.8 找敏感文件读写者
statsnoop 跟踪 stat 调用 4.8 目录遍历风暴
biolatency 块 I/O 延迟直方图 4.9 磁盘慢
biosnoop 块 I/O 逐事件 4.9 I/O 大户
ext4slower ext4 慢操作 4.9 文件系统级延迟
xfsslower xfs 慢操作 4.9 同上
tcpconnect TCP 主动连接 4.8 出向连接审计
tcpaccept TCP 被动接受 4.8 入向连接统计
tcpretrans TCP 重传 4.8 网络质量
tcplife TCP 连接生命周期 4.15 连接时长分析
profile CPU 采样剖析 4.9 火焰图
offcputime off-CPU 时间分析 4.9 阻塞诊断
runqlat 运行队列延迟 4.9 CPU 竞争
runqlen 运行队列长度 4.9 调度压力
cpudist 进程 on-CPU 时长分布 4.9 批量长任务
funclatency 任意函数延迟 4.9 热点函数
funccount 任意函数调用次数 4.9 调用压力
hardirqs 硬中断统计 4.19 中断风暴
softirqs 软中断统计 4.19 同上
cachestat 页缓存命中率 4.9 内存压力
memleak 内存泄漏检测 4.9 长期内存问题
sslsniff SSL/TLS 明文抓取 4.18 加密流量观测
tcpstates TCP 状态转换 4.15 连接异常
netqtop 网卡队列 Top 5.0 网络队列不均
llcstat LLC 命中率 4.9 CPU 缓存分析

每一个都是一条命令就能跑起来的生产级工具。值得记忆:

sudo /usr/share/bcc/tools/execsnoop
sudo /usr/share/bcc/tools/biolatency 10 1
sudo /usr/share/bcc/tools/profile -F 99 30
sudo /usr/share/bcc/tools/tcpretrans
sudo /usr/share/bcc/tools/offcputime -df -p $(pgrep -n java) 30 > out.stacks

九、商业化 eBPF 可观测性工具

eBPF 的工程化红利已经催生了一批商业化或产品化的可观测性工具。以下几款是当前最具代表性的方案。

9.1 Pixie(New Relic)

Pixie 是 2020 年开源、后被 New Relic 收购的 Kubernetes 原生可观测性平台,定位是”无需埋点的 K8s 应用可观测”。它的架构由三部分组成:

  • Pixie 边缘模块(PEM,Pixie Edge Module):运行在每个节点的 DaemonSet,负责通过 eBPF 采集数据。
  • Vizier:集群内的控制平面,负责调度采集、存储时序数据、执行脚本。
  • 云端控制台(Cloud):可选的外部 UI 与 API 平面。

关键能力:

  • 协议自动识别:通过在 SSL_read / SSL_write 以及 tcp_sendmsg / tcp_recvmsg 上挂 uprobe/kprobe,自动解析 HTTP/1、HTTP/2、gRPC、DNS、MySQL、PostgreSQL、Redis、Cassandra、Kafka、NATS、Mongo 等十余种协议,提取请求响应字段。
  • 集群内存储:默认数据不出集群,保留窗口为分钟到小时级。
  • Pixie 脚本语言(PxL,Pixie Language):类 Python/PromQL 的 DSL,用户可以用 PxL 快速查询。
  • Kubernetes 元数据自动关联:所有请求自动打上 pod、namespace、deployment 标签。

Pixie 的优势是真正零改造:给 K8s 集群装一个 DaemonSet,五分钟内就能看到所有服务间的 HTTP/gRPC 调用链,包括 TLS 加密的流量。局限是保留窗口较短,复杂分析需要接入 New Relic 云。

9.2 DeepFlow(云杉网络,国产开源)

DeepFlow 是云杉网络开源的面向云原生应用的可观测性产品,2022 年 CNCF 沙箱项目,国产 eBPF 可观测性的代表。架构由代理(agent)和服务端(server)组成:

  • Agent:混合数据平面,eBPF + AF_PACKET + DPDK 多种采集方式并存,可以同时抓 L3–L7。
  • Server:存储(基于 ClickHouse)、查询、API。

DeepFlow 的核心创新是:

  • AutoTagging:自动把 K8s 元数据、Istio 元数据、云厂商标签注入每一条观测数据,打通拓扑。
  • 全栈分布式追踪:无需埋点即可生成跨服务的调用链,解决了手工埋点覆盖率低的问题。
  • eBPF + cBPF 双模:老内核上退化到 AF_PACKET,保证兼容性。

中文文档完善,社区活跃,国内电信运营商、银行、云厂商的采用案例较多。对内部安全审查严格、偏好自主可控的企业,DeepFlow 是第一优先级选项。

9.3 Grafana Beyla

Beyla 是 Grafana Labs 2023 年推出的 eBPF 自动插桩工具,定位明确:“把任何 HTTP/gRPC 应用变成带 RED 指标(Rate/Errors/Duration)和分布式追踪的服务,无需改代码”。架构极简:单个二进制或容器,挂到目标进程或 K8s 集群上。

核心能力:

  • 自动协议识别:HTTP/1、HTTP/2、gRPC、SQL。
  • 直接产出 RED 指标:以 OTLP(OpenTelemetry Protocol)或 Prometheus 格式导出,无缝接入 Grafana 栈。
  • 分布式追踪:自动生成 span,传播 W3C Trace Context。
  • 低代码配置
print_traces: true
otel_metrics_export:
  endpoint: http://otel-collector:4317
discovery:
  services:
    - name: my-service
      open_ports: "8080"

Beyla 是目前把 eBPF 零侵入和 OpenTelemetry 生态结合得最好的开源方案之一。

9.4 Parca

Parca 是 Polar Signals 开源的持续性能剖析(continuous profiling)平台,核心是一个基于 eBPF 的采样剖析器 parca-agent,以 19Hz 或 99Hz 的频率采集所有进程的调用栈,并使用 DWARF 栈回溯(DWARF-based stack unwinding)在没有帧指针(frame pointer)的情况下仍能拿到准确栈信息。

这一点非常关键:现代发行版为了代码密度,默认不保留帧指针(-fomit-frame-pointer),传统 perf 采样在这种二进制上栈回溯会断。Parca 通过解析 ELF 的 .eh_frame/.debug_frame 段,在 eBPF 里实现 DWARF 状态机,能在不改编译选项的情况下拿到正确栈。

Parca 的输出格式对齐 pprof,正在与 OpenTelemetry Profiles 信号规范融合,未来会成为”可观测性第四支柱”的标准实现之一。

十、国内落地案例

国内大型互联网公司和云厂商是 eBPF 可观测性的重度使用者:

  • 阿里巴巴内核团队:在 Alibaba Cloud Linux(龙蜥 / OpenAnolis 上游贡献者)中维护了大量 eBPF backport,把许多上游 5.x 特性带回 4.19 基线。阿里自研的 SysOM、SysAK 等诊断工具大量使用 eBPF,用于宿主机级别的诊断与性能剖析。
  • 字节跳动:KernelShield 系统结合 eBPF 做容器运行时安全与可观测,实现宿主机级别的调用链追踪与异常行为检测;内部还有面向性能诊断的 eBPF 工具链,在数十万台服务器上常态化运行。
  • 腾讯云:TKE、EKS 系列产品使用 eBPF 做容器网络可观测,结合 Cilium 和自研组件提供跨可用区、跨集群的网络诊断。
  • 百度、美团:内部均有基于 libbpf + CO-RE 的自研 agent,用于持续性能剖析和网络可观测。
  • 中国移动、中国电信:DeepFlow 在国内运营商 5G 核心网、IT 系统中有广泛落地,用于云网一体化的可观测性。

这些案例反映了一个事实:在国内生产环境做 eBPF,内核版本兼容性是最大障碍。CentOS 7(3.10)存量巨大,大规模升级不现实,因此国内厂商普遍走两条路:一是在自研内核(如 Alibaba Cloud Linux、TencentOS Server、openEuler)里 backport eBPF 特性;二是在 agent 里做能力降级,高版本内核用 fentry + ring buffer,低版本内核退化到 kprobe + perf buffer。

十一、工程坑点

以下是 eBPF 可观测性在生产里最常踩的坑,按严重程度排序。

11.1 内核版本与 backport 不一致

uname -r 报 4.18,但能否用 fentry/ring buffer 取决于发行版是否回溯。RHEL 8/9、阿里云 Linux、TencentOS Server 都做了大量 backport。务必在 agent 启动时做能力探测(feature probe),而不是只看版本号。libbpf 提供了 libbpf_probe_bpf_helper()libbpf_probe_bpf_prog_type() 等 API。

11.2 验证器复杂度上限

验证器对程序复杂度有硬限制:

  • Linux 5.2 之前:4096 条指令上限,任何稍复杂的程序都会卡。
  • Linux 5.2+:百万指令上限(1M insns),但分支探索上限仍然存在。
  • 程序中有循环时,必须用 bpf_loop 辅助或 #pragma unroll,否则会被拒。

典型症状是”在开发机上过了,在生产上报 BPF program is too large“,根因通常是内联不同、LLVM 优化级别不同。

11.3 栈回溯与帧指针

上文提到,现代发行版默认关闭帧指针。eBPF 采样拿到的 ustack 在这种进程上会只有 1–2 帧。解决方案:

  • 应用侧重新编译时加 -fno-omit-frame-pointer(Ubuntu 24.04+ 已默认开启)。
  • 使用支持 DWARF 栈回溯的剖析器(如 Parca、Polar Signals、字节跳动开源的 vArmor)。
  • 接受栈不全,用调用关系聚合弥补。

11.4 容器内权限问题

早期内核(< 5.8)必须 CAP_SYS_ADMIN,在多租户集群里这是高风险授权。升级到 5.13+ 后可以用细粒度 CAP_BPF + CAP_PERFMON + CAP_NET_ADMIN 替代。Kubernetes PodSecurityStandard “restricted” profile 下,eBPF agent 必须用独立命名空间或专用 namespace 运行。

11.5 Map 与内存限制

BPF 映射默认受 RLIMIT_MEMLOCK 限制(Linux 5.11 前),生产 agent 通常需要设置:

struct rlimit rlim = { .rlim_cur = RLIM_INFINITY, .rlim_max = RLIM_INFINITY };
setrlimit(RLIMIT_MEMLOCK, &rlim);

Linux 5.11 之后改为 cgroup memcg 统计,这个问题自动消失。

11.6 BTF 不可用

CentOS 7 / Ubuntu 18.04 等老内核没有内置 BTF。CO-RE 无法工作。解决方案:

  • 使用 BTFHub 或 btfgen 生成”最小 BTF”(只含你程序用到的字段),随 agent 一起分发。
  • 或针对这些机器退化到 bcc 模式,牺牲启动速度换兼容性。

11.7 热路径开销

高频函数上挂 eBPF 程序会放大开销。实测案例:

  • vfs_read 上挂 kprobe,NVMe 小 I/O 吞吐下降约 5%–10%。
  • tcp_sendmsg 上挂 uprobe(到 SSL_write)+ 简单记录,HTTPS QPS 下降约 3%–8%。
  • sched:sched_switch 上做 per-CPU 计数(无打印),开销 < 1%。

规则:尽量使用 tracepoint 和 fentry;尽量在内核态过滤;尽量用 per-CPU map 减少竞争

11.8 ring buffer 与 perf buffer 选择

  • Linux < 5.8:只有 perf buffer,需要 per-CPU 分配,内存浪费;排序困难。
  • Linux 5.8+:ring buffer 提供全局 MPSC 队列,更省内存,事件天然有序。
  • 生产建议:目标内核 ≥ 5.8 时一律用 ring buffer。

11.9 符号解析与 JIT 语言

剖析 Java、Node.js、Python 等 JIT 或动态语言时,eBPF 拿到的地址无法直接解析成符号。解决方案:

  • Java:使用 perf-map-agentasync-profiler 生成 /tmp/perf-<pid>.map 文件(agent 应读取 hostPath),然后 agent 解析。
  • Node.js:开启 --perf-basic-prof
  • Python:用 USDT 或 py-spy 等工具提供符号。

11.10 与 cgroup v2 的交互

cgroup v2 下,bpf_get_current_cgroup_id() 返回的是统一 cgroup ID;v1 下每个控制器有自己的 ID,易混淆。容器内 ID 解析要用 bpf_get_current_ancestor_cgroup_id(),并用 bpffs 固定住 cgroup 引用。

十二、选型建议与落地清单

12.1 决策矩阵

场景 推荐方案 理由
生产 SRE 一次性排障 bpftrace 单行命令 启动快,语义清晰
开发调试、短期工具 bcc Python 脚本 生态丰富,教程多
agent 产品化、发布到大规模集群 libbpf + CO-RE 零依赖、可移植、启动快
Kubernetes 应用可观测 Pixie / DeepFlow / Beyla 零埋点、开箱即用
持续性能剖析 Parca / Polar Signals DWARF 栈回溯
网络深度可观测 Cilium Hubble / DeepFlow L3–L7 全栈
安全与合规观测 Tetragon / Falco LSM BPF 可靠
国内主权可控 DeepFlow + 自研 libbpf agent 可审计、可改造

12.2 生产就绪清单

在上线一个 eBPF 可观测性方案之前,建议按以下清单检查:

12.3 分阶段采纳路线

推荐的落地节奏分三阶段:

  1. 体验期(1–2 周):在预发环境装 bpftrace 和 bcc-tools,开始用 execsnoopbiolatencytcpconnectprofile 四个命令解决日常排障。这一步几乎零成本,SRE 能立刻受益。
  2. 工具化期(1–2 月):选 1–2 个开源产品试点,推荐 DeepFlow 或 Grafana Beyla。覆盖 1 个典型业务集群,对比有无 eBPF 可观测的排障效率。
  3. 自研或深度集成期(3–6 月):根据业务特点自研 libbpf + CO-RE agent,解决开源产品覆盖不到的特殊场景(如私有协议、特殊内核版本、内部 trace 上下文传播)。同时把数据平面接入统一可观测平台。

每一步都要有明确的成功指标:排障 MTTR 下降、线上 incident 识别提前时间、高价值指标覆盖率。避免一上来就 all-in 自研。

十二附、常见问答与工程补充

这一节汇总在培训、代码评审、线上事故复盘中反复被问到的 eBPF 可观测性问题,给出工程答案。

附.1 eBPF 程序真的可以做到”永不崩内核”吗

几乎可以,但不是绝对。验证器会拒绝所有它能静态证明为不安全的程序。已知的”验证器漏洞导致内核崩溃”CVE 在 Linux 5.x 早期有过几例(如 CVE-2021-3490、CVE-2022-23222),都与操作数边界计算的不完备有关。现代内核(≥ 5.15)加入了更严格的寄存器范围跟踪后基本稳定。风险点在于:

  • 使用了非主线内核(backport 来源不明)。
  • 使用了 unprivileged BPF(很多发行版已默认禁用)。
  • 内核存在未公开漏洞。

生产建议:升级到受支持的 LTS 内核,禁用 kernel.unprivileged_bpf_disabled = 1 之外的放行配置,agent 代码走静态分析与复核流程。

附.2 eBPF 能替代 perf / ftrace 吗

互补而不是替代。

  • perf 对硬件性能计数器(PMU)的支持仍然不可替代,CPU 采样的大多数场景都是”perf_event + eBPF”组合。
  • ftrace 的函数图形化追踪(function graph tracer)在深度调试内核调用路径时仍然更直观。
  • eBPF 的优势在于可编程、可聚合、可结构化输出,对运维 agent 和产品化工具更友好。

日常排障里三者共存,不需要二选一。

附.3 bpftrace 脚本怎么调试

几个实用技巧:

  • -d 看 AST,-dd 看 LLVM IR,-v 看加载过程。
  • printf + 简化探针逐步收敛。
  • 验证器报错时,看 dmesg | tail,里面有完整的 verifier log。
  • 复杂脚本先用 bpftrace -l 列出可用探针,再拼装。

附.4 libbpf 的 C 程序怎么做单元测试

BPF 程序很难在传统意义上”单测”,因为它运行在内核里。业界常见做法:

  • 用户态函数单测:把事件处理逻辑抽成普通 C 函数,在用户态用 gtest / Unity 测试。
  • 集成测试:启动一个容器,在里面加载 BPF 程序,跑一段已知行为的负载(curlddstrace),断言 ring buffer 输出。
  • CI 用 KVM 多内核:用 GitHub Actions + actions-ebpf 之类项目,在多个内核版本上跑集成测试。

Cilium、Tetragon 都有这方面成熟的 CI 实践可以参考。

附.5 为什么我的 uprobe 没触发

常见原因:

  1. 目标函数被内联了(特别是 inline 或 LTO 编译的库)。
  2. 目标函数被链接器去重(--gc-sections)。
  3. 符号名被 C++ 修饰(mangled),需要用 mangled 名或 /usr/bin/mybin:_ZN...
  4. 目标二进制有 PIE,地址偏移不同,但 libbpf 已处理,不是问题。
  5. 目标库在容器内路径与挂载路径不一致。

排查工具:readelf -Ws /path/to/lib | grep symbolobjdump -d

附.6 ring buffer 丢事件怎么办

eBPF ring buffer 是单生产者多消费者(SPMC)结构,生产速度超过消费速度时会直接丢弃并返回 -E2BIG。应对策略:

  • 加大 ring buffer 大小(建议 2–16MB)。
  • 在内核侧用 per-CPU map 先聚合,减少事件数量。
  • 用户态消费要用独立线程,ring_buffer__poll 循环不能被其他工作阻塞。
  • 暴露一个”丢弃计数”指标到 Prometheus,线上一旦非零要告警。

附.7 BTF 不可用的降级方案

目标机器上没有 /sys/kernel/btf/vmlinux 时的选择:

  • BTFHub:预生成了几乎所有主流发行版的 BTF 文件,直接下载 .btf 放到 agent 目录。
  • BTFGen:只为你的程序生成”最小 BTF”,文件大小几十 KB,可随 agent 分发。
  • 双二进制:分别提供 CO-RE 版本和”老内核 bcc 版本”,根据 uname -r 自动选择。

附.8 eBPF 数据如何接入 OpenTelemetry

几条主流路径:

  1. agent 侧直接产出 OTLP:Beyla 是典型例子。
  2. agent 产出私有格式 → 本地 Collector 转换 → OTLP 上行:DeepFlow、Pixie 属此类。
  3. OpenTelemetry eBPF Profiler(OTel 2024 投票通过将 elastic/otel-profiling-agent 并入):第四支柱 profiles 的参考实现。

对新项目,优先选方案 1 或 3,减少数据搬运。

附.9 eBPF 对 ARM64 支持如何

主流 ARM64(aarch64)服务器(鲲鹏、飞腾、Ampere Altra、AWS Graviton)对 eBPF 支持良好。几个要点:

  • kprobe、tracepoint、fentry 与 x86 同步可用。
  • XDP 对具体网卡驱动依赖较强,需要驱动支持 XDP native mode 才有最佳性能。
  • 内核 5.10+ 在 ARM64 上对 BPF JIT 覆盖完整。
  • 编译 CO-RE 程序时 -D__TARGET_ARCH_arm64

国产化替代场景中,ARM64 上的 eBPF 可观测性已完全可用,龙蜥、openEuler 都做了广泛适配。

附.10 eBPF 与 WebAssembly 有什么关系

两者都是”带验证器的虚拟机字节码”,但定位完全不同:

  • eBPF 运行在内核态,面向系统层可观测、网络、安全。
  • Wasm 运行在用户态沙箱,面向跨平台业务逻辑。

2023 年起有一些尝试把 eBPF 程序编译成 Wasm 让其在用户态运行(如 wasm-bpf),但这属于跨界实验,不要把它当成 eBPF 的替代品。对可观测性工程师而言,只关心 eBPF 本身即可。

十二又、一个完整落地示例:Java 服务的 HTTP 可观测

以一个真实场景贯穿前面所有概念,落地一个”不改 Java 代码的 HTTP 可观测”方案。

场景

  • 目标:对所有 Java HTTP 服务收集 RED(Request Rate / Error / Duration)指标,粒度到 URI。
  • 约束:不改业务代码、不加 Java agent、不重启服务。
  • 集群:Kubernetes 1.28,节点内核 5.15(Ubuntu 22.04 LTS)。

方案选型

候选有三:

  1. Pixie:覆盖完整,数据留在集群。
  2. Grafana Beyla:直接产 OTLP,对接现有 Grafana Stack。
  3. 自研 libbpf agent:完全可控,但开发成本最高。

假设我们已有 Grafana/Prometheus/Tempo,选方案 2。

关键配置

DaemonSet 部署 Beyla:

apiVersion: apps/v1
kind: DaemonSet
metadata: { name: beyla, namespace: obs }
spec:
  selector: { matchLabels: { app: beyla } }
  template:
    metadata: { labels: { app: beyla } }
    spec:
      hostPID: true
      serviceAccountName: beyla
      containers:
      - name: beyla
        image: grafana/beyla:1.6.0
        securityContext:
          capabilities:
            add: ["BPF", "PERFMON", "NET_ADMIN", "SYS_PTRACE", "CHECKPOINT_RESTORE"]
          privileged: false
          runAsUser: 0
        env:
        - { name: BEYLA_CONFIG_PATH, value: /config/beyla.yaml }
        - { name: BEYLA_KUBE_METADATA_ENABLE, value: "true" }
        volumeMounts:
        - { name: config, mountPath: /config }
        - { name: sys, mountPath: /sys, readOnly: true }
      volumes:
      - { name: config, configMap: { name: beyla-config } }
      - { name: sys, hostPath: { path: /sys } }

Beyla 配置:

attributes:
  kubernetes:
    enable: true
discovery:
  services:
    - k8s_namespace: prod
      k8s_deployment_name: "^(order|payment|checkout)-.*"
otel_metrics_export:
  endpoint: http://otel-collector.obs:4317
  interval: 10s
otel_traces_export:
  endpoint: http://otel-collector.obs:4317
  sampler: { name: parentbased_traceidratio, arg: "0.1" }
routes:
  unmatched: heuristic
  patterns:
    - /api/v1/orders/:id
    - /api/v1/payments/:id/confirm

工作原理

部署后,Beyla 会:

  1. 扫描节点上所有运行的进程,匹配 K8s 标签和可执行名。
  2. 识别 Java 进程的 libc read/writetcp_sendmsg/tcp_recvmsg,挂 uprobe/kprobe。
  3. 解析 HTTP 请求行,提取 method、path、status、耗时。
  4. 按路由归类(通过 routes.patterns 归一化),生成 http_server_request_duration_seconds_{bucket,count,sum} 等 OTLP 指标。
  5. 通过 K8s API 把 pod/namespace/deployment 注入标签。
  6. 以 10s 间隔推送到 OTel Collector。

对 HTTPS 的处理

Beyla 会对 JVM 内置 SSL 通过 Java 层定位失败,因此建议 Java 使用 OpenSSL(通过 netty-tcnative 或 Wildfly OpenSSL);或退化到”仅看响应时间+状态码,不看 body”模式。

容量评估

实测数据(参考 Grafana 官方 benchmark,QPS 20k 的 Java 服务):

  • Beyla CPU 占用约 0.3 核/节点。
  • 内存 约 80MB/节点。
  • 对业务 p99 延迟影响 < 3%。
  • Prometheus 指标数量:每个 service/route 约 15 条。

上线检查单

这个例子刻画了 eBPF 可观测性”从选型到落地”的完整链路。它不是最强大的方案(自研 libbpf 更可控),也不是最省事的方案(托管 SaaS 更省事),但它在能控制性、部署成本、开源生态、国内可用性上达到了一个良好平衡,是多数企业起步阶段的合理选择。

十三、参考资料

  1. Linux Kernel Documentation, “BPF Documentation”, https://www.kernel.org/doc/html/latest/bpf/
  2. Brendan Gregg, BPF Performance Tools, Addison-Wesley, 2019.
  3. Cilium Project, “BPF and XDP Reference Guide”, https://docs.cilium.io/en/stable/bpf/
  4. bpftrace Authors, “bpftrace Reference Guide”, https://github.com/bpftrace/bpftrace/blob/master/docs/reference_guide.md
  5. Andrii Nakryiko, “BPF CO-RE (Compile Once – Run Everywhere)”, https://nakryiko.com/posts/bpf-portability-and-co-re/
  6. libbpf Project, https://github.com/libbpf/libbpf
  7. libbpf-bootstrap Project, https://github.com/libbpf/libbpf-bootstrap
  8. IOVisor BCC Project, https://github.com/iovisor/bcc
  9. Pixie Documentation, https://docs.px.dev/
  10. DeepFlow Documentation, https://deepflow.io/docs/zh/
  11. Grafana Beyla Documentation, https://grafana.com/docs/beyla/latest/
  12. Parca Documentation, https://www.parca.dev/docs/
  13. Isovalent Tetragon, https://tetragon.io/
  14. Falco Security, https://falco.org/
  15. BTFHub Project, https://github.com/aquasecurity/btfhub
  16. Alexei Starovoitov, “BPF as a universally applicable engine”, LWN, 2019.
  17. Arnaldo Carvalho de Melo, “pahole and BTF deduplication”, LPC 2020.
  18. Yonghong Song, “BPF Type Format (BTF)”, kernel.org, 2018.
  19. CNCF, “eBPF Landscape”, https://landscape.cncf.io/
  20. OpenAnolis Community, “Alibaba Cloud Linux eBPF Backports”, https://openanolis.cn/

上一篇Events 与变更关联:CloudEvents、发布打点、K8s 事件

下一篇网络可观测性:Cilium Hubble、Pixie、DeepFlow、Tetragon

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-22 · architecture / observability

【可观测性工程】网络可观测性:Cilium Hubble、Pixie、DeepFlow、Tetragon

从 L3/L4/L7 三层观测视角出发,讲解 eBPF socket filter/tc/XDP 的数据采集机制,深入 Cilium Hubble 流日志与指标体系、Tetragon 安全可观测、Pixie 自动化协议解析、国产 DeepFlow 的架构与实践,以及 TLS 解密、HTTP/2 解析、服务拓扑自动发现等核心工程挑战。

2026-04-22 · architecture / observability

持续性能分析(Continuous Profiling):Parca、Pyroscope、Grafana Beyla

深入剖析持续性能分析(Continuous Profiling)的原理、架构与落地实践,覆盖 Parca、Pyroscope、Grafana Beyla 三大主流方案,包含 eBPF 采样、符号解析、火焰图、差异分析以及字节跳动、美团的生产案例与工程坑点。

2026-04-22 · architecture / observability

可观测性工程

从 Metrics、Logs、Traces 到 Profiling、eBPF、OpenTelemetry 与 SLO 治理,面向中国工程团队的可观测性系统化手册。