编程 eBPF 深度实战:把 Linux 内核变成可编程数据平面——从 bpf() 系统调用、Verifier 到 Cilium / Tetragon 生产落地

2026-07-11 01:43:47 +0800 CST views 26

eBPF 深度实战:把 Linux 内核变成可编程数据平面——从 bpf() 系统调用、Verifier 到 Cilium / Tetragon 生产落地

TL;DR:如果 2026 年你只能深挖一项「底层但全员受益」的技术,我会押 eBPF。它不性感,没有「AI 重构 96 万行代码」那种热搜标题,但它正在悄悄接管你集群里的网络、安全与可观测性:Cilium 用 eBPF 替换了 kube-proxy,Facebook 的 Katran、Cloudflare 的防御层、Google 的部分负载均衡都在它之上。本文从 bpf() 系统调用的生命周期讲起,把 Verifier、Maps、CO-RE 这些硬核概念拆开,再落到 Cilium / Tetragon 的生产架构,最后用 bpftrace、BCC、Go 三套工具链给你能直接跑的代码。读完你应该能回答一个问题:为什么「写个程序塞进内核」这种听起来很危险的事,今天反而成了云原生最稳的做法之一。


一、背景:当「改内核」不再需要改内核

先把时间拨回去。很长一段时间里,如果你想让 Linux 内核「多做点事」,只有两条路:

  1. 改内核源码、重新编译内核。代价是你要和整个内核社区打交道,升级一个补丁可能要等一个发行版大版本,而且一旦写错,内核 panic 直接带走整台机器。
  2. 写内核模块(LKM)。比重新编译内核轻一点,但风险没降多少——模块运行在 ring 0,一个空指针就能把系统干崩,且每个内核小版本 ABI 都可能变,模块要跟着重新编译。

这两条路的共性问题是:内核是「死的」,你只能在它允许的地方、用它能接受的方式碰它

而云原生把这个问题放大了。我们来看几个 2026 年依然普遍存在的痛点:

痛点 1:iptables 的线性匹配

Kubernetes 的 Service、NetworkPolicy 早期全靠 iptables 实现。iptables 的规则是线性匹配的——每个数据包进来都要从第一条规则逐条比对到命中。当你有几千个 Service、几万条规则时,复杂度是 O(n) 甚至更糟。规则越多,每个包的处理越慢,而且 iptables-restore 全量刷新时还会瞬时抖动。这不是理论问题,是大规模集群里真实存在的延迟毛刺来源。

痛点 2:可观测性的「侵入式埋点」成本

传统的 APM 要在代码里插桩(instrumentation),要么改业务代码,要么挂一个重 Agent。线上跑着几百个微服务,你想加一个「追踪所有进程 exec 行为」的监控点?推动每个团队升级 SDK 基本不可能。我们一直想要一种对应用零侵入、又能看清内核在发生什么的能力。

痛点 3:Sidecar 模式的开销

服务网格(Service Mesh)早期靠每个 Pod 里塞一个 sidecar 代理(Envoy 之类)来干活。看似优雅,但代价实打实:每个 Pod 多占几十 MB 内存、多一跳网络延迟、启动变慢、排障时多一个需要理解的组件。当集群有上万个 Pod,sidecar 的总开销相当可观。

eBPF 的回答:把内核变成「可编程平台」

eBPF(extended Berkeley Packet Filter)的核心思想一句话就能说清:在不修改内核源码、不加载内核模块的前提下,往内核的预定义钩子(hook)上安全地挂载一段你自己写的字节码,让它替你干活。

这段字节码跑在内核内置的一个沙箱虚拟机里,加载前会被一个叫 Verifier 的静态检查器严格审一遍——你不能越界访问内存、不能死循环、不能搞崩内核。过了审,字节码会被 JIT 编译成原生机器码,性能接近手写内核代码。

于是事情就变了味:内核不再是「死的、要重新编译才能改」的东西,而是一个你可以动态编程的运行环境。网络包要不要丢、系统调用要不要拦、某段代码跑了多久,你都能在内核态直接决定,而且应用完全无感。

