学习目标
学完本章你应该能够:
- 用一句话讲清 eBPF 是什么,以及它“不改内核源码就能在内核跑程序”靠的是钩子点、加载式字节码、verifier 沙箱三件事。
- 背下 eBPF 程序的完整生命周期(编译 → 加载 → verifier → JIT → attach → 运行 → map 通信),并说清 verifier 的几条硬限制为什么是“踩坑高发区”。
- 区分常用挂载点(kprobe / tracepoint / uprobe / fentry / xdp / tc / lsm)的适用场景,理解“kprobe 是不稳定 ABI、tracepoint 更稳定”的工程经验。
- 讲清 BPF Map 为什么是“无状态程序”的通信中枢,能根据访问模式选对 Map 类型(HASH / ARRAY / PERCPU_* / RINGBUF)。
- 把 eBPF 与 AIOps 串起来:说明它如何补齐“内核视角”这一层数据,并能在面试里讲出“先本地聚合异常片段、再喂给 LLM 推理”的落地流程。
前置知识:
- 基本的 Linux 使用经验(命令行、进程、系统调用概念)
- C 语言基础(能看懂简单片段即可,不必会写复杂内核代码)
- 可观测性基本概念:指标 / 日志 / 链路追踪
- 一点云原生常识:Kubernetes、DaemonSet、Sidecar
本章你会动手做的事:
- 在脑子里跑一遍“写个统计 openat 次数的 BPF 程序”的完整链路,标出哪一步会过 verifier。
- 用一张图画出 Node Agent 的 DaemonSet 架构(eBPF 程序组 → 本地聚合 → 多通道上报)。
- 给“偶发慢请求”场景设计一条 eBPF 采集 → LLM 推理的根因定位链路,写出要采哪几个维度。
什么是 eBPF
我刚接触 eBPF 的时候,第一反应是:这玩意儿凭什么能在内核里跑程序,还不用改内核源码、不用重新编译、不用重启机器?听起来像黑魔法。后来自己写了个 hello world 跑在 kprobe:__x64_sys_openat 上才搞明白——它本质上是在内核里开了一个"安全沙箱",你写一段受限的字节码塞进去,内核在特定钩子点帮你执行。
eBPF(extended Berkeley Packet Filter)名字里带 “Packet Filter”,是因为它最早真是用来过滤网络包的(cBPF 时代,tcpdump 那套)。但后来内核社区发现这玩意儿潜力巨大,于是扩展成通用内核可编程框架。现在你拿它做可观测性、做安全审计、做网络转发、做性能分析,都能干。
白话类比:把 Linux 内核想象成一栋不许外人进的“机房”。以前你想看机房里发生了什么,只能等管理员(内核开发者)给你开个后门、重新装修(改源码、重编译、重启)。eBPF 相当于机房墙上装了一排“带安检的投递口”(钩子点)——你写好一段受限小程序(字节码),经过安检(verifier)后塞进去,它就在机房里替你观察、记录,还不用动机房结构。这就是“零侵入可观测”的魔力。
凭什么能做到"不改内核源码就跑程序"?我个人的理解是三点:
- 内核预留了大量钩子点。kprobe、tracepoint、uprobe、XDP、TC、cgroup、LSM……这些点内核早就埋好了,eBPF 程序挂上去就行,不需要你动内核一行代码。
- 程序是"加载式"的。你把 C 写的源码编译成 BPF 字节码,通过
bpf()系统调用喂给内核,内核验证通过后 JIT 成机器码,挂在钩子上。整个过程是动态的,跟你insmod一个内核模块不一样,eBPF 程序不需要内核符号表对齐。 - 沙箱 + verifier。这是关键——内核不信任你写的程序,它先静态分析一遍,确保你不会死循环、不会越界、不会解引用空指针,才让你跑。所以"安全"是它能在内核跑的前提。
这些钩子点就像机房墙上那一排投递口,eBPF 程序挂在不同的口上就能看到不同的事:
flowchart LR
K[kprobe / kretprobe
内核函数] --> E[eBPF 程序]
T[tracepoint
稳定跟踪点] --> E
U[uprobe / uretprobe
用户态函数] --> E
X[xdp / tc
网络包] --> E
L[lsm
安全钩子] --> E
E --> H[(内核态事件采集
+ map 通信)]举几个能跑的实际能力例子,这样更有体感:
- 统计每个进程打开了哪些文件:挂
kprobe:do_filp_open,拿到pid和文件名,丢进 map。你不用改任何一个应用,全机器的文件操作一目了然。 - 统计 TCP 连接建立耗时:挂
kprobe:tcp_v4_connect记录起始时间,kretprobe算差值,按进程出直方图。线上排查"为啥这个服务连数据库慢"的时候特别好用。 - 拦截恶意 syscall:挂
tracepoint:syscalls:sys_enter_openat,发现某个容器在试图读/etc/shadow,直接拒绝。Falco 就是干这个的。 - XDP 在网卡驱动层做 DDoS 防护:包还没进协议栈就被你 drop 掉了,性能比 iptables 高一个量级。
- HTTP 协议级延迟统计:挂 sock 上的数据传输,解析 HTTP 头里的 path/method/status,按服务对聚合延迟。Cilium/Hubble 就是这么干的。
eBPF 的核心价值,我用一句话总结:它把"内核可观测性"和"内核可编程性"从"改源码重新编译内核"降到了"写个脚本热加载"。这个能力级别在云原生时代是颠覆性的。
- 内核级可编程:可以在内核关键路径插入探针。
- 安全沙箱:通过验证器确保程序不会导致内核崩溃或死循环。
- 高性能:JIT 编译为本地机器码执行,开销极低。
- 零侵入:无需修改应用程序代码即可采集数据。
eBPF 工作原理
我之前一直觉得"工作原理"这种章节特别无聊,直到自己写 BPF 程序被 verifier 拒了十几次,才回过头来认真把流程理了一遍。下面这套流程我建议你背下来,因为后面调任何 BPF 工具出问题,最后都能归到这流程的某一环上。
白话类比:eBPF 程序的一生像“投稿发表”。你写稿(C 源码)→ 排版成 PDF(clang 编译字节码)→ 编辑初审(verifier 静态审查,不过就退稿)→ 印刷上架(JIT 编译机器码)→ 摆上报架(attach 钩子)→ 读者翻看触发(事件触发执行)→ 读者留言进意见箱(map)→ 你回收意见箱(用户态读 map)。
完整流程文字图(我画得糙,但每一步都对得上代码):
[用户态]
1. 用 C/Rust 写 BPF 程序源码
2. clang --target=bpf 编译成 .o(BPF 字节码,ELF 格式)
3. 用户态加载器(libbpf/bcc/bpftrace)解析 ELF
|
| bpf(BPF_PROG_LOAD, &attr, sizeof(attr))
v
[内核态]
4. 内核接收字节码,先跑 verifier(验证器)
- 检查指令数上限(100 万条)
- 检查控制流图(CFG),所有路径必须能收敛
- 检查循环必须有上界(5.3+ 才允许 bounded loop)
- 检查指针操作:不能解引用未检查的指针
- 检查 map 访问边界
- 检查 helper 调用参数类型匹配
5. verifier 通过后,JIT 编译为目标架构机器码
- x86_64 / arm64 / riscv 都有对应 JIT 后端
- 没启用 JIT 就走解释器(慢 5-10 倍)
6. 程序 attach 到指定 hook 点(kprobe/tracepoint/xdp/...)
|
v
[运行时]
7. 内核事件触发 -> BPF 程序执行
8. 程序通过 helper 读写 map / 调用内核函数
9. 用户态通过 bpf(BPF_MAP_LOOKUP_ELEM) 读 map
或通过 ringbuf/perf_event_array 收事件
把上面这套“投稿发表”流程画成图,每一步都对得上:
flowchart TD
A[用户态: C/Rust 写 BPF 源码] --> B[clang 编译成 .o 字节码]
B --> C[加载器解析 ELF]
C -->|bpf BPF_PROG_LOAD| D[内核: verifier 验证]
D -->|不通过 直接退稿| X[加载失败]
D -->|通过| E[JIT 编译为机器码]
E --> F[attach 到 hook 点]
F --> G[事件触发 BPF 执行]
G --> H[helper 读写 map]
H --> I[用户态读 map / ringbuf]这里有个坑很多人第一次踩:你以为 verifier 是"运行时检查",其实是加载时静态分析。也就是说它在你 bpf() 系统调用那一刻就把程序翻来覆去扫了一遍,能跑就 JIT,不能跑就直接拒绝加载。所以 BPF 程序"运行时报错"基本不存在——要么加载不进来,要么跑起来不会崩(顶多逻辑错)。
verifier 的几条硬限制我列一下,这是踩坑高发区:
| 限制项 | 数值/规则 | 踩坑提示 |
|---|---|---|
| 指令数上限 | 100 万条(旧内核 4096 条) | 复杂逻辑要拆分,或换 BTF 之后的 bounded loop |
| 栈空间 | 512 字节 | 别在 BPF 里放大数组,超过就 BPF stack limit exceeded |
| 循环 | 必须有静态可证明的上界 | 5.3 之前根本不允许循环,写了直接拒 |
| map 访问 | 必须检查边界(lookup 返回值要 NULL 检查) | 不检查直接编译失败,错误信息会指向那一行 |
| 指针解引用 | 必须先验证非 NULL | packet/data 指针要 bounds check |
| 程序类型与 helper | 不同 type 能调的 helper 不一样 | kprobe 不能调 bpf_xdp_adjust_head,硬上就拒 |
| 最大 map 数量 | 受 RLIMIT_MEMLOCK 限制 | 生产环境记得调大,默认 64KB 太小 |
| 程序大小 | 单程序 1MB(含指令+map 元数据) | 大 map 拆开多个,别一个塞满 |
说实话 verifier 是 eBPF 设计中最聪明的一步——它把"安全"从运行时挪到了加载时,这才有底气让任意用户在内核跑代码。代价就是开发体验:你写个 BPF 程序被 verifier 拒掉十几次是常态。我自己第一次写一个比较复杂的 BPF 程序,被拒了 20 多次,最后改到简洁才过。
eBPF 程序可以挂载到多种事件源,我把常用的列一下,每种给个真实使用场景:
| 事件源 | 说明 | 典型场景 |
|---|---|---|
| kprobe/kretprobe | 内核函数入口/返回点 | 统计函数耗时、抓参数 |
| uprobe/uretprobe | 用户态函数入口/返回点 | 抓 Go/Python 应用函数,比如 SSL_read 解密 HTTPS |
| tracepoint | 内核预定义跟踪点 | syscall 追踪(比 kprobe 稳定) |
| fentry/fexit | 更高效的函数入口/返回 | 替代 kprobe(需要 BTF,性能更好) |
| xdp | 网络数据包处理 | DDoS 防护、LB、防火墙 |
| tc | 流量控制 | 流量分类、限速、NAT |
| cgroup/skb | cgroup 级别网络事件 | 容器网络观测、策略 |
| lsm | Linux 安全模块钩子 | 安全策略拦截,比 audit 强 |
⚠️ 新手必踩的坑:kprobe 是不稳定 ABI。内核升级后函数名可能变(比如
__x64_sys_openat在不同版本叫法不同,arm64 又是另一套名字),生产环境能用 tracepoint 就别用 kprobe,tracepoint 是稳定 ABI,跨版本兼容好得多。
eBPF 关键数据结构:Map
BPF 程序本身是无状态的(每次触发都是独立执行),要跨事件共享数据、跟用户态通信,全靠 map。map 本质是内核里的一段内存,BPF 程序和用户态程序都能通过 helper 访问。
白话类比:BPF 程序像在机房里巡逻的临时工,每次进来只干一件事、记不住上次。Map 就是他们和外面人共用的“留言板”(内核里的一段内存)——巡逻工把发现写上去,外面的程序来读;外面也能往板上写配置让巡逻工照做。没有这块板,临时工之间、临时工和外界就完全失联。
我开始学的时候一直没搞懂为啥要分这么多类型,后来写多了才明白:不同 map 类型对应不同访问模式,性能差很多。比如高频事件你就别用 HASH,用 PERCPU_HASH 自己 per-cpu 计数再合并,能省掉原子操作的锁开销。
这张图把“BPF 程序 ↔ Map ↔ 用户态”的通信画出来:
flowchart TD
P[BPF 程序 内核态
每次触发独立执行] <-->|helper 读写| M[(Map
内核内存)]
M <-->|bpf MAP LOOKUP / UPDATE| U[用户态程序
读取/下发配置]常见类型及我的使用习惯:
BPF_MAP_TYPE_HASH:通用键值对,适合低频更新(如配置、状态表)。BPF_MAP_TYPE_ARRAY:下标访问,查找 O(1),适合"按 PID 查进程信息"这种。BPF_MAP_TYPE_PERCPU_HASH/PERCPU_ARRAY:per-cpu 版本,高频计数器必用。BPF_MAP_TYPE_RINGBUF:环形缓冲区,5.8+ 替代 PERF_EVENT_ARRAY,事件流首选。BPF_MAP_TYPE_PERF_EVENT_ARRAY:老牌事件传递通道,现在新代码建议用 ringbuf。BPF_MAP_TYPE_LRU_HASH:带 LRU 淘汰的哈希表,适合"每连接状态"这种会无限增长的场景。
下面这个 C 程序片段演示了 HASH 和 ARRAY 的实际用法(追踪每个 PID 的 openat 调用次数):
// bpf_prog.bpf.c
#include <linux/bpf.h>
#include <linux/sched.h>
#include <bpf/bpf_helpers.h>
// 为什么用 ARRAY:PID 是连续整数,下标访问 O(1),比 HASH 快
// 注意:ARRAY 大小固定,这里设 65536,PID 大于这个值会访问失败
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__type(key, u32);
__type(value, u64);
__uint(max_entries, 65536);
} pid_open_count SEC(".maps");
// 为什么用 HASH:这里 key 是 (pid, uid) 组合,无法用数组下标
// 适合"按 pid+uid 维度统计",新增条目自动插入
struct call_key {
u32 pid;
u32 uid;
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, struct call_key);
__type(value, u64);
__uint(max_entries, 10240);
} pid_uid_count SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_openat")
int trace_openat(void *ctx)
{
u32 pid = bpf_get_current_pid_tgid() >> 32; // 高 32 位是 tgid(即 PID)
u32 uid = bpf_get_current_uid_gid();
u64 *val, zero = 0;
// ARRAY 查找:永远返回有效指针(不存在则返回零值内存)
val = bpf_map_lookup_elem(&pid_open_count, &pid);
if (val) {
// 这里不担心并发:tracepoint 是 per-cpu 执行,但跨 CPU 仍有竞争
// 严格场景下应该用 PERCPU_ARRAY 或 __sync_fetch_and_add
__sync_fetch_and_add(val, 1);
}
// HASH 查找:可能返回 NULL,必须检查
struct call_key k = { .pid = pid, .uid = uid };
val = bpf_map_lookup_elem(&pid_uid_count, &k);
if (!val) {
// 不存在则初始化为 0
bpf_map_update_elem(&pid_uid_count, &k, &zero, BPF_ANY);
val = bpf_map_lookup_elem(&pid_uid_count, &k);
if (!val) return 0; // 仍然失败说明 map 满了,跳过
}
(*val)++;
return 0;
}
char LICENSE[] SEC("license") = "GPL";
用户态读取 map 的代码(用 libbpf 的 skeleton,简化版):
// reader.c
#include <bpf/libbpf.h>
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
int main() {
struct bpf_object *obj = bpf_object__open_file("bpf_prog.o", NULL);
if (libbpf_get_error(obj)) {
fprintf(stderr, "open bpf obj failed\n");
return 1;
}
if (bpf_object__load(obj)) {
fprintf(stderr, "load bpf obj failed (verifier 拒了?看 dmesg)\n");
return 1;
}
int map_fd = bpf_object__find_map_fd_by_name(obj, "pid_open_count");
printf("Tracing openat... Ctrl-C to stop\n");
while (1) {
sleep(5);
printf("=== snapshot ===\n");
for (uint32_t pid = 1; pid < 65536; pid++) {
uint64_t val = 0;
if (bpf_map_lookup_elem(map_fd, &pid, &val) == 0 && val > 0) {
printf(" PID %u: %llu opens\n", pid, (unsigned long long)val);
}
}
}
return 0;
}
踩坑提示:
- ARRAY 类型即使没初始化,
lookup也会返回指向零值的指针,所以你(*val)++直接就能用,不会段错误。 - HASH 类型
lookup失败返回 NULL,必须检查,否则 verifier 直接拒掉,连加载都过不去。 - 跨 CPU 共享计数器要用
__sync_fetch_and_add或者直接换 PERCPU map,不然数据会丢。 max_entries别设太大,受RLIMIT_MEMLOCK限制,加载会失败,错误信息是Unable to load program这种很迷惑的提示,新手会以为是程序写错了。- map 的 value 是结构体时,对齐要小心。
struct call_key8 字节没问题,要是塞个char[256]进去,verifier 可能会要求 8 字节对齐。
eBPF 网络、系统、容器观测工具
网络观测
- tcpdump / libpcap:传统抓包工具,部分已支持 eBPF。
- Katran:Facebook 开源的四层负载均衡器。
- Cilium:基于 eBPF 的 Kubernetes CNI 和网络策略引擎。
系统观测
- BCC(BPF Compiler Collection):提供高级脚本接口,适合快速开发。
- bpftrace:类似 awk 的高级追踪语言。
- bpftool:查看和管理 eBPF 程序与 Map 的利器。
容器观测
- Pixie:基于 eBPF 的 Kubernetes 可观测平台。
- Falco:基于 eBPF 的运行时安全检测。
- Inspektor Gadget:K8s 专用 eBPF 工具集。
光看名字你不知道该用哪个。我把几个主流工具横向对比一下,方便做选型决策:
| 工具 | 定位 | 学习成本 | 生产就绪 | 擅长场景 | 短板 |
|---|---|---|---|---|---|
| bcc | Python+C 框架 | 中(要懂 C) | 中等 | 快速开发复杂追踪、自定义逻辑 | 编译时依赖内核头,部署重 |
| bpftrace | 单行/脚本语言 | 低 | 中等 | 一次性排障、快速验证假设 | 复杂程序写不了,性能开销大 |
| libbpf | C 库 + CO-RE | 高 | 高 | 生产级独立程序、Agent | 开发慢,要写不少样板 |
| Cilium/Hubble | K8s CNI + 观测 | 中 | 高 | K8s 网络观测、网络策略、L7 流量 | 只管 K8s,非容器场景不适用 |
| Pixie | 完整可观测平台 | 低 | 中高 | K8s 全栈观测、Auto-instrument | 重,集成生态有限 |
| Kindling | 深度网络观测 | 中 | 中 | HTTP/MySQL/Redis 协议级延迟分析 | 社区小,文档少 |
| Falco | 安全检测 | 中 | 高 | 运行时安全、合规审计 | 不适合性能观测 |
| Inspektor Gadget | K8s gadget 集合 | 低 | 中 | K8s 一键排查(kubectl 插件形式) | 不适合长期部署 |
我个人选型思路:
- 临时排障:先
bpftrace,能单行解决就别写脚本。 - 要交付的运维工具:
libbpf + CO-RE,编译一次到处跑。 - K8s 网络观测:
Cilium + Hubble,没什么好纠结的,生态最好。 - 协议级深度观测:
Kindling或自己写 BPF 解析协议。 - 安全审计:
Falco,规则丰富,社区活跃。
选型别贪大求全。我见过有团队一上来就 Pixie 全家桶,结果发现 90% 功能用不上,Agent 开销反而成了负担。先从 bpftrace + Hubble 起步,遇到瓶颈再上更重的方案。
基于 eBPF 的低成本运维观测方案
相比传统 Agent,eBPF 观测方案具有以下优势:
- 零侵入:不需要修改应用代码或注入 Sidecar。
- 全栈覆盖:从网络包到系统调用,再到应用函数调用。
- 低开销:事件在内核态处理,只将必要数据导出到用户态。
- 细粒度:可以观测到每个进程、每个连接、每个请求的行为。
光说不练假把式,我举几个我实际碰到过的低成本观测场景:
场景一:定位"偶发性慢请求"
线上有个 Go 服务,P99 偶尔飙到 5 秒,但应用日志、Prometheus 指标都看不出问题。我们挂了个 bpftrace 脚本,统计这个服务所有 syscall 的耗时分布,跑了两小时发现是 futex 系统调用偶发 3 秒+——后来查出来是 GC 时 STW 持锁等待。这个根因用传统 APM 根本看不到,因为 APM 没有内核态视角。
场景二:找出"哪个 Pod 在打数据库"
测试环境数据库突然 QPS 暴涨,但没人承认是自己干的。用 Cilium Hubble 的 flow 日志按目标 IP 过滤,5 秒定位到某个 namespace 下一个 Pod 在疯狂 select,进去一看是开发忘了关 debug 模式的全表扫描。Hubble 这个能力是 eBPF 给的,传统抓包你得登每台机器。
场景三:检测"配置漂移导致的异常文件访问"
某次安全审计要求监控所有对 /etc/passwd、/etc/shadow、~/.ssh/ 的读操作。写了个 20 行的 bpftrace 脚本挂 tracepoint:syscalls:sys_enter_openat,过滤文件名,5 分钟搞定。要换传统方案,你得给每个应用加 audit rule,或者改镜像埋点,麻烦得多。
场景四:网络抖动根因定位
服务 A 调服务 B 偶发超时,但 ping 不丢包、tcpdump 看不到重传。挂了个 BPF 程序统计 tcp_retransmit_skb 的触发点,发现是 B 所在节点的软中断被某个 CPU 密集型 Pod 占满,导致 ACK 延迟超过 RTO,触发重传。这种问题没 eBPF 你根本无从下手。
典型低成本方案架构(我们生产上跑的版本):
[DaemonSet] Node Agent(每个节点一个 Pod)
|
|- eBPF 程序组
| |- tcp_connect 程序 -> 采集 TCP 连接建立耗时
| |- syscall tracer -> 采集慢 syscall
| |- process exec tracer -> 采集进程启动事件
| |- file access tracer -> 采集敏感文件访问
|
|- 本地聚合层(C/Rust)
| |- 按 cgroup/Pod 聚合
| |- 采样 + 过滤(高频事件按比例采样)
| |- 本地缓存 + 批量发送
|
|- 上行通道
|- OTLP gRPC -> OTel Collector
|- Prometheus exporter -> Prometheus
|- 直推 Kafka -> 下游 LLM 分析
把这套 DaemonSet 架构画成图,能看清“采集 → 聚合 → 多通道上报”三层:
flowchart TD
A[DaemonSet: Node Agent
每节点一个 Pod] --> B[eBPF 程序组
tcp_connect / syscall / exec / file]
B --> C[本地聚合层
按 Pod 聚合 + 采样 + 批量]
C --> D1[OTLP gRPC -> OTel Collector]
C --> D2[Prometheus exporter]
C --> D3[直推 Kafka -> LLM 分析]关键设计点:
- DaemonSet 部署:每个节点一个 Agent,避免单点瓶颈。
- 本地聚合:高频事件先在节点本地聚合(按 PID/cgroup 维度),再上报,减少 90% 网络流量。
- 采样策略:正常运行全采样没必要,按比例采 1/100 即可;告警期间切换到全采样。
- 多通道上报:指标走 Prometheus,事件走 OTLP,原始数据走 Kafka,按消费场景分通道。
这个方案我们对比过传统 Agent(Datadog/New Relic 那种),单节点 CPU 开销从 8% 降到 1.5%,内存从 800MB 降到 200MB,能拿到的数据维度反而更多。低成本不是吹的,是真的省。
eBPF + AIOps 异常根因定位
eBPF 可以为 AIOps 提供更丰富的底层上下文数据:
| 数据类型 | AIOps 应用 |
|---|---|
| 系统调用延迟 | 识别 IO 瓶颈、锁竞争 |
| TCP 重传/丢包 | 网络故障根因分析 |
| 进程调度延迟 | CPU 饱和或调度异常 |
| 文件访问模式 | 识别异常文件操作 |
| 容器网络流量 | 服务依赖异常检测 |
传统 AIOps 难做根因定位,本质上是因为数据不够底层。你拿 Prometheus 指标告诉 LLM “CPU 利用率 90%",LLM 顶多告诉你"可能 CPU 瓶颈”,但它不知道是哪个进程在干啥、哪个 syscall 慢、哪个文件被反复读。eBPF 把这层"内核视角"补上了。
结合 LLM,可以构建这样的根因定位流程:
- 告警触发后,eBPF Agent 自动采集相关进程/容器的系统调用、网络、调度数据。
- 将采集到的数据与指标、日志、链路信息一起输入 LLM。
- LLM 基于多源数据进行推理,给出根因假设。
- 根据根因自动执行修复或推荐修复方案。
我把它具体化一点,画个能落地的流程:
[告警触发] Prometheus: svc-A P99 latency > 2s 持续 5min
|
v
[数据采集调度器]
|- 通知 Node Agent 切换到全采样模式(针对 svc-A Pod 所在节点)
|- 采集维度:
1. TCP 连接建立耗时(按对端 IP 聚合)
2. syscall 耗时分布(按 syscall 类型 + 慢调用 top 10)
3. 进程 on-CPU/off-CPU 火焰图
4. 文件 IO 延迟(按文件路径)
5. GC 事件(如果是 Go/Java)
|
v
[数据预处理]
|- 时间窗对齐(取告警前后各 10 分钟)
|- 异常点提取(用基线 + 3σ 过滤)
|- 多源关联(trace_id 串起 eBPF 数据 + APM + 日志)
|
v
[LLM 推理]
Prompt 结构:
- 告警信息
- 关联的 eBPF 数据摘要(结构化)
- 历史相似事件 + 处置记录(RAG 召回)
- 系统拓扑(svc-A 依赖 svc-B、svc-C,下游 DB)
|
v
[根因假设输出]
"1. svc-A 在 14:23-14:28 期间 futex 平均耗时 850ms(基线 2ms)
2. 同期 svc-A Pod 所在节点 GC 频率上升 5 倍
3. 下游 DB 连接池占用 95%
推断:连接池耗尽 -> 等锁 -> futex 慢 -> 请求堆积
建议操作:扩容 svc-A 副本 + 临时调大 DB 连接池上限"
|
v
[自动/人工处置]
把上面这条落地链路画成图,关键是“先采、再预处理、最后才交给 LLM”:
flowchart TD
A[告警触发
svc-A P99 > 2s] --> B[采集调度器 切全采样]
B --> C[多维度采集
TCP / syscall / 火焰图 / IO / GC]
C --> D[数据预处理
时间窗对齐 + 异常提取 + 多源关联]
D --> E[LLM 推理
告警 + eBPF 摘要 + RAG + 拓扑]
E --> F[根因假设输出
连接池耗尽->等锁->futex 慢]
F --> G[自动 / 人工处置]这个流程我实际跑过 demo,效果比纯指标驱动好太多。关键点在于 eBPF 给的数据是带因果链的——你能从"slow request"追到"哪个 syscall 慢"追到"哪个内核路径慢"追到"哪个资源争用",而传统指标只能告诉你"慢"。
⚠️ 新手必踩的坑:别把原始 eBPF 数据直接喂 LLM。一个节点跑全量 syscall trace,每秒可能产生几万条事件,直接喂既贵又不准。一定要在 Agent 侧做重度本地聚合和异常过滤(比如延迟超基线 3σ 的 syscall),只把结构化的异常摘要送给 LLM。LLM 强在“多源关联 + 假设生成”,弱在“海量原始数据过滤”,分工要清晰。
但有个现实问题必须说:eBPF 数据量极大。一个节点跑全量 syscall trace,每秒可能产生几万条事件,直接喂给 LLM 既贵又不准。所以一定要在 eBPF Agent 侧做重度的本地聚合和异常过滤,只把"异常片段"送给 LLM。这部分是真正难的工程问题,后面实战篇我会展开讲。
还有一个反直觉的经验:别指望 LLM 直接看原始 eBPF 数据。你给它一堆 syscall trace 行,它会说一堆正确但没用的废话。正确姿势是:先用规则/统计算法把"异常片段"捞出来(比如延迟超基线 3σ 的 syscall),再把结构化的异常摘要喂给 LLM,让它做关联推理。LLM 强在"多源关联 + 假设生成",弱在"海量原始数据过滤",分工要清晰。
总结
eBPF 这玩意儿刚开始学的时候觉得门槛高(要懂内核、懂 C、懂 verifier 那些破限制),但一旦上手你会发现它是云原生时代最值得投入的技术之一。它把"内核可观测性"从黑盒变成了白盒,给 AIOps 提供了之前根本拿不到的细粒度数据。
下一篇我会动手写代码,用 bpftrace、BCC、Cilium 跑通几个真实的观测场景,把这些理论落地下来。
自测题与动手练习
自测题(合上书能答出来,才算懂):
- eBPF 凭什么“不改内核源码就能在内核跑程序”?靠的是哪三件事?
- verifier 是“运行时检查”还是“加载时静态分析”?它有几条硬限制是踩坑高发区(至少说出 3 条)?
- kprobe 和 tracepoint 在稳定性上有什么差别?生产环境为什么更推荐 tracepoint?
- BPF Map 为什么是“无状态程序”的通信中枢?HASH 和 ARRAY 各自适合什么访问模式?
- 把 eBPF 数据喂给 LLM 做根因定位时,最大的工程坑是什么?正确姿势是怎样的?
动手练习(建议真做一遍):
- 在纸上画出 eBPF 程序完整生命周期(编译 → 加载 → verifier → JIT → attach → 运行 → map 通信),标出哪一步会过 verifier、不过会怎样。
- 画一张 Node Agent 的 DaemonSet 架构图(eBPF 程序组 → 本地聚合 → 多通道上报),并标出“本地聚合减少 90% 网络流量”发生在哪一层。
- 给“偶发慢请求”场景设计一条根因定位链路:列出要采集的 3 个 eBPF 维度,并写出交给 LLM 的“结构化异常摘要”应该长什么样(而不是原始 trace 行)。
本章小结
- eBPF 是内核里的“安全沙箱”:靠钩子点、加载式字节码、verifier 沙箱三件事实现零侵入内核可编程,把可观测性从“改源码重编译”降到“热加载脚本”。
- eBPF 程序的一生是“投稿发表”:verifier 在加载时静态审查,不过直接退稿;硬限制(指令数、栈 512B、循环上界、map 边界、helper 匹配)是踩坑高发区。
- 挂载点要按需选:kprobe 是不稳定 ABI,生产优先 tracepoint / fentry;xdp 做网络、uprobe 抓用户态函数、lsm 做安全。
- Map 是无状态程序的通信中枢:按访问模式选型(HASH 通用、ARRAY O(1)、PERCPU_* 高频计数、RINGBUF 事件流),ARRAY 返回值永非空、HASH 必须 NULL 检查。
- eBPF + AIOps 补齐“内核视角”,但务必先在 Agent 侧做本地聚合与异常过滤,只把结构化异常摘要喂给 LLM,而非原始 trace。
- 下一篇我们动手写代码,用 bpftrace、BCC、Cilium 跑通几个真实观测场景,把今天的理论全部落地。