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

eBPF 网络子系统的演进:从 hook 到可编程网络操作系统

目录

本文基于 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 是怎么一步步走到今天的”,以及”它将走向何方”。

eBPF 网络子系统演化时间线

一、从 cBPF 到 eBPF:一个过滤器的进化

经典 BPF:只读的包过滤器

1992 年,Steven McCanne 和 Van Jacobson 在 USENIX 会议上发表了 The BSD Packet Filter 论文。经典 BPF(cBPF)的设计目标很简单:让 tcpdump 这样的工具能在内核态高效过滤网络包,而不需要把每个包都拷贝到用户态再判断。

cBPF 的架构极为简洁:

/* 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 的性能优势来自三个设计决策:

  1. 避免 sk_buff 分配sk_buff 是一个包含 200+ 字段的复杂结构体,每次分配和初始化都有显著开销。XDP 直接操作 xdp_buff(只包含 data 指针和长度),极其轻量。

  2. 批量处理:XDP 在 NAPI poll 循环中运行,天然支持批量处理。一次 NAPI poll 可以处理 64 个包,中间没有任何中断或上下文切换。

  3. 无锁路径:XDP 程序运行在 softirq 上下文中,每个 CPU 有独立的处理路径,不需要任何锁。

# 用 xdp-tools 加载一个简单的 XDP 程序
xdp-loader load eth0 xdp_drop.o

# 查看 XDP 程序状态
xdp-loader status eth0

# 用 xdp-bench 测试转发性能
xdp-bench redirect eth0 eth1

Facebook 在 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.1

Cilium 在早期版本中曾使用 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    │
    └─────────┘  └─────────┘

工作流程:

  1. 用户态通过 FILL ring 提交空闲 UMEM chunk 的地址给内核
  2. XDP 程序通过 XDP_REDIRECT 将包重定向到 AF_XDP socket
  3. 内核把包数据写入 UMEM chunk,通过 RX ring 通知用户态
  4. 用户态处理完后,通过 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_SOCKHASHbpf_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 避免了以下开销:

  1. 跳过 backlog 队列:不经过 netif_rx,不需要 softirq 重新调度
  2. 跳过对端 ingress 处理:直接进入对端的 TC ingress 点
  3. 保持 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_cc

struct_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 的价值在于:

七、2021-2022:基础设施成熟

BPF_LINK:程序生命周期管理

在早期,BPF 程序通过 pin 到 bpffs(/sys/fs/bpf/)来保持存活。这种机制有几个问题:

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_cc

BPF_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_BPFCAP_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 正在改变这一局面:

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 的结合有两条路径:

  1. XDP offload:将 XDP 程序直接卸载到 SmartNIC 的 FPGA 或专用处理器上执行。Netronome(现已被 Corigine 收购)是第一个支持 XDP offload 的厂商。

  2. 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 中的价值:

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 程序的调试体验远不如用户态程序:

# 常用的调试手段
# 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 选择了”在驱动中执行 BPF”而非”在用户态执行”,这保证了与内核协议栈的兼容性,但牺牲了一些极端场景下的性能(相比 DPDK 的 kernel bypass)。

Cilium: Networking and Security for Containers (2020)

Thomas Graf 等人发表的这篇论文详细描述了 Cilium 如何利用 eBPF 构建一个完整的容器网络和安全平台:

这篇论文是理解”eBPF 如何在生产环境中替代传统网络栈”的必读文献。相关内容在 Cilium 深度解析 中有进一步讨论。

hXDP: Efficient Software Packet Processing on FPGA NICs (OSDI 2020)

Marco Spaziani Brunella 等人提出了一种在 FPGA SmartNIC 上高效执行 XDP 程序的方法。核心贡献:

hXDP 的意义在于证明了 eBPF 不仅是一个”软件虚拟机”,它的程序模型可以高效地映射到硬件加速器上。

BMC: Accelerating Memcached using Safe In-kernel Caching and Pre-stack Processing (NSDI 2021)

Yoann Ghigoff 等人的 BMC 论文展示了 eBPF 在应用层加速中的潜力:

BMC 是”eBPF 不只是网络工具”的最佳证明——它模糊了”网络处理”和”应用逻辑”的边界。

Electrode: Accelerating Distributed Protocols with eBPF (NSDI 2023)

Yang Zhou 等人的 Electrode 论文将 eBPF 应用于分布式共识协议的加速:

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 模式的特点:

# 启用 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 社区还需要解决几个关键挑战:

  1. 开发体验:verifier 错误信息需要更加友好,调试工具需要达到用户态开发的水平
  2. 可移植性:CO-RE 和 BTF 是正确的方向,但内核版本碎片化仍然是现实问题
  3. 安全性:verifier 的正确性需要形式化验证的支持,而非仅依赖人工审计和 fuzzing
  4. 标准化: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+ 内核”。

参考文献

  1. Toke Hoiland-Jorgensen et al., “The eXpress Data Path: Fast Programmable Packet Processing in the Operating System Kernel,” ACM CoNEXT 2018.
  2. Thomas Graf et al., “Cilium: Networking and Security for Containers with BPF and XDP,” 2020.
  3. Marco Spaziani Brunella et al., “hXDP: Efficient Software Packet Processing on FPGA NICs,” USENIX OSDI 2020.
  4. Yoann Ghigoff et al., “BMC: Accelerating Memcached using Safe In-kernel Caching and Pre-stack Processing,” USENIX NSDI 2021.
  5. Yang Zhou et al., “Electrode: Accelerating Distributed Protocols with eBPF,” USENIX NSDI 2023.
  6. Brendan Gregg, BPF Performance Tools, Addison-Wesley, 2019.
  7. Alexei Starovoitov, “BPF - in-kernel virtual machine,” netdev 0.1, 2015.
  8. Andrii Nakryiko, “BPF CO-RE Reference Guide,” libbpf documentation, 2020.
  9. Cilium Project, “BPF and XDP Reference Guide,” docs.cilium.io.
  10. Jesper Dangaard Brouer, “XDP - Scalable Networking with eBPF/XDP,” Linux Plumbers Conference 2018.

系列导航


By .