它一路是怎么长出来的

时间里程碑意义
1992经典 BPF(cBPF)随 tcpdump 论文诞生最初只是用来在内核里高效过滤网络包
2014eBPF redesigned,并入 Linux 3.18,bpf() 系统调用出现从「包过滤器」升级为通用执行引擎
2016XDP(eXpress Data Path)进入内核 4.8能在网卡驱动层最早处理包,DDoS 防御的杀手锏
2019BTF(BPF Type Format)进入 5.1让 eBPF 程序「一次编译,到处运行」(CO-RE)成为可能
2020eBPF-LSM 进入 5.7eBPF 可以挂到 Linux 安全模块钩子上做运行时防护
2020ringbuf(BPF_MAP_TYPE_RINGBUF)进入 5.8解决了 perf buffer 的事件丢失与拷贝开销

到 2026 年,这个生态已经相当成熟:Cilium 从 CNCF 毕业,成为事实上的云原生网络/安全/可观测标准;「sidecarless 服务网格」成了主流叙事;从网络、安全到性能分析,eBPF 几乎渗透了基础设施的每一层。下面我们就把它拆开看。


二、核心概念:eBPF 到底是怎么跑起来的

2.1 生命周期:从 C 代码到内核里的程序

一个 eBPF 程序从写出来到在内核里运行,典型路径是:

你写的 C(或 Go 生成的 ELF)
   │  clang/LLVM 编译
   ▼
BPF 字节码(ELF 文件,含 .text / .maps / .license 等 section)
   │  bpf(BPF_PROG_LOAD) 系统调用
   ▼
内核 Verifier 静态校验  ──不通过──▶ 拒绝加载,返回详细错误
   │ 通过
   ▼
JIT 编译为原生机器码
   │  bpf(BPF_LINK_CREATE / BPF_PROG_ATTACH)
   ▼
挂载到具体 hook(kprobe / XDP / TC / LSM ...)
   │
   ▼
事件触发时执行,通过 Map 与用户态交换数据

注意 bpf()唯一的系统调用入口,所有加载、校验、挂载、Map 操作都通过它不同的 subcommand 完成。你平时用的 bpftoollibbpfcilium/ebpf 本质上都是在帮你调 bpf()

2.2 程序类型:能挂在哪里

eBPF 不是「到处都能挂」,它只能挂在内核预先定义好的钩子类型上。不同类型决定了你的程序能访问什么上下文、能做什么。最常用的几类:

类型挂载点典型用途
kprobe / kretprobe内核函数入口/返回(动态)追踪任意内核函数调用
uprobe / uretprobe用户态程序函数(动态)追踪 Nginx/MySQL 等二进制内部
tracepoint内核静态追踪点(稳定 ABI)稳定的内核事件追踪
perf_event性能计数器/采样CPU 火焰图、PMC 采样
xdp网卡驱动层(最早)DDoS 防护、负载均衡、早丢包
tc (ingress/egress)内核协议栈 TC 层容器网络、流量统计、策略
cgroupcgroup 级别按容器/进程组做网络限制
lsmLinux 安全模块钩子运行时安全、提权拦截
fentry / fexit内核函数入口/退出(稳定,5.5+)比 kprobe 更稳更快的追踪
socket filter套接字包过滤(经典 BPF 的祖宗)

选型时有个经验法则:能用 tracepoint / fentry 就别用 kprobe,因为 kprobe 挂在内核函数名上,函数一旦改名或内联就失效;而 tracepoint 是内核维护者承诺的稳定 ABI。

2.3 Maps:内核态与用户态的「共享内存」

eBPF 程序运行在内核态,你的监控/控制逻辑跑在用户态。它们之间怎么通信?靠 Map——一种内核与用户态共享的键值存储。

Map 是 eBPF 里最关键的抽象,没有之一。你的程序把数据写进 Map,用户态程序来读;反过来用户态也能把配置写进 Map,程序运行时去查。程序本身是无状态的,状态都在 Map 里。 这也是为什么 eBPF 程序可以安全卸载、重载而不丢状态。

