本文基于 Linux 6.x 内核源码。部分 API 在不同版本间可能有差异。
2014 年,Alexei Starovoitov 向 Linux 内核提交了 extended BPF(eBPF)的第一批 patch。彼时没有人预料到,这个从 Berkeley Packet Filter 演化而来的字节码虚拟机,会在十年间重塑 Linux 网络栈的每一个层次——从驱动层的 XDP 到传输层的可编程拥塞控制,从用户态零拷贝的 AF_XDP 到 SmartNIC 硬件卸载。
eBPF 之于 Linux 网络,类似 JavaScript 之于浏览器:它将一个”只能被内核开发者修改的静态系统”变成了”任何人都可以安全注入逻辑的可编程平台”。但与 JavaScript 不同的是,eBPF 程序运行在内核态,经过 verifier 的静态验证,拥有接近原生代码的执行速度。
本文是 eBPF 网络编程 的延续。那篇文章聚焦于”eBPF 能做什么”;本文聚焦于”eBPF 是怎么一步步走到今天的”,以及”它将走向何方”。
一、从 cBPF 到 eBPF:一个过滤器的进化
经典 BPF:只读的包过滤器
1992 年,Steven McCanne 和 Van Jacobson 在 USENIX
会议上发表了 The BSD Packet Filter 论文。经典
BPF(cBPF)的设计目标很简单:让 tcpdump
这样的工具能在内核态高效过滤网络包,而不需要把每个包都拷贝到用户态再判断。
cBPF 的架构极为简洁:
- 2 个 32 位寄存器(A 和 X)
- 一个固定大小的 scratch memory(16 个 slot)
- 指令集只有 load、store、ALU、jump、return
- 只读:程序只能检查包的内容,不能修改
/* cBPF 指令示例:匹配 TCP 端口 80 */
struct sock_filter code[] = {
BPF_STMT(BPF_LD+BPF_H+BPF_ABS, 12), /* 加载 EtherType */
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, 0x0800, 0, 3), /* IP? */
BPF_STMT(BPF_LD+BPF_B+BPF_ABS, 23), /* 加载协议字段 */
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, 6, 0, 1), /* TCP? */
BPF_STMT(BPF_RET+BPF_K, 65535), /* 匹配:返回包长 */
BPF_STMT(BPF_RET+BPF_K, 0), /* 不匹配:丢弃 */
};cBPF 在包过滤领域运行了超过 20 年,但它的局限性也很明显:只能过滤,不能修改;只有 2 个寄存器,表达能力有限;没有 map 数据结构,程序间无法共享状态。
eBPF:一个完整的内核可编程框架
2014 年(kernel 3.18),eBPF 带来了根本性的架构升级:
| 特性 | cBPF | eBPF |
|---|---|---|
| 寄存器 | 2 个 32-bit | 11 个 64-bit(R0-R10) |
| 指令宽度 | 32-bit | 64-bit |
| 栈空间 | 16 slot scratch | 512 字节栈 |
| Map 支持 | 无 | hash、array、ringbuf 等 20+ 种 |
| 调用约定 | 无 | 兼容 x86-64 ABI |
| JIT | 可选 | 所有主流架构 |
| helper 函数 | 无 | 数千个内核 helper |
| 能力 | 只读过滤 | 读写、重定向、修改、跟踪 |
eBPF 的关键设计决策是 verifier:每个 BPF 程序在加载时都会经过静态分析,确保程序一定会终止(无无限循环)、不会越界访问内存、不会泄漏内核指针。这让 eBPF 程序可以安全地运行在内核态,而不需要像内核模块那样拥有完全的内核权限。
/* eBPF 程序的典型结构 */
SEC("xdp")
int xdp_filter(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
/* verifier 要求的边界检查 */
if ((void *)(eth + 1) > data_end)
return XDP_DROP;
if (eth->h_proto == htons(ETH_P_IP)) {
/* 处理 IP 包 */
return XDP_PASS;
}
return XDP_DROP;
}二、2016:XDP——驱动层的可编程数据面
hook 位置:比 tc 更早,比 DPDK 更安全
XDP(eXpress Data Path)是 eBPF
网络编程的第一个杀手级应用。它在 kernel 4.8(2016
年)合入主线,核心创新是把 BPF hook
放在了网络栈的最早期——NIC 驱动的 napi_poll
路径中,在 sk_buff 分配之前。
NIC 硬件 → DMA → 驱动 rx_ring → [XDP hook] → sk_buff 分配 → tc → netfilter → 协议栈
↑
这里执行 BPF 程序
XDP 程序的返回值决定包的命运:
| Action | 含义 | 典型用途 |
|---|---|---|
XDP_DROP |
在驱动层直接丢弃 | DDoS 防御 |
XDP_PASS |
继续走正常协议栈 | 默认行为 |
XDP_TX |
从同一网卡发回 | 负载均衡器反射 |
XDP_REDIRECT |
转发到另一个网卡/CPU/socket | L3/L4 转发 |
XDP_ABORTED |
错误路径(触发 tracepoint) | 调试 |
性能:单核 24Mpps 的秘密
XDP 的性能优势来自三个设计决策:
避免 sk_buff 分配:
sk_buff是一个包含 200+ 字段的复杂结构体,每次分配和初始化都有显著开销。XDP 直接操作xdp_buff(只包含 data 指针和长度),极其轻量。批量处理:XDP 在 NAPI poll 循环中运行,天然支持批量处理。一次 NAPI poll 可以处理 64 个包,中间没有任何中断或上下文切换。
无锁路径:XDP 程序运行在 softirq 上下文中,每个 CPU 有独立的处理路径,不需要任何锁。
# 用 xdp-tools 加载一个简单的 XDP 程序
xdp-loader load eth0 xdp_drop.o
# 查看 XDP 程序状态
xdp-loader status eth0
# 用 xdp-bench 测试转发性能
xdp-bench redirect eth0 eth1Facebook 在 2018 年的 CoNEXT 论文中报告,XDP 在商用硬件上实现了单核 24Mpps 的转发速率,接近理论线速。更重要的是,XDP 不需要像 DPDK 那样独占 CPU 核心或绕过内核——它与 Linux 网络栈完全兼容。
XDP 的三种运行模式
# Native XDP:驱动原生支持,最高性能
ip link set dev eth0 xdpdrv obj prog.o sec xdp
# Generic XDP:所有网卡都支持,但性能较低(在 sk_buff 之后执行)
ip link set dev eth0 xdpgeneric obj prog.o sec xdp
# Offloaded XDP:程序卸载到 SmartNIC 硬件执行
ip link set dev eth0 xdpoffload obj prog.o sec xdp三、2017:TC BPF 与 LWT BPF——协议栈中的可编程点
TC BPF:ingress/egress 的全功能 hook
XDP 虽然快,但它的能力有限——它看到的是原始的
xdp_buff,无法访问 sk_buff
的丰富元数据(比如路由决策结果、conntrack 状态)。TC
BPF(cls_bpf)填补了这个空白。
TC BPF 在 tc(traffic control)子系统的
ingress 和 egress 路径上挂载 BPF 程序。与 XDP 不同,TC BPF
操作的是完整的
sk_buff,可以访问和修改包的几乎所有字段:
SEC("tc")
int tc_ingress(struct __sk_buff *skb)
{
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
/* 可以读取 sk_buff 的元数据 */
__u32 ifindex = skb->ifindex;
__u32 mark = skb->mark;
/* 可以修改包内容 */
bpf_skb_store_bytes(skb, offset, &new_data, len, 0);
/* 可以重定向到其他接口 */
return bpf_redirect(target_ifindex, 0);
}TC BPF 的 action 集合比 XDP 更丰富:
| Action | 含义 |
|---|---|
TC_ACT_OK |
继续正常处理 |
TC_ACT_SHOT |
丢弃包 |
TC_ACT_REDIRECT |
重定向到指定接口 |
TC_ACT_STOLEN |
程序已消费此包 |
TC_ACT_PIPE |
继续到下一个 filter |
LWT BPF:轻量隧道的可编程封装
LWT(LightWeight Tunnel)BPF 是一个相对小众但重要的 hook 点。它允许在路由决策之后、实际发送之前,用 BPF 程序执行自定义的隧道封装/解封操作。
# 配置 LWT BPF 路由
ip route add 10.0.0.0/24 encap bpf xmit obj lwt_encap.o sec encap \
dev eth0 via 192.168.1.1Cilium 在早期版本中曾使用 LWT BPF 实现 pod-to-pod
的隧道封装。后来随着 TC BPF 和 bpf_redirect
系列 helper 的成熟,LWT BPF 在容器网络中的使用逐渐减少。
四、2018:AF_XDP 与 Socket Ops——用户态与传输层的可编程性
AF_XDP:内核旁路的零拷贝收包
AF_XDP 是 XDP 能力向用户态延伸的产物。它在 kernel 4.18
引入,提供了一个新的 socket
地址族(AF_XDP),允许用户态程序通过共享内存(UMEM)直接收发网络包,绕过内核协议栈。
AF_XDP 的架构基于四个 ring buffer:
UMEM (共享内存区域)
┌──────────────────────────────────────┐
│ chunk 0 │ chunk 1 │ chunk 2 │ ... │
└──────────────────────────────────────┘
↑ ↑ ↑
┌────┴────┐ ┌────┴────┐
│ FILL │ │ COMP │ ← 用户态 → 内核 的 descriptor 传递
│ Ring │ │ Ring │
└─────────┘ └─────────┘
┌─────────┐ ┌─────────┐
│ RX │ │ TX │ ← 包 descriptor 的交付
│ Ring │ │ Ring │
└─────────┘ └─────────┘
工作流程:
- 用户态通过 FILL ring 提交空闲 UMEM chunk 的地址给内核
- XDP 程序通过
XDP_REDIRECT将包重定向到 AF_XDP socket - 内核把包数据写入 UMEM chunk,通过 RX ring 通知用户态
- 用户态处理完后,通过 TX ring 发送,通过 COMPLETION ring 回收
/* 创建 AF_XDP socket 并配置 UMEM */
int fd = socket(AF_XDP, SOCK_RAW, 0);
struct xsk_umem_config cfg = {
.fill_size = 2048, .comp_size = 2048,
.frame_size = 4096, .frame_headroom = 0,
};
xsk_umem__create(&umem, buffer, size, &fq, &cq, &cfg);
struct xsk_socket_config xsk_cfg = {
.rx_size = 2048, .tx_size = 2048,
.bind_flags = XDP_ZEROCOPY,
};
xsk_socket__create(&xsk, ifname, queue_id, umem, &rx, &tx, &xsk_cfg);AF_XDP 的典型用途包括:高性能网络监控(替代 DPDK + libpcap)、用户态协议栈(如 F-Stack)、以及金融低延迟交易系统。
BPF Socket Ops:传输层的可编程回调
BPF_PROG_TYPE_SOCK_OPS 在 kernel 4.13
引入,它在 TCP socket 的生命周期关键事件上提供 hook:
SEC("sockops")
int bpf_sockops(struct bpf_sock_ops *skops)
{
switch (skops->op) {
case BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB:
/* TCP 主动连接建立完成 */
break;
case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB:
/* TCP 被动连接建立完成 */
break;
case BPF_SOCK_OPS_TCP_CONNECT_CB:
/* connect() 调用时 */
break;
case BPF_SOCK_OPS_RWND_INIT:
/* 设置初始接收窗口 */
return 65535;
}
return 1;
}sockops 的一个重要应用是 socket-level
重定向:Cilium 使用
BPF_MAP_TYPE_SOCKHASH 和
bpf_msg_redirect_hash 将同节点 pod 之间的 TCP
流量直接从一个 socket 转发到另一个 socket,完全绕过 TCP/IP
协议栈。这在 Cilium
网络方案深度解析 中有详细讨论。
五、2019:bpf_redirect_peer——容器网络的性能突破
veth pair 的性能痛点
在容器网络中,pod 流量通常通过 veth pair 在 pod namespace 和 host namespace 之间传递。一个包从 pod 发出,经过 veth 到达 host 端时,会经历完整的 ingress 处理路径:
Pod namespace Host namespace
┌─────────────────┐ ┌──────────────────────────┐
│ 应用程序 │ │ │
│ ↓ sendmsg │ │ tc ingress │
│ TCP/IP 协议栈 │ │ ↓ │
│ ↓ │ │ netfilter PREROUTING │
│ tc egress │ │ ↓ │
│ ↓ │ │ 路由决策 │
│ veth-pod ────────┼──────────── │→ veth-host │
└─────────────────┘ softirq │ (完整 ingress 处理) │
重新调度 └──────────────────────────┘
问题在于:veth pair 的对端收包会触发一次
netif_rx,进入 backlog 队列,然后被 softirq
重新调度处理。这意味着每个包至少多一次上下文切换和完整的
ingress 路径遍历。
bpf_redirect_peer:跳过 ingress 处理
bpf_redirect_peer(kernel
5.10)的核心思想是:既然我们知道 veth
对端是同一台机器上的另一个
namespace,为什么不直接把包”注入”到对端的 ingress 路径,跳过
netif_rx 和 backlog 队列?
/* 在 host 端的 tc ingress BPF 程序中 */
SEC("tc")
int tc_to_pod(struct __sk_buff *skb)
{
/* 直接将包注入 pod namespace 的 veth 端 */
return bpf_redirect_peer(pod_veth_ifindex, 0);
}与普通的 bpf_redirect
相比,bpf_redirect_peer 避免了以下开销:
- 跳过 backlog 队列:不经过
netif_rx,不需要 softirq 重新调度 - 跳过对端 ingress 处理:直接进入对端的 TC ingress 点
- 保持 CPU 亲和性:包在同一个 CPU 上完成从 host 到 pod 的传递
Cilium 的基准测试显示,使用
bpf_redirect_peer 后,pod-to-pod 的吞吐量提升约
15-25%,延迟降低约 10-20%。
bpf_redirect_neigh:L3 邻居表加速
bpf_redirect_neigh(kernel
5.10)是另一个重要的转发加速 helper。它在执行
bpf_redirect 的同时,直接查找内核的
FIB(Forwarding Information Base)和邻居表,填充 L2
头部:
SEC("tc")
int tc_forward(struct __sk_buff *skb)
{
struct bpf_fib_lookup fib = {};
fib.ifindex = skb->ifindex;
/* 设置 L3 查找参数 */
int ret = bpf_fib_lookup(skb, &fib, sizeof(fib), 0);
if (ret == BPF_FIB_LKUP_RET_SUCCESS) {
/* FIB 查找成功,直接设置 MAC 并转发 */
return bpf_redirect_neigh(fib.ifindex, &fib.params, sizeof(fib.params), 0);
}
return TC_ACT_OK;
}这两个 helper 是 Cilium 实现 “eBPF host-routing” 模式的基础,使得容器网络可以完全绕过 iptables/netfilter。更详细的分析见 eBPF 与 netfilter 的对比。
六、2020:struct_ops——可编程拥塞控制
TCP 拥塞控制的内核框架
Linux 内核的 TCP 拥塞控制是通过
struct tcp_congestion_ops
实现的——这是一个包含多个函数指针的结构体,每个 TCP
连接可以绑定不同的拥塞控制算法:
/* include/net/tcp.h */
struct tcp_congestion_ops {
/* 必须实现 */
void (*cong_avoid)(struct sock *sk, u32 ack, u32 acked);
u32 (*ssthresh)(struct sock *sk);
/* 可选回调 */
void (*init)(struct sock *sk);
void (*release)(struct sock *sk);
void (*cwnd_event)(struct sock *sk, enum tcp_ca_event ev);
void (*set_state)(struct sock *sk, u8 new_state);
void (*pkts_acked)(struct sock *sk, const struct ack_sample *sample);
char name[TCP_CA_NAME_MAX];
struct module *owner;
};传统上,要添加一个新的拥塞控制算法,你需要编写一个内核模块、编译、加载、测试。这个流程对于数据中心运维团队来说过于重量级——他们可能只想微调 cubic 的某个参数,或者在特定场景下切换到 BBR 的变体。
BPF struct_ops:运行时注入拥塞控制
BPF struct_ops(kernel 5.6)允许用 BPF 程序替换
tcp_congestion_ops
中的函数指针。这意味着你可以在不重启内核、不加载内核模块的情况下,动态地部署和切换拥塞控制算法。
/* BPF 实现的自定义拥塞控制 */
SEC("struct_ops/my_cc_init")
void BPF_PROG(my_cc_init, struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct my_cc_data *data = inet_csk_ca(sk);
data->min_rtt = U32_MAX;
data->mode = MODE_STARTUP;
}
SEC("struct_ops/my_cc_cong_avoid")
void BPF_PROG(my_cc_cong_avoid, struct sock *sk, u32 ack, u32 acked)
{
struct tcp_sock *tp = tcp_sk(sk);
struct my_cc_data *data = inet_csk_ca(sk);
if (tp->snd_cwnd < tp->snd_ssthresh) {
/* 慢启动阶段 */
tp->snd_cwnd += acked;
} else {
/* 拥塞避免阶段:自定义增长函数 */
u32 increment = max(1U, acked * acked / tp->snd_cwnd);
tp->snd_cwnd += increment;
}
}
SEC("struct_ops/my_cc_ssthresh")
__u32 BPF_PROG(my_cc_ssthresh, struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
/* 自定义 ssthresh 计算 */
return max(tp->snd_cwnd * 7 / 10, 2U);
}
SEC(".struct_ops")
struct tcp_congestion_ops my_cc = {
.init = (void *)my_cc_init,
.cong_avoid = (void *)my_cc_cong_avoid,
.ssthresh = (void *)my_cc_ssthresh,
.name = "my_bpf_cc",
};加载和使用:
# 编译 BPF 程序
clang -O2 -target bpf -c my_cc.bpf.c -o my_cc.bpf.o
# 加载 struct_ops(通过 libbpf 或 bpftool)
bpftool struct_ops register my_cc.bpf.o
# 查看已注册的 struct_ops
bpftool struct_ops list
# 为特定 socket 设置拥塞控制算法
setsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, "my_bpf_cc", sizeof("my_bpf_cc"));
# 或者全局设置
sysctl -w net.ipv4.tcp_congestion_control=my_bpf_ccstruct_ops 的应用场景
struct_ops 不仅限于拥塞控制。随着内核的演进,越来越多的”ops 结构体”可以被 BPF 替换:
| struct_ops 目标 | 引入版本 | 用途 |
|---|---|---|
tcp_congestion_ops |
5.6 | 自定义拥塞控制算法 |
bpf_struct_ops
(通用) |
5.13 | 框架扩展 |
sched_ext_ops |
6.12 | 可编程 CPU 调度器 |
对于数据中心网络,struct_ops 的价值在于:
- 快速迭代:拥塞控制算法的 A/B 测试不再需要滚动重启
- 精细控制:不同的 service 可以使用不同的 CC 算法
- 安全性:verifier 保证 BPF CC 不会导致内核崩溃
七、2021-2022:基础设施成熟
BPF_LINK:程序生命周期管理
在早期,BPF 程序通过 pin 到
bpffs(/sys/fs/bpf/)来保持存活。这种机制有几个问题:
- 程序的引用计数管理容易出错
- 没有标准化的 attach/detach 语义
- 多个程序 attach 到同一个 hook 时的优先级不明确
BPF_LINK(kernel 5.7+)提供了统一的程序附着抽象:
/* 使用 bpf_link 附着 XDP 程序 */
struct bpf_link *link = bpf_program__attach_xdp(prog, ifindex);
/* 程序随 link 的生命周期自动管理 */
/* 关闭 link fd 时自动 detach */
bpf_link__destroy(link);
/* 或者通过 bpftool 查看和管理 */# 查看所有 bpf_link
bpftool link list
# 输出示例
# 1: xdp prog 42 dev eth0
# 2: tc prog 43 dev eth0 direction ingress
# 3: struct_ops prog 44 name my_bpf_ccBPF_LINK 的另一个重要特性是 replace 语义:可以原子地替换一个已附着的程序,而不会出现”先 detach 再 attach”的窗口期。这对于生产环境中的热升级至关重要。
BPF arena:突破内存限制
传统 BPF 程序的栈空间限制为 512 字节,map value 的大小也有限制。BPF arena(kernel 6.4+)引入了一种新的内存模型:BPF 程序和用户态程序可以共享一片大内存区域,通过类似 mmap 的机制访问。
/* 定义一个 BPF arena */
struct {
__uint(type, BPF_MAP_TYPE_ARENA);
__uint(map_flags, BPF_F_MMAPABLE);
__uint(max_entries, 1024 * 1024); /* 1M entries */
} arena SEC(".maps");
SEC("xdp")
int xdp_with_arena(struct xdp_md *ctx)
{
/* 在 arena 中分配和访问大块内存 */
void *ptr = bpf_arena_alloc(&arena, 4096);
if (!ptr)
return XDP_PASS;
/* 可以在 arena 中构建复杂数据结构 */
struct flow_table *ft = ptr;
/* ... */
return XDP_PASS;
}BPF arena 对网络应用的意义在于:可以在 BPF 程序中构建更复杂的数据结构(如完整的连接跟踪表、路由缓存),而不受 512 字节栈的限制。
BPF token:容器化环境的权限委托
BPF token(kernel 6.9+)解决了容器中运行 BPF 程序需要
CAP_BPF 或 CAP_SYS_ADMIN
权限的问题。特权进程创建 token
后,可以将其委托给非特权容器,容器只能执行 token
允许的操作(如仅限 xdp/tc 程序类型)。
bpf_iter:高效的网络状态遍历
bpf_iter(kernel
5.8+)提供了一种高效遍历内核数据结构的机制。对于网络场景,它可以遍历所有
TCP/UDP socket、路由表项、邻居表项等:
SEC("iter/tcp")
int dump_tcp(struct bpf_iter__tcp *ctx)
{
struct sock_common *sk_common = ctx->sk_common;
struct tcp_sock *tp;
struct seq_file *seq = ctx->meta->seq;
if (!sk_common)
return 0;
tp = bpf_skc_to_tcp_sock(sk_common);
if (!tp)
return 0;
/* 输出 TCP 连接信息 */
BPF_SEQ_PRINTF(seq, "%pI4:%d -> %pI4:%d cwnd=%u srtt=%u\n",
&sk_common->skc_rcv_saddr,
sk_common->skc_num,
&sk_common->skc_daddr,
ntohs(sk_common->skc_dport),
tp->snd_cwnd,
tp->srtt_us >> 3);
return 0;
}# 使用 bpf_iter 遍历所有 TCP 连接
bpftool iter pin tcp_iter.o /sys/fs/bpf/tcp_iter
cat /sys/fs/bpf/tcp_iter与传统的 /proc/net/tcp
相比,bpf_iter
可以在内核态完成过滤和聚合,避免将大量数据拷贝到用户态。
八、BPF 网络的未来方向
可编程 QoS 与 Traffic Shaping
传统的 Linux QoS 依赖 tc 的 qdisc
层级(HTB、CBQ、fq_codel 等),配置复杂且灵活性有限。eBPF
正在改变这一局面:
EDT(Earliest Departure Time)模型:BPF 程序可以为每个包设置精确的发送时间戳(
skb->tstamp),配合fqqdisc 实现亚微秒精度的发送调度。Google 的 Carousel 系统就是基于这个思路。可编程 qdisc:未来的 struct_ops 扩展可能允许用 BPF 实现完全自定义的 qdisc,包括入队/出队逻辑和调度策略。
SEC("tc")
int edt_pacer(struct __sk_buff *skb)
{
/* 基于流优先级设置发送时间 */
__u64 now = bpf_ktime_get_ns();
__u64 delay_ns = calculate_delay(skb);
skb->tstamp = now + delay_ns;
return TC_ACT_OK;
}BPF-based NIC Offload(SmartNIC)
SmartNIC(如 NVIDIA BlueField、Intel IPU、Pensando DSC)正在成为数据中心网络的标准配置。eBPF 与 SmartNIC 的结合有两条路径:
XDP offload:将 XDP 程序直接卸载到 SmartNIC 的 FPGA 或专用处理器上执行。Netronome(现已被 Corigine 收购)是第一个支持 XDP offload 的厂商。
P4-to-eBPF 编译:SmartNIC 通常支持 P4 语言编写的数据面程序。通过 P4-to-eBPF 编译器(如
p4c-ebpf),可以将 P4 程序编译为 eBPF 字节码,在 SmartNIC 或主机上运行。
P4 程序
│
↓
┌──────────┐
│ p4c-ebpf │
└──────────┘
│
┌───────┴───────┐
↓ ↓
eBPF 字节码 FPGA 比特流
│ │
↓ ↓
主机 XDP/TC SmartNIC 硬件
SmartNIC offload 面临的挑战是 eBPF 能力子集的差异:硬件能执行的 BPF 指令集通常比主机 CPU 上的要小得多。verifier 需要根据 offload 目标限制程序的复杂度。
P4 与 eBPF 融合
P4 和 eBPF 代表了两种不同的网络可编程哲学:
| 维度 | P4 | eBPF |
|---|---|---|
| 目标 | 交换机/NIC 硬件 | 通用 CPU(主机内核) |
| 抽象层次 | 协议无关的 parser + match-action | 通用字节码虚拟机 |
| 编译目标 | FPGA / ASIC / 软件交换机 | JIT 原生代码 / SmartNIC |
| 优势 | 线速处理、硬件确定性 | 灵活性、生态丰富 |
| 局限 | 表达能力受限、硬件依赖 | CPU 密集、无法线速 |
两者的融合趋势越来越明显。Intel Tofino 交换芯片支持
P4,同时其 SmartNIC 产品也支持 eBPF offload。开源项目如
p4c-xdp 将 P4 程序编译为 XDP BPF
程序,使得同一份网络逻辑可以在不同目标上运行。
NFV 中的 eBPF
网络功能虚拟化(NFV)正在从 VM-based 向 container-based 演进。eBPF 在 NFV 中的价值:
- Service Function Chaining:使用 BPF
重定向(
bpf_redirect)实现高效的 SFC,替代 VXLAN + OVS 的传统方案。 - 动态网络功能:防火墙规则、NAT、负载均衡等网络功能可以作为 BPF 程序动态加载和卸载,无需重启 VNF。
- 性能监控:eBPF 可以在不修改 VNF 代码的情况下,注入细粒度的性能监控探针。
ETSI NFV 工作组已经开始关注 eBPF 在 NFV 架构中的定位,虽然标准化工作仍在早期阶段。
九、eBPF 的局限性与批评
verifier 限制
BPF verifier 是 eBPF 安全性的基石,但也是最大的开发痛点:
复杂度上限:verifier 对程序的指令数和分支复杂度有硬性限制。在 kernel 5.x 中,单个程序的验证指令上限是 100 万条。看起来很多,但对于包含大量条件分支和 map 查找的网络程序,很容易触及这个限制。
/* 常见的 verifier 错误 */
libbpf: prog 'tc_main': BPF program is too large. Processed 1000001 insns
libbpf: prog 'tc_main': -- BEGIN PROG LOAD LOG --
processed 1000001 insns (limit 1000000) ...
指针追踪的保守性:verifier 对指针的追踪非常保守。即使你确定某个指针不为 NULL,verifier 可能仍然要求显式的 NULL 检查。这导致 BPF 代码中充斥着防御性检查:
/* verifier 要求的冗余检查 */
struct iphdr *ip = data + sizeof(struct ethhdr);
if ((void *)(ip + 1) > data_end) /* 必须检查 */
return XDP_DROP;
__u8 proto = ip->protocol;
/* 即使刚检查过边界,访问下一层头部仍需再次检查 */
if (proto == IPPROTO_TCP) {
struct tcphdr *tcp = (void *)ip + (ip->ihl * 4);
if ((void *)(tcp + 1) > data_end) /* 又要检查 */
return XDP_DROP;
/* ... */
}循环限制:虽然 kernel 5.3+
允许有界循环(bpf_loop),但 verifier
对循环边界的分析仍然保守。复杂的循环嵌套或动态循环边界可能无法通过验证。
调试困难
eBPF 程序的调试体验远不如用户态程序:
- 没有 GDB:BPF 程序运行在内核态,无法使用传统调试器
- printf 调试:
bpf_printk输出到 trace_pipe,格式有限,性能影响大 - 崩溃信息不足:verifier 拒绝程序时的错误信息经常令人困惑
- BTF 依赖:CO-RE(Compile Once Run Everywhere)依赖目标内核的 BTF 信息,缺失时难以诊断
# 常用的调试手段
# 1. bpf_printk 追踪
cat /sys/kernel/debug/tracing/trace_pipe
# 2. bpftool 查看程序和 map 状态
bpftool prog show
bpftool map dump id <map_id>
# 3. verifier 日志
bpftool prog load prog.o /sys/fs/bpf/prog --debug内核紧耦合与升级风险
eBPF 程序虽然通过 CO-RE 和 BTF 实现了一定程度的内核版本兼容性,但仍然与内核紧密耦合:helper 函数签名可能变化,新的 map 类型只在特定内核版本之后可用,verifier 行为在不同版本间可能不同,kfunc 没有稳定性保证。这对于需要支持多个内核版本的项目(如 Cilium)来说是一个持续的工程挑战。
安全性考量
verifier 并非万无一失。历史上出现过多次 verifier bypass 漏洞(如 CVE-2021-3490、CVE-2022-23222),攻击者可以构造特殊的 BPF 程序绕过验证,实现任意内核内存读写。
# 默认情况下,非特权用户可以加载 BPF 程序
# 建议在生产环境中禁用
sysctl -w kernel.unprivileged_bpf_disabled=1
eBPF 的安全模型本质上依赖于 verifier 的正确性——这是一个不断被发现和修补漏洞的攻击面。
十、学术前沿:论文综述
XDP - The Express Data Path (CoNEXT 2018)
Toke Hoiland-Jorgensen 等人的这篇论文是 XDP 的奠基性文献。论文的核心贡献:
- 系统性描述了 XDP 的设计哲学:在内核中提供”软件 offload”能力,而不是像 DPDK 那样绕过内核
- 性能评估:在 Intel i40e 网卡上测得单核 24Mpps 的 XDP_DROP 性能,XDP_TX 约 12Mpps
- 用例分析:DDoS 缓解(Cloudflare)、负载均衡(Facebook Katran)、路由器(BIRD + XDP)
论文中一个关键的设计取舍:XDP 选择了”在驱动中执行 BPF”而非”在用户态执行”,这保证了与内核协议栈的兼容性,但牺牲了一些极端场景下的性能(相比 DPDK 的 kernel bypass)。
Cilium: Networking and Security for Containers (2020)
Thomas Graf 等人发表的这篇论文详细描述了 Cilium 如何利用 eBPF 构建一个完整的容器网络和安全平台:
- datapath 架构:使用 XDP + TC BPF 替代 iptables,实现 L3/L4 负载均衡和网络策略
- 身份系统:基于 BPF map 的安全身份(security identity)机制,替代传统的 IP-based 策略
- 性能数据:在 100 节点集群中,Cilium eBPF 模式比 iptables 模式快 3-5 倍
这篇论文是理解”eBPF 如何在生产环境中替代传统网络栈”的必读文献。相关内容在 Cilium 深度解析 中有进一步讨论。
hXDP: Efficient Software Packet Processing on FPGA NICs (OSDI 2020)
Marco Spaziani Brunella 等人提出了一种在 FPGA SmartNIC 上高效执行 XDP 程序的方法。核心贡献:
- 硬件流水线:将 eBPF 指令映射到 FPGA 上的多级流水线,实现 40Gbps 线速处理
- 并行化策略:多个 XDP 程序实例在 FPGA 上并行执行,每个实例处理不同的包
- BPF map 加速:使用 FPGA 上的 BRAM 和 HBM 实现低延迟的 map 访问
hXDP 的意义在于证明了 eBPF 不仅是一个”软件虚拟机”,它的程序模型可以高效地映射到硬件加速器上。
BMC: Accelerating Memcached using Safe In-kernel Caching and Pre-stack Processing (NSDI 2021)
Yoann Ghigoff 等人的 BMC 论文展示了 eBPF 在应用层加速中的潜力:
- 核心思想:在 XDP 层拦截 Memcached 的 GET 请求,直接从 BPF map 中查找缓存值并回复,绕过整个协议栈和用户态 Memcached 进程
- 实现:XDP 程序解析 UDP/TCP Memcached 协议,BPF hash map 存储热点 key-value
- 性能:GET 请求延迟降低 18 倍,吞吐量提升至 原来的 12 倍
- 缓存一致性:通过 kprobe 拦截 Memcached 的 SET/DELETE 操作,同步更新 BPF map
BMC 是”eBPF 不只是网络工具”的最佳证明——它模糊了”网络处理”和”应用逻辑”的边界。
Electrode: Accelerating Distributed Protocols with eBPF (NSDI 2023)
Yang Zhou 等人的 Electrode 论文将 eBPF 应用于分布式共识协议的加速:
- 核心思想:将 Paxos/Raft 的消息处理逻辑(投票、心跳检测、日志复制的确认)用 eBPF 在内核态实现,减少用户态-内核态切换
- 实现:XDP 程序识别共识协议消息,在内核态完成快速路径处理;只有需要状态机更新的操作才上报用户态
- 性能:Paxos 延迟降低 50%,吞吐量提升 2-3 倍
Electrode 代表了 eBPF 应用的一个新方向:不仅仅是网络功能,而是将应用的关键路径下沉到内核。
十一、eBPF 在 Kubernetes 网络中的实践
Cilium 的 eBPF datapath 演进
Cilium 是 eBPF 在 Kubernetes 网络中最成熟的实践。其 datapath 经历了多次重大演进:
Phase 1 (2017-2018):使用 TC BPF + veth,替代 kube-proxy 的 iptables 规则。
Phase 2 (2019-2020):引入 XDP 加速(L4 负载均衡)、socket-level 重定向(同节点 pod 通信)。
Phase 3 (2021+):使用
bpf_redirect_peer +
bpf_redirect_neigh 实现 host-routing
模式,完全绕过 host namespace 的 iptables:
# 启用 Cilium eBPF host-routing
helm upgrade cilium cilium/cilium \
--set routingMode=native \
--set bpf.masquerade=true \
--set kubeProxyReplacement=true在这种模式下,一个包从 pod A 到 pod B 的路径是:
Pod A → veth → [TC BPF: 查找目标 pod]
→ bpf_redirect_peer → Pod B 的 veth
→ [TC BPF: 安全策略检查] → Pod B
与传统的 iptables 路径相比:
Pod A → veth → host netns → iptables PREROUTING
→ routing → iptables FORWARD → iptables POSTROUTING
→ veth → Pod B netns → iptables PREROUTING → Pod B
性能差异是数量级的:在 100+ Service 的场景下,eBPF 模式的 Service 访问延迟比 iptables 模式低 5-10 倍。更详细的对比见 eBPF 与 netfilter 的对比。
Calico eBPF 模式
Calico 从 v3.13 开始提供 eBPF 数据面模式,作为其传统 iptables 模式的替代。Calico eBPF 模式的特点:
- 使用 TC BPF 实现 L3/L4 负载均衡,替代 kube-proxy
- 支持 DSR(Direct Server Return)模式
- 保留 Calico 的 BGP 路由能力
# 启用 Calico eBPF 模式
kubectl patch installation.operator.tigera.io default \
--type merge \
-p '{"spec":{"calicoNetwork":{"linuxDataplane":"BPF"}}}'与 Cilium 相比,Calico 的 eBPF 模式更侧重于”eBPF 作为 iptables 的高性能替代”,而非 Cilium 那样构建完整的 eBPF-native 网络栈。两者的详细对比见 Calico 深度解析 和 Cilium 深度解析。
十二、eBPF 生态工具链
工程实践中,eBPF 网络程序的开发和调试依赖一系列工具:
| 工具 | 用途 | 典型命令 |
|---|---|---|
bpftool |
BPF 对象管理和调试 | bpftool prog show |
libbpf |
C 语言 BPF 开发库 | CO-RE skeleton 生成 |
cilium/ebpf |
Go 语言 BPF 开发库 | bpf2go
代码生成 |
bpftrace |
高级追踪脚本 | bpftrace -e 'kprobe:tcp_*' |
xdp-tools |
XDP 程序管理 | xdp-loader load eth0 prog.o |
iproute2 |
TC BPF 管理 | tc filter add ... bpf obj prog.o |
retsnoop |
BPF 函数追踪调试 | retsnoop -e '*xdp*' |
bpf-linker |
BPF 对象文件链接 | 多文件 BPF 程序构建 |
典型的开发流程:编写 BPF C 程序 →
bpftool gen skeleton 生成加载框架 →
clang -target bpf 编译 → 用户态程序加载 →
bpftool prog show 监控。
# 完整编译和加载流程
clang -O2 -target bpf -g -c my_xdp.bpf.c -o my_xdp.bpf.o
bpftool gen skeleton my_xdp.bpf.o > my_xdp.skel.h
gcc -o my_xdp my_xdp.c -lbpf -lelf -lz
sudo ./my_xdp eth0十三、从 hook 到操作系统:eBPF 的范式转变
回顾 eBPF 网络能力的十年演化,我们可以看到一个清晰的范式转变:
阶段一:hook 时代(2014-2017)。eBPF 作为现有网络栈的”钩子”存在——你可以在特定位置(socket filter、XDP、TC)注入自定义逻辑,但网络栈本身的结构和行为不受影响。
阶段二:替代时代(2018-2021)。eBPF
开始替代网络栈的核心组件——kube-proxy 被 eBPF LB
替代,iptables 被 TC BPF 替代,veth 转发被
bpf_redirect_peer 加速,拥塞控制被 struct_ops
可编程化。
阶段三:平台时代(2022+)。eBPF 正在成为一个完整的”网络可编程平台”——BPF arena 提供灵活的内存模型,BPF token 支持安全的权限委托,bpf_iter 提供高效的状态遍历,SmartNIC offload 将 BPF 程序扩展到硬件。
这个趋势的终点是什么?也许是 Brendan Gregg 所说的:“eBPF 正在将 Linux 内核变成一个微内核——一个可编程的操作系统。” 在网络领域,这意味着数据面的每一个行为——从包的过滤、转发、封装,到拥塞控制、QoS 调度——都可以在运行时被安全地定义和修改。
但要实现这个愿景,eBPF 社区还需要解决几个关键挑战:
- 开发体验:verifier 错误信息需要更加友好,调试工具需要达到用户态开发的水平
- 可移植性:CO-RE 和 BTF 是正确的方向,但内核版本碎片化仍然是现实问题
- 安全性:verifier 的正确性需要形式化验证的支持,而非仅依赖人工审计和 fuzzing
- 标准化:eBPF Foundation 正在推动跨平台的 eBPF 规范(包括 Windows),但这条路还很长
十四、总结
| 年份 | 里程碑 | 网络能力提升 |
|---|---|---|
| 2014 | eBPF 诞生 (3.18) | 可编程 socket filter |
| 2016 | XDP (4.8) | 驱动层高性能数据面 |
| 2017 | TC BPF / LWT BPF | 协议栈 ingress/egress 可编程 |
| 2018 | AF_XDP / socket ops | 用户态零拷贝 + 传输层回调 |
| 2019 | bpf_redirect_peer | 容器 veth 加速 |
| 2020 | struct_ops | 可编程拥塞控制 |
| 2021 | BPF_LINK / redirect_neigh | 生命周期管理 + L3 加速 |
| 2022+ | arena / token / iter | 内存模型 + 权限委托 + 状态遍历 |
eBPF 用十年时间证明了一个理念:不需要修改内核源码,也能重塑网络栈的每一个层次。从 cBPF 的只读过滤器,到 XDP 的高性能数据面,到 struct_ops 的可编程传输层,再到 SmartNIC 的硬件加速——eBPF 正在将 Linux 网络栈从一个”写死的实现”变成一个”可编程的平台”。
对于 Kubernetes 网络工程师而言,理解 eBPF 的演化不仅是学术兴趣,更是实际需要:Cilium、Calico eBPF、Katran 等项目的每一次架构决策,都直接对应着 eBPF 能力的某一次扩展。掌握这条演化线索,你就能理解”为什么 Cilium 在 5.10 之后才能启用 host-routing”,或者”为什么 BPF 拥塞控制需要 5.6+ 内核”。
参考文献
- Toke Hoiland-Jorgensen et al., “The eXpress Data Path: Fast Programmable Packet Processing in the Operating System Kernel,” ACM CoNEXT 2018.
- Thomas Graf et al., “Cilium: Networking and Security for Containers with BPF and XDP,” 2020.
- Marco Spaziani Brunella et al., “hXDP: Efficient Software Packet Processing on FPGA NICs,” USENIX OSDI 2020.
- Yoann Ghigoff et al., “BMC: Accelerating Memcached using Safe In-kernel Caching and Pre-stack Processing,” USENIX NSDI 2021.
- Yang Zhou et al., “Electrode: Accelerating Distributed Protocols with eBPF,” USENIX NSDI 2023.
- Brendan Gregg, BPF Performance Tools, Addison-Wesley, 2019.
- Alexei Starovoitov, “BPF - in-kernel virtual machine,” netdev 0.1, 2015.
- Andrii Nakryiko, “BPF CO-RE Reference Guide,” libbpf documentation, 2020.
- Cilium Project, “BPF and XDP Reference Guide,” docs.cilium.io.
- Jesper Dangaard Brouer, “XDP - Scalable Networking with eBPF/XDP,” Linux Plumbers Conference 2018.
系列导航
- 上一篇:跨云 Kubernetes 网络互联
- 下一篇:Kubernetes 网络的未来
- 相关:eBPF 网络编程
- 相关:eBPF 与 netfilter 的对比
- 相关:Cilium 深度解析