一、为什么需要 eBPF/XDP

在带宽发展到 10Gbps、甚至 100Gbps 的今天,Linux 内核网络栈的标准处理路径成为了一个严重的瓶颈:一个千兆网卡每秒需要处理千万个数据包,标准内核处理路径长达数十个函数调用,占用大量 CPU 周期,在巨大的流量下客观中断了内核网络栈。

eBPF (Extended Berkeley Packet Filter) 以及在网卡层发射的 XDP (eXpress Data Path),几乎抹除了这个瓶颈——让用户在网卡驱动层插入自定义处理逻辑,与网卡驱动同步接收数据包,此时内核尚未创建 sk_buff,CPU 未分配一个字节内存,已经将"轻量的判断"完成了。

本文的框架:

  • BPF 的诞生 - 从 Van Jacobson 的经典论文到内核虚拟机
  • eBPF 的寄存器架构 - 10 个 64 位寄存器与调用约定
  • BPF 验证器 - 内核安全的关键保障
  • BPF 映射 (Maps) - 用户空间和内核空间的互通桥梁
  • XDP 的返回动作 - XDP_DROP / PASS / FORWARD / REDIRECT
  • "Hello World" 实战 - 编写第一个 XDP 程序
  • 实战演练:构建 DDoS 防护防火墙 - 基于 LPM_TRIE 的黑名单过滤
  • 性能分析与调优 - 单核 24Mpps 的秘诀
  • 现实世界案例 - Cloudflare、Facebook、AWS 的生产实践
  • 总结与展望 - CO-RE 与 BTF 的未来

二、历史:从 BPF 到 eBPF

BPF 的故事要追溯到 1992 年,Steven McCanne 和 Van Jacobson 在加州大学伯克利分校创立了——为了改善 tcpdump 的性能,他们在内核内运行了一个过滤器。该机器只有 32 位寄存器,仅支持 256 个机器指令,用一个"一次一个一次一个"的古老名字称呼。

2014 年,Alexei Starovoitov 对内核进行了彻底的改造,引入了新的寄存器构造(10 个 64 位寄存器,其中 R0 保存返回值),从此 eBPF 开始了蓬勃的生命力。

2016 年,XDP 被引入高速网络路径,能够在网卡驱动层面直接处理数据包,绕过整个内核协议栈。

三、eBPF 程序结构

eBPF 程序本质是一段 64 位机器码,内核通过验证器确保其安全性,然后通过 JIT 编译到本机机器码。一个典型的 eBPF 程序包含:

// 映射定义区
struct bpf_map_def SEC("maps") pkt_count = {
    .type        = BPF_MAP_TYPE_PERCPU_ARRAY,
    .key_size    = sizeof(u32),
    .value_size  = sizeof(u64),
    .max_entries = 1,
};