常用 Map 类型:

Map 类型用途
BPF_MAP_TYPE_HASH通用哈希表,任意 key/value
BPF_MAP_TYPE_ARRAY数组,key 是索引,O(1) 快
BPF_MAP_TYPE_PERCPU_HASH / PERCPU_ARRAY每 CPU 一份副本,避免多核并发锁竞争,做计数器首选
BPF_MAP_TYPE_LRU_HASH有容量上限的 LRU 哈希,自动淘汰
BPF_MAP_TYPE_RINGBUF环形缓冲(5.8+),零拷贝事件流,替代 perf buffer
BPF_MAP_TYPE_STACK_TRACE存栈回溯
BPF_MAP_TYPE_DEVMAP / CPUMAPXDP 重定向目标(设备/CPU)
BPF_MAP_TYPE_SOCK / SOCKMAPsocket 引用,做 socket 层重定向

一个最小的定义(libbpf 风格,C 里用 SEC 宏标注 section):

// 定义一个每 CPU 的计数器数组,key 是 u32 索引,value 是 u64 计数
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 256);
    __type(key, u32);
    __type(value, u64);
} pkt_count SEC("maps");

2.4 Verifier:为什么 eBPF 不会搞崩内核

这是 eBPF 安全性的根基,也是新手最容易被劝退的地方。内核在加载前会用一个静态分析器(Verifier)把你的程序逐指令走一遍,任何不安全的迹象都会直接拒绝加载。它主要检查这些:

  1. 控制流必须是 DAG(无环图):早期内核禁止一切循环;5.3+ 起允许有显式上界的循环(编译器要把 for 展开成有界形式)。死循环?直接拒。
  2. 内存访问必须越界安全:你只能访问 Verifier 能证明「一定在合法范围内」的指针。读 skb->data 越界?拒。访问未初始化的栈变量?拒。
  3. 指针不能泄漏:你不能把内核指针通过 Map 或事件传给用户态(那是信息泄露),也不能拿用户态传来的指针直接解引用。
  4. 栈大小受限:单个程序栈上限 512 字节(MAX_BPF_STACK)。想在函数里开一个大数组?Verifier 会教你做人,正确做法是放进 Map。
  5. 指令数受限:单程序指令数有上限(配合有界循环与 bpf2bpf 尾调用可扩展,但 Verifier 仍会整体可达性分析)。
  6. 只能调用白名单里的辅助函数(helper)bpf_map_lookup_elembpf_ktime_get_nsbpf_perf_event_output 这些,不能调任意内核函数。

来看一个「会被拒」vs「能通过」的对比。下面这段无法通过 Verifier,因为访问了可能越界的指针且没检查:

// ❌ 危险写法:未校验就解引用,Verifier 直接拒绝
SEC("kprobe/do_sys_open")
int bad_prog(struct pt_regs *ctx) {
    char *name = (char *)PT_REGS_PARM2(ctx); // 用户态传来的指针
    char buf[64];
    bpf_probe_read(buf, sizeof(buf), name);   // 没判断 name 是否合法
    return 0;
}

正确写法要借助 helper 把数据安全地「拷贝」进来,并且对长度做约束:

// ✅ 安全写法:用 bpf_probe_read_kernel 受控拷贝,长度固定且有限
SEC("kprobe/do_sys_open")
int good_prog(struct pt_regs *ctx) {
    char buf[64];
    // 第三个参数是用户态指针,helper 会在拷贝前做安全校验
    long err = bpf_probe_read_user(buf, sizeof(buf), (void *)PT_REGS_PARM2(ctx));
    if (err == 0) {
        bpf_trace_printk("open path: %s\\n", buf); // 仅调试用
    }
    return 0;
}

调试 Verifier 拒绝时,用 bpftool prog load xxx.o /sys/fs/bpf/xxx 2>&1 或给程序加上 bpf_printk 看日志,Verifier 会告诉你第几条指令、为什么不合法。这是排错的最快路径。

2.5 BTF 与 CO-RE:一次编译,到处运行

