在 DPDK 需要独占网卡的同时,eBPF 给出了另一种答案:不旁路内核,而是在内核里插入可编程的钩子。报文仍然走内核协议栈,但你能在最早的时刻(网卡驱动收到包的瞬间)执行自己的逻辑——丢弃、重定向、改写,全部由验证器保证安全。
本文从 eBPF 的定位讲起,梳理验证器与加载流程、XDP 与 tc 两大挂载点的语义差异、map 的类型与用法、可编程转发的实战写法与观测手段,最后落到性能特征与常见坑。
一、eBPF 与可编程数据面的定位
1.1 为什么是 eBPF
传统内核模块能改数据面,但风险极高:一行空指针就能 panic 整个内核。eBPF 用「受限指令集 + 验证器」把风险关进笼子——程序必须通过静态检查才能加载,运行时有边界检查与超时保护。
eBPF 相对内核模块的优势:
安全性:验证器静态检查,禁止越界与死循环
可热插拔:无需重启内核,attach/detach 即时生效
可观测:配合 map 与 perf ring buffer 导出数据
可移植:CO-RE 让同一字节码适配不同内核版本
代价:
指令集受限(无浮点、栈 512 字节)
不能直接调用任意内核函数(只能用 helper)
1.2 数据面挂载点全景
内核提供了多个 eBPF 挂载点,越靠近网卡越早、越省资源,但可用的上下文越少。选择挂载点,本质是在「性能」与「信息量」之间权衡。
| 挂载点 | 时机 | 上下文 | 典型用途 |
|---|---|---|---|
| XDP | 驱动收包后、skb 分配前 | 原始帧,无 skb | DDoS 清洗、负载均衡 |
| tc ingress | 进入协议栈前 | skb,可访问元数据 | 策略路由、NAT |
| tc egress | 发包前 | skb | 流量整形、限速 |
| socket filter | socket 层 | 部分报文 | 抓包(tcpdump) |
| cgroup/sock | socket 创建时 | socket 元数据 | 容器网络策略 |
二、eBPF 程序的加载与验证器
2.1 加载流程与字节码
eBPF 程序通常用 C 写(clang 编译成 BPF 字节码),再通过 bpf() 系统调用加载。加载时内核会做验证、JIT 编译,成功后返回一个 fd,attach 到挂载点后生效。
# 用 clang 把 C 编译成 eBPF 字节码
clang -O2 -g -target bpf -c xdp_drop.c -o xdp_drop.o
# 用 iproute2 加载并挂到网卡(XDP 模式)
ip link set dev eth0 xdp obj xdp_drop.o sec xdp
# 查看已加载的程序
bpftool prog show
# 卸载
ip link set dev eth0 xdp off
2.2 验证器的安全约束
验证器是 eBPF 的安全基石,它在加载前做数据流分析:所有内存访问必须有边界证明,所有循环必须可判定终止,栈使用不超过 512 字节。
验证器会拒绝的典型情况:
1. 越界访问:指针未做范围检查就解引用
2. 无限循环:循环上界无法静态确定(新内核支持有界循环)
3. 未初始化读取:读栈上未赋值的内存
4. 非法 helper 调用:在不允许的挂载点调用
5. 栈溢出:局部变量超过 512 字节
排错手段:
bpftool prog load ... type xdp # 看验证器报错行号
bpftool prog dump xlated id <id> # 看翻译后的指令
三、XDP:最早的数据面钩子
3.1 XDP 的处理时机与返回值
XDP 在网卡驱动收到包、尚未分配 skb 时执行,因此开销极低,但也拿不到 skb 元数据。程序的返回值决定报文命运,这是 XDP 最核心的语义。
| 返回值 | 含义 | 后续动作 |
|---|---|---|
| XDP_DROP | 丢弃 | 报文就地释放,不进协议栈 |
| XDP_PASS | 放行 | 继续走内核协议栈 |
| XDP_TX | 从同一网卡发回 | 立即反弹回去 |
| XDP_REDIRECT | 重定向到其他网卡/CPU | 走 AF_XDP 或另一网口 |
| XDP_ABORTED | 出错 | 丢弃并记录 trace |
/* XDP 程序骨架:丢弃所有 UDP 端口 53 之外的报文(示意) */
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
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;
if ((void *)(eth + 1) > data_end)
return XDP_PASS; /* 边界检查,验证器要求 */
if (eth->h_proto != __constant_htons(ETH_P_IP))
return XDP_PASS;
return XDP_PASS;
}
3.2 XDP 三种模式
XDP 可以运行在三种模式,性能与兼容性各不相同。生产环境优先用 native 或 offload,generic 仅作兼容兜底。
| 模式 | 执行位置 | 性能 | 兼容性 |
|---|---|---|---|
| native | 驱动内,收包早期 | 最高 | 需驱动支持 |
| offload | 网卡硬件 | 极高,几乎零 CPU | 仅少数智能网卡 |
| generic | 协议栈入口(skb 后) | 较低 | 所有网卡 |
# 指定 XDP 模式加载
ip link set dev eth0 xdpdrv obj prog.o sec xdp # native
ip link set dev eth0 xdpgeneric obj prog.o sec xdp # generic
ip link set dev eth0 xdpoffload obj prog.o sec xdp # offload
# 查看当前模式
ip link show dev eth0 | grep xdp
四、tc 与流量控制钩子
4.1 tc ingress/egress
tc(traffic control)的 cls_bpf 分类器支持在 ingress 与 egress 两个方向挂 eBPF 程序。相比 XDP,tc 拿到的已经是 skb,可以读写更多元数据(如 mark、优先级、VLAN),也支持更复杂的重定向与封装。
# 在 eth0 的 ingress 挂 eBPF 分类器
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj tc_prog.o sec ingress
# 在 egress 挂
tc filter add dev eth0 egress bpf da obj tc_prog.o sec egress
# 查看
tc filter show dev eth0 ingress
4.2 XDP 与 tc 的分工
XDP 与 tc 不是替代关系,而是不同层次的分工:XDP 做「最快的粗筛」,tc 做「更细的加工」。很多生产系统两者配合使用。
分层协作的典型架构:
XDP(最早):
- DDoS 清洗:识别并丢弃攻击流量
- 四层负载均衡:改写目的地址并 XDP_TX 转发
- 快速 ACL:按源 IP/端口黑名单丢弃
tc ingress/egress:
- NAT / 连接跟踪
- 封装(VXLAN、Geneve)
- 限速与流量整形
- 配合 kube-proxy 替代方案做 Service 转发
五、map 与核间数据共享
5.1 map 类型
map 是 eBPF 程序与用户态、以及不同程序之间共享数据的唯一通道。不同 map 类型针对不同访问模式优化,选错类型会显著影响性能。
| map 类型 | 结构 | 适用场景 |
|---|---|---|
| BPF_MAP_TYPE_HASH | 哈希表 | 通用键值查询(连接表) |
| BPF_MAP_TYPE_ARRAY | 数组 | 固定索引、最快查找 |
| BPF_MAP_TYPE_LRU_HASH | LRU 哈希 | 有上限的会话表 |
| BPF_MAP_TYPE_PERCPU_HASH | 每 CPU 分片 | 高频计数,免锁 |
| BPF_MAP_TYPE_RINGBUF | 环形缓冲 | 高效向用户态推事件 |
| BPF_MAP_TYPE_LPM_TRIE | 最长前缀匹配 | 路由表、CIDR 匹配 |
5.2 与用户态交互
用户态用 bpf() 系统调用或 libbpf 读写 map;ringbuf 提供了比 perf buffer 更高效的事件通道。控制面程序据此下发规则、读取统计。
/* 定义并查表(内核侧,示意) */
struct bpf_map_def SEC("maps") conn_map = {
.type = BPF_MAP_TYPE_LRU_HASH,
.key_size = sizeof(struct flow_key),
.value_size = sizeof(struct flow_stat),
.max_entries = 65536,
};
/* 查找;未命中则插入 */
struct flow_stat *s = bpf_map_lookup_elem(&conn_map, &key);
if (!s) {
struct flow_stat init = {0};
bpf_map_update_elem(&conn_map, &key, &init, BPF_NOEXIST);
}
# 用户态查看 map 内容
bpftool map show
bpftool map dump id 12
六、可编程转发实战
6.1 一个最小的四层转发程序
四层负载均衡的经典做法:XDP 程序查表得到后端地址,改写目的 IP/MAC 后用 XDP_TX 直接发回。整个转发不经过内核协议栈,单核可达百万级 QPS。
XDP 四层转发的处理步骤:
1. 解析以太头、IP 头、TCP/UDP 头(每步做边界检查)
2. 用五元组查 map,得到后端 VIP/后端地址
3. 改写目的 MAC、目的 IP、目的端口
4. 重算 IP 与传输层校验和
5. 交换源/目的,返回 XDP_TX 发回
注意:
- 校验和增量更新(bpf_csum_diff)比全量重算快
- 回程流量需要反向映射,通常在另一张 map
- 需处理分片与 ICMP 错误报文
6.2 用 bpftool 与 bpftrace 观测
调试 eBPF 数据面时,bpftool 看程序与 map,bpftrace 做动态追踪,perf 看 CPU 周期,三者结合能定位绝大多数问题。
# 查看 attach 情况
bpftool net show
# 统计 XDP 各返回值的计数(若程序写了统计 map)
bpftool map dump name xdp_stats
# 用 bpftrace 观察丢包点
bpftrace -e 'tracepoint:xdp:xdp_exception { @[args->act] = count(); }'
# 用 perf 看每包周期
perf stat -e cycles,instructions -a sleep 5
七、性能、卸载与常见坑
7.1 性能特征
eBPF 的性能优势来自「早」和「省」:XDP 在 skb 之前处理,避免了 skb 分配;JIT 编译后是原生指令;per-CPU map 免锁。但它的上限受限于「不能取代整个协议栈」。
性能参考(数量级,随硬件与程序复杂度变化):
XDP_DROP(最简单):单核 10+ Mpps
XDP 四层转发:单核 1~5 Mpps(含校验和与查表)
tc eBPF:单核 1~3 Mpps(已有 skb 开销)
对比:内核 iptables 转发 单核 0.3~1 Mpps
影响因素:
程序指令数(验证器上限 100 万条,但越短越快)
map 查找次数与类型
是否命中 per-CPU cache
是否触发 tail call 链
7.2 常见坑与对策
| 现象 | 原因 | 对策 |
|---|---|---|
| 加载失败 | 验证器拒绝(越界/循环) | 补边界检查,看 bpftool 报错 |
| 程序不生效 | attach 到错误网卡/方向 | ip link / tc filter show 核对 |
| 丢包但无日志 | 直接 XDP_DROP 无统计 | 加 per-CPU 计数 map |
| 性能不及预期 | map 选型不当 | 高频路径用 ARRAY/PERCPU |
| 内核升级后失效 | 依赖内核结构体偏移 | 用 CO-RE + vmlinux.h |
| 报文被截断 | 未处理分片或非线性区 | 用 bpf_xdp_adjust_head 等 helper |
八、总结
| 主题 | 核心知识点 | 落地建议 |
|---|---|---|
| 定位 | 内核内可编程钩子,非旁路 | 优先于内核模块,安全可热插拔 |
| 验证器 | 静态检查保证安全 | 每步指针都做边界检查 |
| XDP | 最早钩子,五类返回值 | 粗筛与四层转发首选 |
| tc | skb 级钩子,元数据丰富 | 精细加工与封装用 tc |
| map | 数据共享唯一通道 | 按访问模式选类型,热点用 PERCPU |
| 观测 | bpftool / bpftrace / perf | 先加统计 map 再调优 |
eBPF 的真正突破在于把内核数据面从「编译期固化」变成了「运行时可编程」:过去要改转发逻辑得写内核模块、重启内核,现在一段通过验证的字节码就能即时生效。它没有 DPDK 那样极致的性能上限,但保留了内核的管理能力与协议栈,工程上更可控。对后端工程师来说,XDP 与 tc 已经不只是网络团队的工具——Service Mesh 的数据面、可观测性采集、容器网络策略都在用它,理解挂载点与 map,就是理解云原生数据面的通用语言。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。