土法炼钢兴趣小组的算法知识备份

eBPF + io_uring:Linux 高性能网络栈的终极形态

目录

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% 的开销换来的架构简洁性是值得的。

限制

  1. AF_XDP 只支持 raw frame – 你拿到的是以太网帧,不是 TCP 流。如果你需要 TCP,要么自己实现(别),要么把 TCP 流量走传统协议栈。
  2. XDP 程序的限制 – 验证器的所有限制(512 字节栈、不能调用任意函数等)都在。复杂的包处理逻辑放 XDP 里会和验证器打架。
  3. 需要驱动支持 – AF_XDP 的 zero-copy 模式需要网卡驱动支持。主流驱动(i40e, ixgbe, mlx5)都支持,但虚拟网卡(virtio)的支持不完整。
  4. 不适合通用 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 的整个数据路径。


延伸阅读:

参考资料:

  1. AF_XDP 官方文档
  2. Karlsson, M. & Topel, B. (2018). Introduction to AF_XDP. LPC.
  3. libbpf + libxdp AF_XDP 示例

By .