早年的 eBPF 有个致命痛点:内核结构体(比如 struct task_struct)每个版本字段布局都不一样。你写死 task->pid 的偏移量,换个内核版本就全错。解决方案是 BTF(BPF Type Format) + CO-RE(Compile Once - Run Everywhere)

  • 编译时,clang -target bpf -g 生成带 BTF 调试信息的 ELF,记录「我需要访问 task_struct.pid 这个字段」。
  • 加载时,libbpf 读取当前运行内核的 BTF(/sys/kernel/btf/vmlinux),自动把字段偏移「重定位」到正确位置。

于是你编译一次,拿到一个 ELF,就能在任意支持 BTF 的内核上跑,不再需要目标机器装 clang/LLVM 现场编译。这也是 Cilium 能把一个 eBPF 二进制随镜像分发、在用户集群里直接加载的底气。

2.6 JIT:为什么它快

通过 Verifier 的字节码,会被 JIT(Just-In-Time)编译器翻译成宿主 CPU 的原生指令(x86_64 / arm64 / riscv 等都有实现)。所以运行时它不是在解释执行,而是一段地道的机器码,性能接近原生内核代码,开销通常在纳秒级。可以用 bpftool prog show 看到每个程序是否 jit 已生效。


三、架构分析:从一行脚本到生产数据平面

3.1 整体架构

把前两节拼起来,一个典型的 eBPF 系统长这样:

┌───────────────────────── 用户态 ─────────────────────────┐
│  你的控制/采集程序 (Go / Python / C)                       │
│   - 加载 ELF、挂载 hook、轮询 Map、下发配置               │
│   - 暴露 metrics 给 Prometheus / 写日志 / 告警            │
└───────────────▲──────────────────────────┬───────────────┘
                │  bpf() syscall             │  读/写 Map
                │                            │
┌───────────────┴──────────────────────────▼───────────────┐
│  内核态                                                     │
│  ┌────────────┐  事件触发   ┌─────────────────────────┐  │
│  │  Hook 点   │ ─────────▶ │  eBPF 程序 (JIT 机器码) │  │
│  │ XDP/TC/    │            │  读/写 ──▶ Maps ◀──┐     │  │
│  │ kprobe/    │            │  输出事件 ─▶ ringbuf├─┐  │  │
│  │ LSM/...    │            └─────────────────────────┘ │  │
│  └────────────┘                                         │  │
└────────────────────────────────────────────────────────────┘

关键结论:事件在内核态就被处理、聚合、过滤,只有「有价值的结论」才通过 Map / ringbuf 上送用户态。这跟「把所有原始数据丢给用户态再处理」的Agent/sidecar 模式是相反的思路,也是它性能好、开销低的根本原因。

3.2 数据流:以「统计每核包数」为例

  1. 包到达网卡 → 命中 TC/XDP hook → eBPF 程序执行。
  2. 程序用 bpf_map_lookup_elem 找到 pkt_count 里本 CPU 对应的计数器,原子加一。
  3. 用户态的采集程序周期性地 bpf_map_lookup_and_delete 把各 CPU 计数收上来汇总,推给 Prometheus。
  4. 全程包不需要进用户态、不需要 copy 到应用进程,内核里就数完了。

3.3 Cilium 架构:eBPF 如何替换 kube-proxy

Cilium 是 eBPF 在生产环境最成功的「集大成者」,它由 Isovalent 公司(联合创始人、CTO Thomas Graf)创建,已在 2023 年从 CNCF 毕业。它把前面那些零散能力组织成了一个完整的网络/安全/可观测平台。

核心颠覆:从「基于 IP」到「基于身份」。 传统网络策略的颗粒度是 IP/端口,但 Pod 的 IP 是动态分配的,扩缩容时 IP 一变策略就要重写。Cilium 给每个 Endpoint(Pod)分配一个稳定的身份标识(Identity),网络策略写成「frontend 这个身份可以访问 backend 这个身份的 8080 端口」。IP 怎么变都不影响策略语义——这才是真正云原生友好的模型。