SEC("xdp")
int xdp_counter(struct xdp_md *ctx) {
    u32 key = 0;
    u64 *counter = bpf_map_lookup_elem(&pkt_count, &key);
    if (counter) __sync_fetch_and_add(counter, 1);
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

关键组件:

  • 寄存器:R0-R9(保存中间结果)和 R10(唯壹的帧指针/栈指针)
  • 调用约束:程序必须终至,不能无限循环,所有访问都需要边界检查
  • 内存访问:仅通过 bpf_map_lookup_elem 和 bpf_probe_read 安全访问
  • 辅助函数:bpf_map_update_elem、bpf_perf_event_output 等

四、BPF 验证器:安全的关键

BPF 验证器是 eBPF 安全模型的核心,它会在加载时对程序进行静态分析:

  • 禁止无限循环:跟踪所有可能的执行路径,确保程序必定终止(循环次数上限通常为 100 万条指令后限制)
  • 边界检查:所有内存访问都必须经过显式检查,确保不越界(如 data + header_size <= data_end)
  • 寄存器状态跟踪:区分已初始化的寄存器和未使用的寄存器,防止信息泄露
  • 栈深度限制:eBPF 栈大小固定为 512 字节,递归调用会被拒绝
  • 有界执行:总指令数最大值 100 万条,确保可在合理时间内验证完毕

五、BPF 映射 (Maps):用户空间和内核的桥梁

BPF 映射是 eBPF 程序与用户空间以及 eBPF 程序之间传输数据的主要方式:

映射类型用途
BPF_MAP_TYPE_HASH高速键值查找,O(1) 复杂度
BPF_MAP_TYPE_ARRAY索引数组,速度最快
BPF_MAP_TYPE_PERF_EVENT_ARRAY向用户空间发送事件数据(perf ring buffer)
BPF_MAP_TYPE_LPM_TRIE最长前缀匹配,适合路由表和 IP 黑名单
BPF_MAP_TYPE_DEVMAP网卡设备映射,配合 XDP_REDIRECT 转发到其他网卡
BPF_MAP_TYPE_CPUMAP将数据包分配到不同 CPU

六、XDP:最快的包处理路径

XDP 是 eBPF 在网卡驱动层的入口点,在数据包 DMA 内存区生成之前就运行用户定义的 eBPF 程序。XDP 程序返回值决定数据包的命运:

// XDP 包的 5 种命运
XDP_ABORTED    : 异常,丢弃数据包,触发 tracepoint
XDP_DROP       : 在驱动层直接丢弃,不过内核(最高性能)
XDP_PASS       : 交给内核网络栈继续处理
XDP_TX         : 从接收到它的同一网卡发出
XDP_REDIRECT   : 转发到另一网卡(或 cpumap)

七、实战:编写 Hello XDP 程序

我们从最简单的 eBPF 程序开始——有一个唯一的功能:计数包并传递:

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

struct bpf_map_def SEC("maps") pkt_count = {
    .type        = BPF_MAP_TYPE_PERCPU_ARRAY,
    .key_size    = sizeof(u32),
    .value_size  = sizeof(u64),
    .max_entries = 1,
};

SEC("xdp")
int xdp_hello(struct xdp_md *ctx) {
    u32 key = 0;
    u64 *pkts;
    pkts = bpf_map_lookup_elem(&pkt_count, &key);
    if (pkts)
        __sync_fetch_and_add(pkts, 1);
    return XDP_PASS; // 默认将包交给内核处理
}

char _license[] SEC("license") = "GPL";

编译和加载:

$ clang -O2 -target bpf -c xdp_hello.c -o xdp_hello.o
$ ip link set dev eth0 xdp obj xdp_hello.o sec xdp   # 加载
$ ip link show dev eth0                              # 查看加载状态
$ ip link set dev eth0 xdp off                        # 卸载

八、实战:构建 DDoS 防护防火墙

用 XDP 实现一个生产级的 DDoS 防护系统,核心思想是用 ip-prefix 查询黑名单,快速过滤恶意流量:

8.1 定义 LPM 黑名单映射

// 定义最长前缀匹配的 IP 黑名单
struct bpf_map_def SEC("maps") blocklist = {
    .type        = BPF_MAP_TYPE_LPM_TRIE,
    .key_size    = sizeof(struct lpm_trie_key) + 4,  // +4 字节为 IPv4 地址
    .value_size  = sizeof(u32),
    .max_entries = 10000,
    .map_flags   = BPF_F_NO_PREALLOC,
};

8.2 包解析逻辑

struct parsed_pkt {
    __u8  proto;
    __u16 len;
    __u32 saddr;
    __u32 daddr;
};

static __always_inline int parse_ipv4(void *data, __u32 offset,
                                       struct parsed_pkt *pkt) {
    struct iphdr *iph = data + offset;
    if ((void *)(iph + 1) > data_end)
        return -1;  // 边界检查,验证器要求
    pkt->proto = iph->protocol;
    pkt->saddr = iph->saddr;
    pkt->daddr = iph->daddr;
    pkt->len   = ntohs(iph->tot_len);
    return 0;
}

8.3 主处理函数

SEC("xdp")
int xdp_ddos_filter(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;
    void *data     = (void *)(long)ctx->data;
    struct ethhdr *eth = data;
    struct parsed_pkt pkt = {0};
    struct lpm_trie_key key = {0};
    __u32 *value;

    // 裁取以太网,仅支持 IPv4
    if ((void *)(eth + 1) > data_end) return XDP_PASS;
    if (eth->h_proto != bpf_htons(ETH_P_IP)) return XDP_PASS;

    if (parse_ipv4(data, sizeof(struct ethhdr), &pkt) < 0)
        return XDP_ABORTED;

    // 查询黑名单 LPM 表
    key.prefixlen = 32;  // 全 32 位精确匹配
    key.data[0] = pkt.saddr & 0xFF;
    key.data[1] = (pkt.saddr >> 8) & 0xFF;
    key.data[2] = (pkt.saddr >> 16) & 0xFF;
    key.data[3] = (pkt.saddr >> 24) & 0xFF;

    value = bpf_map_lookup_elem(&blocklist, &key);
    if (value) return XDP_DROP;  // 在黑名单中,直接丢弃

    return XDP_PASS;  // 不在黑名单,放行
}

8.4 用户空间管理工具

# 添加 192.168.1.100 到黑名单
$ bpftool map update id <map_id> key 192 168 1 100 value 1

# 插入 CIDR 网段到黑名单(/24)
$ bpftool map update id <map_id> key 10 0 0 0 prefixlen 24 value 1

# 查看黑名单内容
$ bpftool map dump id <map_id>

# 查看 XDP 统计信息
$ ip -s link show dev eth0

九、性能优化与诊断

性能对比:XDP vs 传统内核处理 vs DPDK

方案1GE (Mpps)10GE (Mpps)
传统内核~6 Mpps~2-3 Mpps
XDP (单核)~24 Mpps~12-18 Mpps
DPDK~100 Mpps~80-100 Mpps

关键优化技巧:

  • PERCPU_ARRAY:使用 per-CPU 数组映射,消除 CPU 竞争
  • 减少函数调用:用 static always_inline 强制内联
  • LPM_TRIE 前缀匹配 O(k):k 为地址位数(IPv4 为 32),适合有大量 CIDR 规则的场合
  • 尽早退出:在做任何昂贵的检查前,先做 cheapest 的判断
  • 批量更新:使用 BPF_MAP_TYPE_HASH 的批量操作减少系统调用次数

诊断工具链:

$ bpftool prog show              # 列出加载的 eBPF 程序
$ bpftool prog dump xlated id N  # 查看 JIT 后的汇编
$ bpftool map dump id N          # 查看映射内容
$ bpftrace -e 'tracepoint:xdp:* { @[probe] = count(); }'  # 实时监控

十、现实世界部署案例

10.1 Cloudflare:XDP 驱动的 DDoS 防护

Cloudflare 利用 XDP 构建了业界最强的 DDoS 防护系统,在 30 秒内将防护策略推送到全球网络。其核心架构:

  • 使用 XDP_DROP 在驱动层直接丢弃攻击包
  • 每分钟处理超过 1000 万个攻击数据包的过滤规则
  • 在大规模真实攻击(超过 30 Tbps)中依然保持稳定
  • BGP Flowspec + XDP 动态规则联动

10.2 Facebook/Meta:Katran L4 负载均衡器

Meta 开源的 Katran 使用 XDP 实现了第 4 层负载均衡,核心技术点:

  • XDP_REDIRECT 实现零拷贝转发(L4LB 不修改数据包)
  • IP-in-IP 封装 + XDP_DEVMAP 转发到其他主机
  • ICP(内联连接配对)避免连接跟踪表
  • 单核心可处理超过 1800 万包/秒

10.3 AWS:ENA 驱动 + Nitro 卡

AWS 的 EKS 使用 Nitro 专用卡处理网络虚拟化,将 25-40 Gbps 卸载到硬件:

  • Nitro 卡运行 XDP 进行内网流量过滤
  • eBPF 用于安全组的快速路径匹配
  • 无需内核改造即可添加新的过滤策略

10.4 Google:GRO 优化与 GKE

Google 的强大之处在于:

  • Android 10+ 默认加载 cgroup/BPF socket 程序
  • BPF (eBPF) 在内核 4.9 引入,允许用户定义的网络功能
  • Google 使用 eBPF 优化 GKE 的 CNI 网络插件

十一、进阶:BPF 调用栈与跟踪

XDP 只是 eBPF 的一个挂载点,eBPF 的强大之处在于它的多样性:

挂载类型挂载点用途
XDP网卡驱动层包过滤/转发
TC (Traffic Control)内核 Qdisc 层流量整形/QoS
Socket / cgroupsocket 操作连接控制/调度
kprobe / kretprobe内核函数进入/退出内核行为跟踪
uprobe / uretprobe用户函数进入/退出应用性能分析
tracepoint静态跟踪点稳定的性能监控接口
LSM安全钩子强制访问控制

十二、性能与调试技巧

通过 bpf_perf_event_output 记录统计:

// 额外定义:投递记录映射
struct bpf_map_def SEC("maps") nh_events = {
    .type        = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
    .key_size    = sizeof(u32),
    .value_size  = sizeof(u32),
    .max_entries = MAX_CPUS,
};

// 在 XDP 程序中发送事件
bpf_perf_event_output(ctx, &nh_events, BPF_F_CURRENT_CPU, &pkt, sizeof(pkt));

trace_pipe 输出示例:

irqworker 5-6667 [005] d..2: xdp_ddos_filter():
    src=192.168.1.100 dst=10.0.0.1 proto=6(TCP) action=DROP
    duration 47ns

十三、总结:eBPF 的未来

eBPF 是当前最备受关注的技术之一,其应用涵盖:

  • 网络:XDP/TC 双剑合璧,网卡驱动(mlx5、i40e、ionic)持续推动支持
  • 观测:kprobes/uprobes/tracepoints,同时 BTF (BPF Type Format) 推动可移植性
  • 安全:LSM eBPF,加上 LANDLOCK 将成为容器安全的重要工具
  • 跨平台:CO-RE(Compile Once - Run Everywhere),用 BTF 实现二进制兼容的 eBPF 程序
  • 可热可编程:a/b BPF 程序不重启内核即可动态更新

学习资源:

写在最后

eBPF 是一个战场,而不是一个软件。在演进的未来里,除了传统 BPF,内核的 BPF 编程模型将被不断扩展,CO-RE + BTF 将使 eBPF 程序更容易跨平台部署。掌握 eBPF 意味着掌握内核可编程的能力,这是云原生时代最重要的底层技能之一。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部