首页/技术文章
ARTICLE

XDP 快路径为什么比内核转发快

它不是「更快的 iptables」——省掉的是整条路径,而不只是某个函数。

技术 · 数据面

XDP 快路径为什么比内核转发快

2026-10-18 · 约 1800 字 · 阅读 6 分钟

很多人把 XDP 理解成「更快的 iptables」,这个理解会误导设计。XDP 快的原因是它在协议栈之前就把包处理完了,省掉的是整条路径,而不仅是某个函数。

一、内核转发一个包要经过什么

网卡收到包后,走的是:DMA 到内存 → 硬中断 → 软中断(NAPI poll)→ 分配 sk_buff → 进入协议栈 → 路由查找 → netfilter 各 hook → 邻居子系统 → 重新封装 → 排队 → 驱动发送。每一步都有开销,其中最贵的三项是:

  • sk_buff 的分配与释放:每个包一次,高包量下分配器压力很大
  • 多次数据结构穿越:路由、连接跟踪、netfilter 各有自己的表与锁
  • 上下文切换与软中断:多核之间要分发,缓存局部性变差

二、XDP 省掉了什么

XDP 程序挂在驱动的收包路径上,拿到的是原始帧的线性缓冲区,还没有 sk_buff。它可以直接读包头、改包头、决定这个包的去向:

  • XDP_PASS:交给内核协议栈,走原来的路
  • XDP_DROP:就地丢弃,连 sk_buff 都不分配
  • XDP_TX:从同一网卡发回去
  • XDP_REDIRECT:转发到另一张网卡或另一个 CPU

对「转发 + NAT + 限速」这类明确的数据面工作,绝大多数包根本不需要进协议栈,于是 sk_buff、路由查找、netfilter 全部省掉。

SEC("xdp")
int xdp_forward(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;
    if (eth->h_proto != htons(ETH_P_IP)) return XDP_PASS;

    /* 查表:命中则改写并重定向,未命中交给内核 */
    struct fwd_key key = { .daddr = ... };
    struct fwd_val *val = bpf_map_lookup_elem(&fwd_table, &key);
    if (!val) return XDP_PASS;

    return bpf_redirect_map(&tx_ports, val->port, 0);
}

三、为什么不是所有场景都该上 XDP

XDP 有两个硬约束:

  • 驱动支持:只有 native 模式才是真正在驱动层跑。generic 模式(SKB 模式)要走一遍 sk_buff,性能收益大幅缩水,很多「XDP 没效果」的结论都来自这里
  • 能力边界:分片包、复杂连接跟踪、需要完整协议栈语义的功能,在 XDP 里要么做不了,要么要写很多辅助逻辑

我们的做法是分层:明确可判定的数据面动作(转发、NAT、限速、简单过滤)进 XDP;需要状态机与完整语义的(认证、审计、复杂策略)留在控制面,两边通过 BPF map 交换状态。

四、怎么判断「真的变快了」

不要只看一个数字。至少要有三组对照:

  • 同一台机器,XDP 开 / 关,同样包长与包量下的吞吐与 CPU 占用
  • 不同包长(小包看 PPS,大包看带宽)分别测
  • 多队列与单队列的差异,确认是否真的吃满了多核

我们交付时会把这些实测数据写进验收报告,而不是在页面上写一个漂亮的总量数字——因为没有基线的数字没有意义。

一句话总结:XDP 快不是因为「优化了代码」,而是因为它不在那条路上。