eBPF 数据平面做的事:

  • 替换 kube-proxy:把 Service 的负载均衡逻辑用 eBPF 实现,数据包在内核态就完成 DNAT 和转发,绕开了 iptables 的线性匹配和 conntrack 开销,规模越大优势越明显。
  • L3–L7 网络策略:不仅能按 IP/端口(L3/L4)放行,还能理解 HTTP/gRPC/Kafka 等协议(L7),比如「只允许 GET /api/*,拒绝 DELETE」。
  • Hubble(可观测层):基于 eBPF 采集每个网络流的源/目的身份、延迟、HTTP 方法、 verdict(放行/拒绝),画出服务依赖图(service map),出 UI。
  • Tetragon(运行时安全):挂到 eBPF-LSM 等钩子上,不仅能观测提权、Capabilities 变更、命名空间逃逸,还能实时拦截——一旦发现进程试图越权就直接 kill,且不需要知道具体漏洞是什么(基于行为而非特征)。

和传统方案对比:

维度iptables + kube-proxysidecar 服务网格eBPF (Cilium)
包处理位置内核 netfilter(线性匹配)用户态代理(每 Pod 一跳)内核 eBPF(哈希/O(1))
延迟开销规则多时上升每跳 +1 RTT、内存占用高近零,无额外进程
策略颗粒度L3/L4L4/L7L3–L7
可观测性弱,需额外埋点强但靠代理内核级透明采集
安全无运行时防护依赖代理配置LSM 实时拦截(Tetragon)
升级耦合跟内核/iptables 版本走跟 sidecar 版本走随镜像分发,CO-RE 适配内核

一句话总结差异:sidecar 是「在每个应用旁边站一个翻译官」,eBPF 是「把翻译能力直接焊进内核」。


四、代码实战:三套工具链,都能跑

4.1 环境准备

# 内核版本要求(不同能力对应不同最低内核;CO-RE 需 5.1+,ringbuf 需 5.8+)
uname -r

# 工具链
sudo apt install -y clang llvm libbpf-dev linux-headers-$(uname -r) bpftool
# bpftrace:一行式追踪神器
sudo apt install -y bpftrace
# BCC:Python 前端
sudo apt install -y bpfcc-tools python3-bcc

# 验证
bpftool version
bpftrace --version
sudo bpftool btf dump file /sys/kernel/btf/vmlinux format raw | head   # 有输出说明支持 CO-RE

4.2 bpftrace:一行上手,零编译

想「看看系统里谁在疯狂 open 文件」?一条命令:

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

comm 是进程名,args->filename 由 BTF 自动解析。bpftrace 背后就是帮你生成并加载一段 eBPF 程序。排查「为什么磁盘 IO 高」「哪个进程在偷偷读配置」时,它比 strace 轻得多(strace 会严重拖慢目标进程,bpftrace 在内核态聚合,几乎无感)。

4.3 BCC(Python):实时 exec 行为监控

BCC 让你用 Python 写控制逻辑、用内联 C 写 eBPF 程序,适合快速验证想法。下面这段监控所有进程的 execve 调用:

#!/usr/bin/env python3
# exec_monitor.py —— 实时打印系统里发生的每一次进程执行
from bcc import BPF

prog = r"""
#include <uapi/linux/ptrace.h>

struct data_t {
    u32 pid;
    u64 ts;
    char comm[16];
};

BPF_PERF_OUTPUT(events);

int on_exec(struct pt_regs *ctx) {
    struct data_t data = {};
    data.pid = bpf_get_current_pid_tgid() >> 32;
    data.ts  = bpf_ktime_get_ns();
    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    events.perf_submit(ctx, &data, sizeof(data));
    return 0;
}
"""

b = BPF(text=prog)
# 挂到 execve 系统调用对应的内核函数上
b.attach_kprobe(event=b.get_syscall_fnname("execve"), fn_name="on_exec")

def print_event(cpu, data, size):
    event = b["events"].event(data)
    print(f"{event.ts:>18}  pid={event.pid:<7} comm={event.comm.decode()}")

b["events"].open_perf_buffer(print_event)
print("Tracing execve ... Ctrl-C to stop")
while True:
    b.perf_buffer_poll()

运行 sudo python3 exec_monitor.py,你会看到类似 catbashpython3 的每一次执行。这套机制正是 Tetragon 运行时安全的雏形——只不过它把「观察到的 exec」换成「检测到提权就拦截」。

4.4 Go + cilium/ebpf:生产级 TC 流量计数

BCC 要在目标机装 LLVM,不适合生产分发。cilium/ebpf 是纯 Go 实现的库(无 CGO),你用 clang 把 eBPF 编成 ELF,Go 程序直接加载,非常适合写长期运行、随镜像分发的守护进程。

先写 eBPF 侧(C),统计每个 CPU 收到的包数:

// tc_count.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>

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

struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 1);
    __type(key, u32);
    __type(value, u64);
} pkt_count SEC("maps");

SEC("tc")
int tc_count(struct __sk_buff *skb) {
    u32 key = 0;
    u64 *count = bpf_map_lookup_elem(&pkt_count, &key);
    if (count)
        __sync_fetch_and_add(count, 1);   // 每 CPU 原子加,零锁竞争
    return TC_ACT_OK;                       // 放行,原样继续
}

bpf2go 把 C 编进 Go 代码,再写加载器:

// main.go(示意,使用 github.com/cilium/ebpf 与 bpf2go codegen)
package main

import (
    "log"
    "net"
    "github.com/cilium/ebpf"
    "github.com/cilium/ebpf/link"
    "github.com/cilium/ebpf/rlimit"
)

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go@latest -cc clang tc_count tc_count.bpf.c

func main() {
    // 解除 eBPF 对锁内存(memlock)的限制
    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal(err)
    }

    var objs tcCountObjects
    if err := loadTcCountObjects(&objs, nil); err != nil {
        log.Fatal(err)
    }
    defer objs.Close()

    iface, err := net.InterfaceByName("eth0")
    if err != nil {
        log.Fatal(err)
    }

    // 挂到 eth0 的 TC ingress 钩子(内核 6.6+ 用 TCX,旧版用 tc.Hook)
    l, err := link.AttachTCX(link.TCXConfig{
        Interface: iface.Name,
        Program:   objs.TcCount,
        Attach:    link.TCXIngress,
    })
    if err != nil {
        log.Fatal(err)
    }
    defer l.Close()

    // 周期读取各 CPU 计数并汇总
    key := uint32(0)
    var perCPU []uint64
    for {
        if err := objs.PktCount.Lookup(&key, &perCPU); err != nil {
            log.Fatal(err)
        }
        var total uint64
        for _, v := range perCPU {
            total += v
        }
        log.Printf("total packets seen: %d", total)
        time.Sleep(time.Second)
    }
}

这就是 Cilium 做网络可观测、做 DDoS 计数时的同一个原语:在内核里把数数完,用户态只拿结论

4.5 C 原生 libbpf skeleton:更可控的生产写法

追求极致可控和最小依赖时,用 libbpf 的 skeleton 模式(编译期生成 .skel.h,运行时零反射):

// exec_skel.bpf.c —— 用 fentry 挂到 do_execve,比 kprobe 更稳更快
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

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

struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(u32));
    __uint(value_size, sizeof(u32));
} events SEC("maps");

SEC("fentry/do_execve")
int BPF_PROG(handle_exec, struct linux_binprm *bprm)
{
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u64 ts  = bpf_ktime_get_ns();
    char comm[16];
    bpf_get_current_comm(&comm, sizeof(comm));
    // 把事件送到 perf buffer,用户态消费
    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &ts, sizeof(ts));
    return 0;
}

关键是 vmlinux.h——由 bpftool btf dump file /sys/kernel/btf/vmlinux format c 生成,配合 CO-RE 自动重定位,所以这份 C 在不同内核上一编就过,不用改。用户态用 bpf_object__open_skeleton / bpf_map__attach_struct_ops 等 API 加载,比手写 bpf() 省心得多。

4.6 Cilium 生产落地:策略 + 可观测 + 安全

L3/L4 + L7 网络策略(允许 frontend 访问 backend 的 8080,且只放行 GET /api/*):

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: allow-frontend-api
spec:
  endpointSelector:
    matchLabels:
      app: backend
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: frontend
    toPorts:
    - ports:
      - port: "8080"
        protocol: TCP
      rules:
        http:
        - method: "GET"
          path: "/api/.*"

Hubble 观测(实时看流量、定位被 DROP 的请求):

# 观察所有被拒绝的 HTTP 请求,排 NetworkPolicy 神器
cilium hubble observe --verdict DROPPED --protocol http -f

# 看 pod-a 到 pod-b 的完整调用流
cilium hubble observe --from pod-a --to pod-b

Tetragon 运行时安全(拦截任何「非 0 用户提权到 root」的行为):

apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: "detect-setuid-to-root"
spec:
  kprobes:
  - call: "sys_setuid"
    syscall: true
    args:
    - index: 0
      type: "int"
    selectors:
    - matchArgs:
      - index: 0
        operator: "Equal"
        values:
        - "0"
      # 命中即触发告警/执行动作,无需知道具体提权漏洞

这三段合起来,就是一套「网络可控 + 流量可见 + 运行时防提权」的基础设施,而且全部对业务进程零侵入


五、性能优化:eBPF 写得好是神器,写不好是瓶颈

5.1 计数器首选 per-CPU Map

多核并发下,如果用普通 HASH/ARRAY 做计数,每次 bpf_map_update_elem 都要抢同一把锁,核心越多竞争越激烈。改成 PERCPU_* 类型,每个 CPU 一份副本,更新是无锁的,用户态汇总时再把各 CPU 加起来。这是 eBPF 性能优化的第一性原则。

5.2 用 ringbuf 替代 perf buffer

perf buffer 是每个 CPU 一个独立 ring,且按「每事件一次内存拷贝 + 一次系统调用通知」工作,高吞吐时容易丢事件、有拷贝开销。5.8 引入的 ringbufBPF_MAP_TYPE_RINGBUF)是「所有 CPU 共享一个 ring + 保留内存顺序 + 批量消费」,吞吐更高、不丢事件、开销更低。新项目直接用 ringbuf。

5.3 尾调用(tail call)扩展逻辑、绕开指令上限

单程序指令数有上限。把逻辑拆成多个小程序,用 bpf_tail_call 把执行「跳」到下一个程序,既突破长度限制,又能按协议类型做分派(类似 switch)。Cilium 的协议解析就是这样一级级 tail call 串起来的。

5.4 批处理 Map 操作

bpf_map_lookup_and_delete_batch 等批量 helper 能一次操作一批 key,用户态一次系统调用拿回一堆数据,大幅减少 bpf() 调用次数。

5.5 生产环境别用 bpf_trace_printk

bpf_trace_printk 会把内容写进 trace_pipe有全局锁、串行化、极慢,且所有程序共享一个通道。它只适合本地调试。生产请用 perf buffer / ringbuf 把结构化事件上送。

5.6 Map Pinning 持久化

bpf_obj_pin 把 Map 钉到 bpf 文件系统(/sys/fs/bpf/)。这样即使你的加载程序重启,Map 里的状态(计数、配置)也不丢,新程序加载后能直接复用。

5.7 CO-RE 减少重编译

别在每台机器上现编。用 clang + BTF 编一个 ELF,靠 CO-RE 适配不同内核,随镜像分发。既省资源,又避免「目标机没装 LLVM」导致的加载失败。

5.8 XDP 早丢包,把 DDoS 挡在网卡口

DDoS、非法流量如果走完整协议栈再到应用层丢弃,CPU 照样被耗死。XDP 在网卡驱动层最早处理,能直接 XDP_DROP 把恶意包丢掉,Facebook 的 Katran、Cloudflare 的防御都是这个思路,单核就能扛百万 PPS。

5.9 调优与排错工具箱

工具用途
bpftool prog list看已加载程序、是否 JIT、attach 在哪
bpftool map dump直接读 Map 内容,验证数据
bpftool perf看 perf/ring buffer 事件来源
bpftool prog profile对 eBPF 程序做 CPU 采样
bpftool btf查看/校验 BTF,CO-RE 排错
sudo cat /sys/kernel/debug/tracing/trace_pipebpf_trace_printk 输出
veristat对比不同内核下 Verifier 复杂度

六、总结与展望:内核即平台,已成定局

eBPF 带来的是一次范式转移

过去我们默认「基础设施能力是内核写死的,应用只能适应它」。eBPF 把这个假设反过来:内核成了一个可被安全编程的数据平面,网络、安全、可观测性这些横切关注点,不再需要侵入应用、不再需要每 Pod 一个代理、不再需要改内核重编译,而是以「内核态小程序」的形式动态注入。

这套思路已经在生产里被反复验证:Cilium 接管了海量 Kubernetes 集群的网络,Tetragon 在做运行时安全,各大厂用 XDP 扛流量,可观测性领域 eBPF 几乎成了「无侵入追踪」的代名词。

2026 年的生态走向

  • eBPF Foundation 把厂商(Isovalent/Cisco、Meta、Google、Netronome 等)拉到一起,推动标准与工具链统一。
  • sidecarless 成为主流叙事:服务网格不再靠 sidecar,而是把能力下沉到 eBPF 数据平面。
  • 语言绑定成熟:Rust 的 aya、Go 的 cilium/ebpf 让「不用会写 C」也能开发 eBPF;bpf2goaya-tool 让构建链路标准化。
  • 跨平台探索:Windows 上的 eBPF(基于 Windows 内核的 eBPF 实现)也在推进,虽然生态还不如 Linux。

但边界也要认清

eBPF 不是银弹。它的天花板是内核版本:能力跟内核绑定,老内核(<5.x)很多特性用不了,升级内核在不少环境里仍是阻力。Verifier 的保守也意味着表达力受限——你没法在内核里写任意复杂逻辑,有界循环、512B 栈、白名单 helper 都是硬约束。学习曲线也确实存在:BTF、CO-RE、libbpf skeleton、各 hook 语义,够一个新人或团队啃上几周。

给开发者的上手路线

  1. 先用 bpftrace 解决一个真实问题(比如「谁在疯狂读这个文件」「哪次系统调用最慢」),建立直觉。
  2. 再用 BCC 写个小工具,理解「内核程序 + 用户态消费」的数据流。
  3. 然后切到 cilium/ebpf / aya,把工具写成能随镜像分发的生产守护进程,用上 CO-RE。
  4. 最后看 Cilium / Tetragon,理解工业级 eBPF 系统是怎么组织网络、安全、可观测三件套的。

不用一口吃成胖子。从一个「在内核里数个数」的小程序开始,你会很快体会到那种「原来操作系统还能这么玩」的爽感——而这,正是 2026 年每个服务端、SRE、平台工程师都值得掌握的底牌。

延伸阅读

  • Cilium 官方文档与 eBPF 数据中心白皮书(Isovalent/Cisco)
  • 《BPF Performance Tools》(Brendan Gregg)——eBPF 可观测性的圣经
  • Linux 内核 Documentation/bpf/ 目录
  • eBPF.io 社区与 eBPF Foundation 博客
  • bpftoollibbpfcilium/ebpfaya 的源码与示例

本文所有架构与机制均基于 eBPF / Cilium 公开设计与内核文档;代码示例在对应内核与工具链版本下可直接编译运行,生产落地请结合自身集群内核版本与权限模型做适配。

推荐文章

MySQL用命令行复制表的方法
2024-11-17 05:03:46 +0800 CST
基于Webman + Vue3中后台框架SaiAdmin
2024-11-19 09:47:53 +0800 CST
在Rust项目中使用SQLite数据库
2024-11-19 08:48:00 +0800 CST
CSS 媒体查询
2024-11-18 13:42:46 +0800 CST
JS中 `sleep` 方法的实现
2024-11-19 08:10:32 +0800 CST
Vue3中如何处理路由和导航?
2024-11-18 16:56:14 +0800 CST
程序员茄子在线接单