eBPF:内核变成可编程的基础设施

一、引言

Cilium 替换了 kube-proxy,让 Kubernetes 的服务路由从 O(n) 变成 O(1)。Tetragon 在内核层追踪进程行为,在违规发生的瞬间直接终止进程。Pixie 在不修改任何应用代码的情况下,给出整个集群的服务拓扑和延迟分布。

这三件事背后是同一个机制:eBPF。

它不是一个工具,是一个平台——网络、可观测性、安全三个领域都在向它收敛,原因是它提供了一个此前不存在的能力:在 Linux 内核里安全地运行自定义逻辑。

二、为什么:内核边界的困境

边界的存在与代价

Linux 内核和用户空间之间有一条边界。这条边界存在的意义是稳定性与安全性:用户程序不能直接操作硬件,不能随意访问内存,不能破坏内核状态。任何需要”靠近硬件”的操作,都必须通过系统调用穿越这条边界。(参见 06-系统调用:内核与用户空间的边界

边界保护了系统,但也带来了代价:每次穿越都有上下文切换开销;在用户空间处理数据意味着数据必须从内核复制到用户空间,再复制回去;无法在内核路径上做早期拦截——数据包必须走完整个协议栈才能到达用户空间的过滤逻辑。

内核模块的两难

在 eBPF 之前,如果需要在内核路径上执行自定义逻辑,只有两条路。

修改内核源码:周期以年计,需要深厚的内核知识,每次内核版本升级都需要重新适配。

编写内核模块(Kernel Module):可以动态加载,不需要重编内核。但内核模块和内核共享同一个地址空间,任何内存越界、空指针解引用、死锁——都会导致整个内核崩溃。内核模块是强大的,但没有任何安全边界。

结果是:大量本该在内核做的事情,被迫推到了用户空间。

被推到用户空间的代价:iptables 的故事

以 Kubernetes 的 kube-proxy 为例。它需要实现 Service 的负载均衡:把发往 ClusterIP 的流量,路由到后端某个 Pod。

传统实现用 iptables:为每个 Service 创建一组规则,数据包进来时依次遍历所有规则,找到匹配的就执行 NAT 转发。

这套机制在小规模下工作正常。但 iptables 是链表结构,查找复杂度是 O(n)——Service 数量增长到几千个时,每个数据包要遍历数万条规则,CPU 开销和延迟都显著上升。(参见 11-网络:Linux 的网络模型

这不是 iptables 的 bug,是它的设计没有为这个规模而生。真正的问题是:负载均衡逻辑在内核里执行,但没有办法用更高效的数据结构来实现它——iptables 的规则是线性的,你没有选择。

eBPF 的回答

eBPF 打开了第三条路:在内核里运行经过数学验证的沙箱程序

“数学验证”是这句话的核心。eBPF verifier 在程序加载到内核之前,对整个程序做静态分析,证明它会终止、不会越界访问内存、不会破坏内核状态。这不是运行时检查,是加载时的静态证明。一旦通过验证,这个程序在内核路径上执行,和内核原生代码没有本质区别,但安全性有了保证。

三、怎么设计:eBPF 的核心机制

沙箱虚拟机

eBPF 虚拟机是一个精简的 RISC 指令集:11 个 64 位寄存器,固定大小的栈(512 字节),有限的指令集。限制不是缺陷,是安全设计的结果——受限的指令集让 verifier 的静态分析成为可能。

与通用虚拟机(JVM、V8)不同,eBPF 不以完备性为目标。它的目标是:在内核路径上以最低开销执行一小段经过验证的逻辑。

Verifier:加载前的静态证明

Verifier 是 eBPF 安全保证的核心,它在 bpf() 系统调用加载程序时执行,做三件事:

终止性证明:eBPF 程序不允许有无限循环(早期版本),或者循环次数必须有上界可以被证明(现代版本)。Verifier 通过控制流分析确认程序一定会返回。

内存安全:程序不能访问任意内存地址,只能访问:它自己的栈、通过辅助函数(helper function)访问的 Maps、以及内核明确暴露的上下文(如数据包内容)。任何潜在的越界访问都会被拒绝。

类型安全:Verifier 追踪每个寄存器的类型状态,确保指针不会被误用(比如把整数当指针解引用)。

Verifier 的局限:对于复杂的程序,静态分析的路径数可能呈指数增长,超出 verifier 的分析上限。这意味着 eBPF 程序需要刻意编写,避免过于复杂的控制流。

Maps:内核与用户空间的双向通道

eBPF 程序运行在内核里,但它产生的数据(统计信息、追踪事件、路由表)需要被用户空间的程序读取;用户空间的控制逻辑(策略配置)也需要传入内核。Maps 是唯一合法的通道。

用户空间程序
    ↕ (bpf() 系统调用)
  BPF Maps(内核内存)
    ↕ (helper function)
  eBPF 程序(内核路径)

Maps 有多种类型,按使用场景选择:

类型特性典型用途
BPF_MAP_TYPE_HASHkey-value 哈希表,O(1) 查找Cilium 的 Service 路由表
BPF_MAP_TYPE_ARRAY整数索引数组,极低开销计数器、统计信息
BPF_MAP_TYPE_LRU_HASH有 LRU 淘汰的哈希表连接追踪(自动清理旧连接)
BPF_MAP_TYPE_RINGBUF无锁环形缓冲区高性能事件流(替代 perf buffer)
BPF_MAP_TYPE_PERF_EVENT_ARRAYperf 事件数组追踪事件上报

挂载点(Hook Points):eBPF 能插入的位置

eBPF 程序的能力边界由它挂载的位置决定。内核在关键路径上暴露了多种挂载点:

XDP(eXpress Data Path):数据包在网卡驱动层,甚至在分配 sk_buff 之前,就可以被 eBPF 程序处理。这是 Linux 网络栈上最早的介入点,绕过了整个协议栈,适合 DDoS 防护和超高性能负载均衡。

TC(Traffic Control):数据包在进入/离开网络接口时的处理点,比 XDP 晚但功能更丰富,可以访问完整的 sk_buff 结构。Cilium 的大部分数据包处理逻辑在这里。

kprobe / kretprobe:动态挂载到任意内核函数的入口或返回。kprobe:vfs_read 可以在任何进程读文件时触发 eBPF 程序。不需要修改内核代码,不需要重新编译——动态挂载,即时生效。

tracepoint:内核预设的稳定追踪点,有稳定的 ABI。相比 kprobe(绑定到函数名,内核版本间可能变化),tracepoint 更稳定,适合生产环境。(参见 13-可观测性:系统如何记录自己

uprobe:挂载到用户空间程序的函数入口。可以追踪任意语言的应用代码——Go 的 HTTP 请求处理函数、Python 的数据库查询——不需要修改应用代码,不需要重新部署。

LSM(Linux Security Module):安全策略钩子。eBPF 程序可以挂载到 LSM 钩子上,在内核层做访问控制决策——文件访问、进程创建、网络连接,在系统调用层面就被拦截,而不是事后监控。

eBPF 程序的生命周期

① 编写:用 C(受限子集)编写 eBPF 程序

② 编译:Clang + LLVM 编译为 eBPF 字节码(.o 文件)

③ 验证:调用 bpf(BPF_PROG_LOAD) 系统调用
         内核 verifier 静态分析,通过则接受,不通过则返回错误

④ JIT 编译:内核把 eBPF 字节码 JIT 编译为本机机器码
             (x86_64、ARM64……),性能接近原生代码

⑤ 挂载:把程序挂载到 hook point(XDP、kprobe、tracepoint……)

⑥ 执行:触发条件满足时,程序在内核路径上执行

⑦ 交互:通过 Maps 和用户空间程序交换数据

四、三个统一的领域

网络:从 iptables 到 eBPF Maps

iptables 的核心问题是线性规则遍历。一个有 5000 个 Service 的 Kubernetes 集群,kube-proxy 会创建数万条 iptables 规则。每个数据包都要遍历所有规则,CPU 花在规则匹配上的时间远超实际转发。

Cilium 的解法是用 eBPF Maps 替代 iptables 规则链:

iptables 查找(传统):
  数据包 → 规则 1 → 规则 2 → ... → 规则 N → 匹配 → 转发
  复杂度:O(n),n = 规则数量

Cilium BPF Map 查找:
  数据包 → 哈希(目标 IP:Port) → Map 查找 → 后端 Pod IP
  复杂度:O(1),不受 Service 数量影响

Network Policy 的实现同样受益:传统 NetworkPolicy 在用户空间计算,产生 iptables 规则,规则数量随 Pod 和策略数量增长。Cilium 把策略决策放在内核的 eBPF 程序里——不匹配策略的数据包在 TC 层直接被丢弃,不进入协议栈,不消耗后续处理资源。

XDP 把这个逻辑推向极限:在网卡驱动层、sk_buff 分配之前就做过滤。一个 XDP 程序可以在单核上以线速处理数十 Gbps 的流量,这是用户空间程序无法实现的性能边界。

可观测性:零侵入的内核级追踪

传统的应用可观测性方案都有一个前提:需要修改应用——加 SDK、加 agent、加 sidecar。这个前提意味着:未插桩的应用不可观测;插桩有运行时开销;语言不同需要不同的 SDK。

eBPF 打破了这个前提。通过 kprobe 和 uprobe,可以在不修改任何代码、不重新部署的情况下,追踪任意函数的调用。(参见 13-可观测性:系统如何记录自己

bpftrace:eBPF 的命令行追踪工具,语法类似 awk/DTrace:

# 追踪所有进程的文件读操作,打印进程名和文件名
bpftrace -e 'kprobe:vfs_read { printf("%s read\n", comm); }'
 
# 统计各进程的系统调用次数(每秒输出)
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }
             interval:s:1 { print(@); clear(@); }'
 
# 追踪 TCP 连接建立,打印源 IP、目标 IP 和端口
bpftrace -e 'kprobe:tcp_connect {
  $sk = (struct sock *)arg0;
  printf("%s -> %s:%d\n",
    ntop(AF_INET, $sk->__sk_common.skc_rcv_saddr),
    ntop(AF_INET, $sk->__sk_common.skc_daddr),
    $sk->__sk_common.skc_dport >> 8);
}'
 
# 统计 vfs_read 的延迟分布(直方图)
bpftrace -e '
kprobe:vfs_read { @start[tid] = nsecs; }
kretprobe:vfs_read /@start[tid]/  {
  @latency = hist(nsecs - @start[tid]);
  delete(@start[tid]);
}'

一行命令,零侵入,生产环境可用,即时生效。这是 eBPF 可观测性最直接的体现。

Pixie:基于 eBPF 的 Kubernetes 全栈可观测性平台。不需要修改任何应用代码,不需要 sidecar,部署 Pixie agent 之后自动追踪:HTTP/gRPC 请求(包括请求头、响应码、延迟)、数据库查询(SQL 语句和执行时间)、服务间调用拓扑。这是零侵入可观测性的生产级实现。

安全:策略下沉到内核层

传统的 Linux 安全是静态的:seccomp 在进程启动时定义允许的系统调用白名单,之后无法修改,也无法感知系统调用的上下文(谁在调用、为什么调用、携带了什么参数)。

Tetragon 代表了 eBPF 在安全领域的范式转变:

运行时行为追踪:不是”这个进程启动时被允许了什么”,而是”这个进程运行时实际做了什么”。Tetragon 在内核层追踪:

  • 哪个进程(精确到 PID、容器、Namespace、Pod)读了哪个文件
  • 哪个进程建立了哪个网络连接(目标 IP、端口)
  • 哪个进程 fork 出了子进程、执行了什么命令

Kubernetes 感知:Tetragon 把内核事件和 K8s 的 Pod / Namespace / ServiceAccount 关联,产生带有 K8s 上下文的安全事件——不只是”PID 1234 读了 /etc/passwd”,而是”Namespace production 里 Pod frontend-xxx 的进程读了 /etc/passwd”。

内核层终止:传统安全工具是”监控 + 事后报警”——违规发生了,你收到通知,然后手动处置。Tetragon 可以配置为”违规发生时,在内核层直接终止进程”——违规在发生的瞬间就被阻止,而不是被记录后等待响应。

# Tetragon TracingPolicy:禁止任何进程读取 /etc/shadow
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: protect-shadow
spec:
  kprobes:
  - call: "security_file_open"
    syscall: false
    args:
    - index: 0
      type: "file"
    selectors:
    - matchArgs:
      - index: 0
        operator: "Prefix"
        values:
        - "/etc/shadow"
      matchActions:
      - action: Sigkill    # 直接发 SIGKILL 给违规进程

五、在 Kubernetes 里的位置

eBPF 在 Kubernetes 生态里已经不是”可选的优化”,而是正在成为默认的数据平面。

Cilium 替换 kube-proxy

越来越多的 Kubernetes 发行版(GKE、EKS、AKS 的最新版本)默认使用 Cilium 作为 CNI,并用 Cilium 的 eBPF 实现替换 kube-proxy。部署时加上 --set kubeProxyReplacement=true,kube-proxy 不再运行,Service 路由完全由 eBPF 处理。

性能差异在规模上是显著的:1000 个 Service 时,iptables 模式的每包延迟约 50μs,Cilium eBPF 模式约 5μs;10000 个 Service 时,差距进一步拉大。

Hubble:Cilium 的可观测性层

Hubble 构建在 Cilium 之上,利用 eBPF 已经处理的每个数据包,产生服务间流量的实时拓扑和指标:

# 实时查看流量
hubble observe --pod myapp --follow
 
# 查看服务间调用拓扑
hubble observe --namespace production --protocol http

不需要 sidecar,不需要修改应用,因为数据包本来就在 eBPF 程序里流过。

Ambient Mesh 的数据平面

Istio Ambient Mesh(下一篇会详细展开)用 ztunnel 替代了 sidecar。ztunnel 是每节点的 L4 代理,它用 eBPF 做流量重定向——把发往 Pod 的流量透明地拦截并转发给 ztunnel,不需要修改 Pod 的网络配置。eBPF 让”零 sidecar 的服务网格”成为可能。

六、上手:用 bpftrace 感受 eBPF 的能力

环境准备

# Ubuntu / Debian
sudo apt install bpftrace
 
# 验证内核版本(eBPF 需要 4.9+,bpftrace 推荐 5.4+)
uname -r
 
# 查看已加载的 eBPF 程序
sudo bpftool prog list
 
# 查看内核暴露的 tracepoint
sudo bpftrace -l 'tracepoint:*' | head -20

基础 one-liners:感受零侵入追踪

# 追踪所有 open() 系统调用,打印进程名和文件路径
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_openat {
  printf("%s opened %s\n", comm, str(args->filename));
}'
 
# 统计哪些进程在写文件(按写入字节数排序)
sudo bpftrace -e '
kretprobe:vfs_write { @bytes[comm] = sum(retval); }
END { print(@bytes); }'
 
# 追踪新建的 TCP 连接
sudo bpftrace -e '
kprobe:tcp_connect {
  printf("connect: %s (pid %d)\n", comm, pid);
}'
 
# 统计系统调用频率(10 秒内)
sudo bpftrace -e '
tracepoint:raw_syscalls:sys_enter { @syscalls[args->id] = count(); }
interval:s:10 { print(@syscalls); exit(); }'

这些命令在生产系统上运行是安全的:eBPF 程序经过 verifier 验证,观测行为不影响被观测对象,开销极低(通常 < 1% CPU)。这是 eBPF 可观测性最核心的价值——任何系统,任何时候,任何函数,都可以安全地实时追踪

理解输出

bpftrace 的输出是内核事件的直接映射。comm 是触发事件的进程名,pid 是进程 ID,tid 是线程 ID,nsecs 是纳秒时间戳。Maps 操作(@bytes[comm] = sum(retval))把数据聚合在内核里,减少用户空间的数据传输量。

进阶:用 BCC 写延迟统计

BCC(BPF Compiler Collection)允许用 Python 控制逻辑 + C 内核逻辑来写更复杂的 eBPF 工具:

# 统计 do_sys_open 的延迟分布
from bcc import BPF
 
program = """
#include <uapi/linux/ptrace.h>
 
BPF_HASH(start, u32);
BPF_HISTOGRAM(dist);
 
int kprobe__do_sys_open(struct pt_regs *ctx) {
    u32 pid = bpf_get_current_pid_tgid();
    u64 ts = bpf_ktime_get_ns();
    start.update(&pid, &ts);
    return 0;
}
 
int kretprobe__do_sys_open(struct pt_regs *ctx) {
    u32 pid = bpf_get_current_pid_tgid();
    u64 *tsp = start.lookup(&pid);
    if (tsp) {
        u64 latency = bpf_ktime_get_ns() - *tsp;
        dist.increment(bpf_log2l(latency));
        start.delete(&pid);
    }
    return 0;
}
"""
 
b = BPF(text=program)
print("Tracing open() latency... Ctrl+C to stop.")
try:
    sleep(10)
except KeyboardInterrupt:
    pass
b["dist"].print_log2_hist("nsecs")

七、故障剧场:亲手把它弄坏

实验一:关掉 Cilium 的 kube-proxy replacement,看 iptables 规则膨胀

# 先创建大量 Service(用脚本批量创建 100 个)
for i in $(seq 1 100); do
  kubectl create service clusterip svc-$i --tcp=80:80
done
 
# 在 Cilium 模式下,查看 iptables 规则数量
sudo iptables -L -n | wc -l
# 输出:约 10 条(Cilium 接管了路由,iptables 几乎为空)
 
# 禁用 Cilium 的 kube-proxy replacement,切换为传统 kube-proxy
# (在测试集群里操作,不要在生产执行)
# 重新查看 iptables 规则数量
sudo iptables -L -n | wc -l
# 输出:数百到数千条(每个 Service 产生多条规则)
 
# 用 iptables-save 看规则的实际内容
sudo iptables-save | grep "KUBE-SVC" | wc -l

规则数量的差异直接可见。在 10000 个 Service 的集群里,这个差异意味着每个数据包的处理路径长度相差数百倍。

实验二:加载一个不通过 verifier 的程序

// bad_prog.c:一个有无限循环的 eBPF 程序
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
 
SEC("tracepoint/syscalls/sys_enter_openat")
int bad_program(void *ctx) {
    int i = 0;
    while (1) {    // verifier 会拒绝这个
        i++;
    }
    return 0;
}
# 编译并尝试加载
clang -O2 -target bpf -c bad_prog.c -o bad_prog.o
sudo bpftool prog load bad_prog.o /sys/fs/bpf/bad_prog
 
# Verifier 拒绝,输出详细的错误报告:
# libbpf: prog 'bad_program': BPF program load failed: Permission denied
# libbpf: prog 'bad_program': -- BEGIN PROG LOAD LOG --
# 0: R1=ctx(off=0,imm=0) R10=fp0
# ; int i = 0;
# ...
# infinite loop detected at insn X
# -- END PROG LOAD LOG --

verifier 的错误报告非常详细,精确到出问题的指令偏移。这是 eBPF 安全保证的直接体现:危险的程序在进入内核之前就被拒绝,不会有任何执行机会。

实验三:用 bpftrace 追踪一个”慢”的操作

# 场景:某个应用响应慢,不知道是哪里慢
# 用 bpftrace 追踪它的 read() 系统调用延迟
 
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_read /comm == "python3"/ {
  @start[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_read /comm == "python3" && @start[tid]/ {
  $latency = nsecs - @start[tid];
  if ($latency > 1000000) {  // 只记录超过 1ms 的
    printf("slow read: %d us\n", $latency / 1000);
  }
  delete(@start[tid]);
}'

不修改应用代码,不重新部署,实时看到哪些 read() 调用超过了 1ms 阈值。

八、日常排障:eBPF 工具链坏了怎么查

Cilium agent 不健康

# 查看 Cilium 整体状态
cilium status
# 输出:Cilium: OK / KVStore: OK / Kubernetes: OK / ...
# 任何 Not OK 的组件都会显示具体错误
 
# 详细连通性测试(会在集群里创建测试 pod)
cilium connectivity test
 
# 查看 Cilium agent 日志
kubectl logs -n kube-system -l k8s-app=cilium --tail=100
 
# 查看特定 Pod 的网络策略是否生效
cilium endpoint list
cilium endpoint get <endpoint-id>

eBPF 程序加载失败:读 verifier 错误

verifier 的错误报告是最重要的调试信息:

# 用 bpftool 查看加载失败的详细原因
sudo bpftool prog load my_prog.o /sys/fs/bpf/my_prog 2>&1
 
# 错误报告的关键字段:
# "invalid mem access 'scalar'"  → 空指针或越界访问
# "R0 !read_ok"                   → 返回值未初始化
# "back-edge from insn X to Y"    → 检测到可能的无限循环
# "jump out of range"             → 跳转目标超出程序边界

Maps 数据异常:bpftool 查看内核 Map

# 列出所有 eBPF Maps
sudo bpftool map list
 
# 查看某个 Map 的内容(以 Cilium 的路由 Map 为例)
sudo bpftool map dump id <map-id>
 
# 查看 Map 的统计信息
sudo bpftool map show id <map-id>

内核版本兼容性

不同 eBPF 特性需要不同的最低内核版本:

特性最低内核版本
eBPF 基础3.18
Maps3.19
kprobe4.1
XDP4.8
cgroup eBPF4.10
BTF(CO-RE)5.2
LSM BPF5.7
Ring Buffer5.8
# 查看当前内核版本
uname -r
 
# 查看内核支持的 eBPF 程序类型
sudo bpftool feature probe

九、往哪走

eBPF 的定局

2026 年,几件事已经尘埃落定:Cilium 是事实标准 CNI,主要云厂商的托管 K8s 都把它作为默认或推荐选项;Istio Ambient Mesh 已经 GA,eBPF 数据平面替代 sidecar 不再是实验特性;Tetragon 进入生产级安全工具的主流选型。线索四”可编程内核”不再是趋势判断,是既成事实。

BTF 与 CO-RE:一次编译,跨内核版本运行

eBPF 程序的历史痛点是内核版本兼容性:不同内核版本的内部数据结构(如 task_struct)字段偏移不同,程序需要针对每个内核版本重新编译。

BTF(BPF Type Format)把内核的类型信息嵌入到内核镜像里,CO-RE(Compile Once, Run Everywhere)利用 BTF 在加载时动态重定位字段偏移。结果是:一个编译好的 eBPF 程序,可以在任何支持 BTF 的内核上运行(Linux 5.2+),不需要在每台机器上重新编译。这是 eBPF 走向大规模生产部署的关键基础设施改进。

eBPF for Windows

Microsoft 正在推进 eBPF for Windows 项目——在 Windows 内核上实现 eBPF 兼容层。可编程内核的思想不再是 Linux 的专属,而是正在成为跨平台的基础设施范式。

这条线的终点:eBPF 是一个平台,不是一个工具。网络、可观测性、安全三个领域向它收敛,意味着未来的云原生基础设施将越来越多地把决策逻辑下沉到内核层——更高性能、更强安全、更低侵入。理解 eBPF,就理解了云原生下一个五年的底层趋势。

关联