eBPF 网络调优:用 tc-bpf 在 Linux 内核层实现智能流量整形
eBPF 网络调优:用 tc-bpf 在 Linux 内核层实现智能流量整形
一、传统 QoS 的局限性:tc 规则在万兆网络中的性能瓶颈
Linux 的 Traffic Control(tc)子系统是网络流量管理的标准工具,但它的分类器(classifier)和过滤器(filter)在内核中采用线性匹配算法。当规则数量超过 1000 条时,每包的处理时间从微秒级飙升至百微秒级,成为万兆网卡线速转发的性能瓶颈。
eBPF 为这个问题提供了全新的解决方案:将分类逻辑从内核的线性匹配替换为 eBPF 程序中的哈希映射 O(1) 查找,在处理数万条规则时仍保持恒定的包处理延迟。这就好比把羽毛球场上的步伐从"逐个试探"优化为"预判直取"——不再线性遍历所有可能的回球路线,而是通过经验和模式直接定位最可能的落点。
二、tc-bpf 的架构与数据流
flowchart LR subgraph Userspace [用户态] A[tc 命令<br/>加载 BPF 程序] --> B[BPF Map<br/>流量策略配置] end subgraph Kernel [内核态] C[网卡驱动<br/>NAPI 收包] --> D[tc ingress hook<br/>入站分类点] D --> E[eBPF Classifier<br/>O(1) 哈希分类] E --> F{流量分类结果} F -->|高优先级| G[classid 1:10<br/>低延迟队列] F -->|普通| H[classid 1:20<br/>默认队列] F -->|低优先级| I[classid 1:30<br/>后台传输] G --> J[内核协议栈] H --> J I --> J end style E fill:#e67e22,color:#fff style D fill:#9b59b6,color:#fffeBPF 程序挂载在tc子系统的clsacthook 点上,在数据包进入内核协议栈之前(ingress)或离开网卡之前(egress)执行。程序通过 BPF Map 与用户态通信——用户态可以动态更新分类规则,无需重新加载 eBPF 程序。
三、完整实现:智能 HTTP 流量分类器
// tc_classifier.bpf.c —— 基于 eBPF 的 HTTP 流量分类器 #include <linux/bpf.h> #include <linux/pkt_cls.h> #include <linux/if_ether.h> #include <linux/ip.h> #include <linux/tcp.h> #include <bpf/bpf_helpers.h> #include <bpf/bpf_endian.h> // 流量优先级定义 #define PRIO_HIGH 10 // 实时 API 请求 #define PRIO_NORMAL 20 // 普通 HTTP 流量 #define PRIO_LOW 30 // 批量/后台传输 // BPF Map: 高优先级目标端口(如 API 端口) // key=端口号, value=优先级 struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 1024); __type(key, __u16); __type(value, __u8); } prio_port_map SEC(".maps"); // BPF Map: 流量统计(按优先级计数) struct { __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); __uint(max_entries, 4); __type(key, __u32); // 优先级索引 __type(value, __u64); // 包计数 } traffic_stats SEC(".maps"); SEC("tc_cls") int http_traffic_classifier(struct __sk_buff *skb) { void *data_end = (void *)(long)skb->data_end; void *data = (void *)(long)skb->data; struct ethhdr *eth = data; // 边界检查:确保以太网头完整 if ((void *)(eth + 1) > data_end) return TC_ACT_OK; // 仅处理 IPv4 流量 if (eth->h_proto != bpf_htons(ETH_P_IP)) return TC_ACT_OK; struct iphdr *ip = (void *)(eth + 1); if ((void *)(ip + 1) > data_end) return TC_ACT_OK; // 仅处理 TCP 协议 if (ip->protocol != IPPROTO_TCP) return TC_ACT_OK; struct tcphdr *tcp = (void *)(ip + 1); if ((void *)(tcp + 1) > data_end) return TC_ACT_OK; // 提取目标端口(网络字节序 → 主机字节序) __u16 dport = bpf_ntohs(tcp->dport); // 检查是否为高优先级 API 端口 __u8 *prio = bpf_map_lookup_elem(&prio_port_map, &dport); __u32 classid; __u32 stat_key; if (prio && *prio == PRIO_HIGH) { classid = PRIO_HIGH; stat_key = 1; } else if (dport == 80 || dport == 443) { // 标准 HTTP/HTTPS 端口 classid = PRIO_NORMAL; stat_key = 2; } else { classid = PRIO_LOW; stat_key = 3; } // 更新流量统计 __u64 *count = bpf_map_lookup_elem(&traffic_stats, &stat_key); if (count) __sync_fetch_and_add(count, 1); // 将 classid 写入 skb,供 tc qdisc 使用 skb->tc_classid = classid; return TC_ACT_OK; } char _license[] SEC("license") = "GPL";# ============ 编译并加载 eBPF 程序 ============ # 使用 clang 编译 eBPF C 代码为目标文件 clang -O2 -target bpf -c tc_classifier.bpf.c -o tc_classifier.bpf.o # 创建 clsact qdisc(分类-动作排队规则) tc qdisc add dev eth0 clsact # 在 ingress 方向加载 eBPF 分类器 # direct-action 表示 eBPF 程序直接返回动作,不经过 tc 的 action 链 tc filter add dev eth0 ingress bpf da obj tc_classifier.bpf.o sec tc_cls # ============ 配置流量队列 ============ # 创建 HTB (Hierarchical Token Bucket) 队列 tc qdisc add dev eth0 root handle 1: htb default 20 # 优先级 10: 高优先级(API 流量),保证 500Mbps tc class add dev eth0 parent 1: classid 1:10 htb rate 500mbit ceil 1gbit prio 0 # 优先级 20: 普通流量,保证 300Mbps tc class add dev eth0 parent 1: classid 1:20 htb rate 300mbit ceil 1gbit prio 1 # 优先级 30: 低优先级,保证 100Mbps tc class add dev eth0 parent 1: classid 1:30 htb rate 100mbit ceil 1gbit prio 2 # ============ 动态更新 API 端口优先级 ============ # 通过 bpftool 向 BPF Map 中写入数据 bpftool map update id $(bpftool map show | grep prio_port_map | awk '{print $1}') \ key hex 1f 90 \ # 端口 8080 value hex 0a # 优先级 PRIO_HIGH (10)性能对比数据
| 指标 | 传统 tc (1000 规则) | eBPF tc-bpf (10000 规则) |
|---|---|---|
| 单包分类延迟 (P50) | 12.3μs | 0.8μs |
| 单包分类延迟 (P99) | 180μs | 1.2μs |
| CPU 使用率 (10Gbps 线速) | 32% | 14% |
| 规则更新延迟 | 需重建 tc 链 | 即时 (BPF Map 原子更新) |
四、eBPF 网络编程的复杂性与限制
Verifier 约束:eBPF Verifier 严格限制程序长度(100 万条指令上限)、循环必须可证明终止、禁止不可达代码。复杂的分类逻辑可能需要拆分为多个 eBPF 程序。
内核版本依赖:BPF Map 类型和 helper 函数因内核版本而异。生产环境需锁定最低内核版本(推荐 Linux 5.15+)。
调试困难:eBPF 程序在内核中执行,普通 debugger 无法附加。只能通过bpf_printk或bpftrace进行日志追踪。
不适合有状态流量处理:eBPF 程序是无状态的(虽然 Map 可以存储状态),复杂的协议解析(如 TLS 解密)应交给用户态代理处理。
五、总结
eBPF 将网络流量分类从 O(n) 的规则匹配优化为 O(1) 的哈希查找,在万兆网络中实现了线速处理。配合 HTB 队列,可以构建灵活而高性能的流量整形系统。
部署路线:
- 识别需要优先处理的关键流量(API 端口、WebSocket 连接);
- 编写 eBPF 分类器,通过 BPF Map 暴露可动态更新端口列表;
- 使用 HTB 创建分层带宽保证;
- 监控
traffic_statsMap 验证分类效果; - 在预发环境进行金丝雀测试后全量部署。
