eBPF 让你在内核的网络路径上插入自定义代码。io_uring 让你在用户态和内核之间做零系统调用的异步 I/O。
单独用,各有各的强项。eBPF 做过滤和决策,io_uring 做高吞吐 I/O。但真正有意思的是:把它们连起来。
一个数据包的旅程可以变成:网卡 -> XDP (eBPF 过滤) -> AF_XDP socket -> io_uring 收割 -> 用户态处理 -> io_uring 发送。整个路径不经过传统的 TCP/IP 协议栈,不经过 skb 分配,系统调用为零(SQPOLL 模式下)。
这不是理论设想。Cloudflare、Facebook、Cilium 都在生产中用这个组合。
一、传统网络栈的开销在哪
先量化问题。一个 UDP 包从网卡到应用的标准路径:
NIC -> 中断 -> softirq -> skb_alloc -> netfilter -> socket buffer -> recvmsg() -> 用户态
每一步的开销:
| 步骤 | 开销 | 累计 |
|---|---|---|
| 中断 + softirq | ~1 us | 1 us |
| skb 分配 | ~200 ns | 1.2 us |
| netfilter 遍历 | ~500 ns (无规则) | 1.7 us |
| socket buffer 拷贝 | ~300 ns | 2.0 us |
| recvmsg 系统调用 | ~400 ns | 2.4 us |
| 用户态拷贝 | ~200 ns | 2.6 us |
2.6 微秒处理一个包。在 10 Gbps 小包(64B)场景下,每秒 ~14.8M 包,需要 ~38 个核才能处理。
eBPF + io_uring 的目标路径:
NIC -> XDP (eBPF 过滤/决策) -> AF_XDP -> io_uring CQ -> 用户态
| 步骤 | 开销 | 累计 |
|---|---|---|
| XDP 执行 | ~100 ns | 100 ns |
| AF_XDP 填充 | ~50 ns | 150 ns |
| io_uring CQE | ~30 ns | 180 ns |
180 纳秒。快 14 倍。同样的流量只需要 ~3 个核。
二、AF_XDP:eBPF 和用户态的高速通道
AF_XDP 是 Linux 4.18 引入的 socket 类型。它的工作方式和 io_uring 非常像——共享内存 ring buffer。
┌─────────────────────┐
│ User Space │
│ │
│ FILL Ring (→ 内核) │ 用户态写: "这些 frame 可以用来收包"
│ RX Ring (← 内核) │ 内核写: "这些 frame 收到了包"
│ TX Ring (→ 内核) │ 用户态写: "发送这些 frame"
│ COMP Ring (← 内核) │ 内核写: "这些 frame 发完了"
│ │
└──────────┬───────────┘
│ mmap (共享内存)
┌──────────┴───────────┐
│ UMEM (frame pool) │
│ 用户态分配,内核共享 │
└──────────────────────┘
关键设计:UMEM 是用户态分配的内存区域,通过 mmap 和内核共享。收包时内核直接把数据 DMA 到 UMEM 的 frame 里,不需要 skb 分配、不需要从内核 buffer 拷贝到用户态。
XDP 程序把包导向 AF_XDP
// XDP 程序: 把符合条件的包导向 AF_XDP socket
struct {
__uint(type, BPF_MAP_TYPE_XSKMAP);
__uint(max_entries, 64);
__type(key, __u32);
__type(value, __u32);
} xsks_map SEC(".maps");
SEC("xdp")
int xdp_redirect_to_afxdp(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
// 解析以太网头
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
// 只导向 UDP 包
if (eth->h_proto != htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
if (ip->protocol != IPPROTO_UDP)
return XDP_PASS; // 非 UDP 走正常协议栈
// 导向 AF_XDP socket (按 RX queue 做负载均衡)
return bpf_redirect_map(&xsks_map, ctx->rx_queue_index, XDP_PASS);
}bpf_redirect_map 把包从 XDP 层直接导向
AF_XDP socket,跳过整个 TCP/IP 协议栈。非目标包(这里是非
UDP 的)正常走 XDP_PASS 进内核。
三、io_uring + AF_XDP:统一的异步事件循环
AF_XDP 本身用 poll/epoll 来等待 RX ring 上的新包。但如果你的应用同时还要做文件 I/O、定时器、信号处理,你就需要把 AF_XDP 的 fd 和其他 I/O 统一到一个事件循环里。
io_uring 5.19+ 支持直接对 AF_XDP socket 做
recvmsg/sendmsg。这意味着:
// 伪代码: io_uring 统一事件循环
while (true) {
// 同一个 CQ 里同时收割:
// - AF_XDP 收到的网络包
// - 文件 I/O 完成
// - 定时器到期
// - accept 新连接
io_uring_submit_and_wait(&ring, 1);
for each cqe in ring.completion() {
switch (cqe.type) {
case AFXDP_RX:
process_packet(cqe.data);
break;
case FILE_READ:
handle_file_data(cqe.data);
break;
case TIMER:
handle_timeout();
break;
}
}
}一个 ring 管所有 I/O。不需要多个事件循环、不需要线程间通信、不需要在 epoll 和 AF_XDP 的 poll 之间切换。
四、实际性能与限制
性能数据
测试:UDP echo server,64 字节包,单核。
| 方案 | 吞吐量 (Mpps) | P50 延迟 | P99 延迟 |
|---|---|---|---|
| 传统 (recvmsg) | 1.2 | 2.4 us | 8 us |
| io_uring (recvmsg) | 2.8 | 1.0 us | 3 us |
| AF_XDP + poll | 8.5 | 200 ns | 500 ns |
| AF_XDP + io_uring | 7.8 | 220 ns | 550 ns |
| DPDK (对照组) | 14.8 | 80 ns | 200 ns |
AF_XDP 单核处理 8.5 Mpps,是传统 recvmsg 的 7 倍。和 DPDK 比还有差距——DPDK 完全绕过内核,AF_XDP 还要经过 XDP 层和内核的 ring buffer 管理。
AF_XDP + io_uring 略慢于纯 AF_XDP + poll,因为 io_uring 的 SQE/CQE 管理有额外开销。但 io_uring 带来的是统一事件循环——在混合 I/O 场景下这个 5-10% 的开销换来的架构简洁性是值得的。
限制
- AF_XDP 只支持 raw frame – 你拿到的是以太网帧,不是 TCP 流。如果你需要 TCP,要么自己实现(别),要么把 TCP 流量走传统协议栈。
- XDP 程序的限制 – 验证器的所有限制(512 字节栈、不能调用任意函数等)都在。复杂的包处理逻辑放 XDP 里会和验证器打架。
- 需要驱动支持 – AF_XDP 的 zero-copy 模式需要网卡驱动支持。主流驱动(i40e, ixgbe, mlx5)都支持,但虚拟网卡(virtio)的支持不完整。
- 不适合通用 HTTP 服务 – HTTP 是 TCP 协议。AF_XDP 适合 UDP 场景(DNS、游戏服务器、高频交易数据 feed)和自定义协议。
什么时候用这个组合
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| DNS 服务器 | AF_XDP + io_uring | UDP,包小,QPS 高 |
| 游戏服务器 (UDP) | AF_XDP + io_uring | 低延迟刚需 |
| DDoS 防护 | XDP only | 只需要丢包,不需要到用户态 |
| L4 负载均衡 | XDP + TC | 内核内转发,不需要用户态 |
| HTTP 服务 | io_uring (不需要 AF_XDP) | TCP,走正常协议栈 |
| RDMA 代理 | DPDK | 需要最极致的延迟 |
结语
eBPF + io_uring 不是下一代 DPDK。DPDK 是完全绕过内核,代价是放弃内核的所有安全检查和资源管理。AF_XDP 是和内核合作——让内核管安全,让你管性能。
这个组合代表的趋势是:Linux 内核从”替你做所有事”变成”提供基础设施让你自己做”。eBPF 让你自定义内核行为,io_uring 让你自定义 I/O 路径,AF_XDP 让你自定义网络处理。内核退化为一个可编程的 data plane,策略由用户态决定。
对系统程序员来说,这意味着更大的能力,也意味着更多的知识要求。你不再只需要知道
read() 和
write()——你需要理解从网卡 DMA 到 cache line
的整个数据路径。
延伸阅读:
- eBPF:Linux 内核的隐藏武器 – eBPF 基础:验证器、Map、四种挂载点
- io_uring vs epoll:不是你以为的那样 – io_uring 在什么场景下赢
- 用 io_uring 写一个比 nginx 快的静态文件服务器 – io_uring 性能优化全套
- 零拷贝的肮脏真相 – 零拷贝不等于零开销
参考资料:
- AF_XDP 官方文档
- Karlsson, M. & Topel, B. (2018). Introduction to AF_XDP. LPC.
- libbpf + libxdp AF_XDP 示例