当前位置:首页>Linux>Linux内核:流量控制的艺术

Linux内核:流量控制的艺术

  • 2026-10-11 07:03:56
Linux内核:流量控制的艺术

eBPF 深度剖析 第11节

eBPF 与 tc——流量控制的艺术

就像交响乐团的指挥用指挥棒精确控制每个乐手的出场时机,Linux 的流量控制(tc)用 eBPF 程序精确控制每个数据包的去向——谁先走、谁后走、谁要改道、谁直接劝退。


一、为什么需要 tc?(XDP 解决不了的问题)

XDP 很强大,但它有个致命弱点:只能在网卡驱动层拦截数据包,而且只能处理入站流量(ingress)。

XDP 能做
XDP 不能做
超高速丢包(DDOS 防护)
处理出站流量(egress)
早期过滤(最前端)
修改数据包后重新排队
简单统计
复杂的流量整形(traffic shaping)
重定向到其他网卡
与 QoS 队列配合

1.2 tc 出场

tc(traffic control) 是 Linux 内核的流量控制系统,它能做 XDP 做不到的事:

tc 能力
说明
出站流量控制
控制发送速率、优先级、排队策略
入站流量处理
用 ingress qdisc 处理到达的包
流量整形
限速、延迟、丢包模拟
队列管理
用不同队列调度发包顺序
与 eBPF 配合
用 eBPF 程序做分类、过滤、修改

一句话总结:XDP 是"门卫"(只管进门),tc 是"交通指挥官"(管进出双向)。


二、tc 架构——qdisc、class、filter

tc 的核心概念有三个,理解它们就理解了流量控制:

2.1 qdisc(排队规则)

qdisc = queueing discipline(排队规则)。每个网卡都有一个 qdisc,决定数据包如何排队、如何发送。

qdisc 类型
说明
典型用途
pfifo_fast
默认的简单 FIFO 队列
普通场景
htb
分层令牌桶(Hierarchical Token Bucket)
限速、QoS
sfq
随机公平队列
防止某个流占满带宽
clsact
专为 eBPF 设计(无队列,只做分类)
eBPF + tc 首选

2.2 class(类别)

class 是 qdisc 的子节点,可以对流量分层限速。比如:

htb qdisc

├── class 1:1(总带宽 100Mbps)

├── class 1:10(SSH,保证 10Mbps)

├── class 1:20(HTTP,保证 30Mbps)

└── class 1:30(其他,剩余带宽)

2.3 filter(过滤器)

filter 决定"这个数据包属于哪个 class"。而 eBPF 程序,就是最高级的 filter。

数据包到达 → filter 判断 → 放到对应 class 的队列 → 按规则发送


三、clsact qdisc——eBPF 的最佳拍档

3.1 为什么用 clsact?

传统的 tc qdisc(如 htb)是有队列的,会引入延迟。而 clsact(classifier and action) 是专为 eBPF 设计的无队列 qdisc:

特性
clsact
传统 qdisc(htb 等)
是否有队列
无(直接判决)
有(引入延迟)
性能
极高(接近 XDP)
较低
用途
eBPF 分类/过滤
流量整形/限速
挂载 eBPF
原生支持
需要繁琐配置

3.2 挂载 clsact

# 在网卡 eth0 上挂载 clsact qdisc

sudo tc qdisc add dev eth0 clsact

# 验证

sudo tc qdisc show dev eth0

# 输出示例:

# qdisc clsact ffff: parent ffff:fff1


四、eBPF 在 tc 上的挂载方式

4.1 ingress 和 egress

tc 支持两个挂载点:

挂载点
方向
说明
ingress
入站(数据包到达网卡)
类似 XDP,但更靠后(经过协议栈初步处理)
egress
出站(数据包从网卡发出)
XDP 做不到!这是 tc 的独门绝技

4.2 用 tc 命令挂载 eBPF 程序

# 1. 挂载到 ingress(入站)

sudo tc filter add dev eth0 ingress bpf da obj xdp_prog.o sec tc

# 2. 挂载到 egress(出站)

sudo tc filter add dev eth0 egress bpf da obj xdp_prog.o sec tc

# 参数说明:

# - da:direct-action(直接判决,不需要额外 action 模块)

# - obj xdp_prog.o:eBPF 字节码文件

# - sec tc:ELF section 名称(程序中用 SEC("tc") 声明)

4.3 查看已挂载的 tc eBPF 程序

# 查看 ingress 上的 filter

sudo tc filter show dev eth0 ingress

# 查看 egress 上的 filter

sudo tc filter show dev eth0 egress

# 输出示例:

# filter protocol all pref 49152 bpf chain 0

# filter protocol all pref 49152 bpf chain 0 handle 0x1 xdp_prog.o:[tc] direct-action ...


五、XDP vs tc——该用哪个?

这是 eBPF 开发者最常问的问题。答案:看场景。

5.1 性能对比

指标
XDP
tc(clsact)
处理位置
网卡驱动层(最前端)
协议栈之后(ingress)或之前(egress)
延迟
最低(微秒级)
低(接近 XDP,但稍高)
能丢包吗
能(最快)
能
能修改包吗
能(但有限制)
能(更灵活)
能重定向吗
能(到其它网卡/CPU)
能
能处理 egress 吗
不能
能

5.2 决策树

需要超高速丢包(DDOS)→ XDP

需要处理出站流量(egress)→ tc

需要修改数据包后重新进入协议栈 → tc

需要早期过滤(在协议栈之前)→ XDP

需要流量整形/限速 → tc(配合 htb qdisc)

两者都要 → 先用 XDP 过滤,再用 tc 精细控制


六、实战:用 tc + eBPF 做流量分类

6.1 目标

功能:用 tc 挂载 eBPF 程序,实现:

1. 统计入站/出站包数量

2. 丢弃所有 SSH(端口 22)流量

3. 标记 HTTP(端口 80)流量(修改 DSCP 字段)

6.2 内核态代码(tc_classifier.c)

// tc_classifier.c

#include <linux/bpf.h>

#include <linux/if_ether.h>

#include <linux/ip.h>

#include <linux/tcp.h>

#include <bpf/bpf_helpers.h>

// 定义 Map:统计入站/出站包数量

struct {

__uint(type, BPF_MAP_TYPE_ARRAY);

__uint(max_entries, 2); // 0 = ingress, 1 = egress

__type(key, __u32);

__type(value, __u64);

} pkt_stats SEC(".maps");

// tc 程序入口(ingress 和 egress 共用)

SEC("tc")

inttc_classifier(struct __sk_buff *skb) {

void *data = (void *)(long)skb->data;

void *data_end = (void *)(long)skb->data_end;

// 1. 检查以太网头

if (data + sizeof(struct ethhdr) > data_end) {

return TC_ACT_OK;

}

struct ethhdr *eth = data;

// 2. 只处理 IPv4

if (eth->h_proto != __constant_htons(ETH_P_IP)) {

return TC_ACT_OK;

}

// 3. 检查 IP 头

struct iphdr *ip = data + sizeof(struct ethhdr);

if (data + sizeof(struct ethhdr) + sizeof(struct iphdr) > data_end) {

return TC_ACT_OK;

}

// 4. 只处理 TCP

if (ip->protocol != IPPROTO_TCP) {

return TC_ACT_OK;

}

// 5. 检查 TCP 头

struct tcphdr *tcp = data + sizeof(struct ethhdr) + sizeof(struct iphdr);

if (data + sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct tcphdr) > data_end) {

return TC_ACT_OK;

}

// 6. 统计包数量(根据 skb->ingress_ifindex 判断方向)

__u32 key = (skb->ingress_ifindex == 0) ? 1 : 0; // 1=egress, 0=ingress

__u64 *count = bpf_map_lookup_elem(&pkt_stats, &key);

if (count) {

__sync_fetch_and_add(count, 1);

}

// 7. 如果是 SSH(端口 22),丢弃

if (tcp->dest == __constant_htons(22)) {

return TC_ACT_SHOT; // 丢包

}

// 8. 如果是 HTTP(端口 80),标记 DSCP(修改 IP TOS 字段)

if (tcp->dest == __constant_htons(80)) {

// 设置 DSCP = 46(EF,加速转发)

ip->tos = 0xb8; // 10111000 = DSCP 46 + CU 0

// 注意:需要重新计算 IP 校验和(这里简化,实际要用 bpf_csum_diff)

}

// 9. 其他包放行

return TC_ACT_OK;

}

// 必须声明 License(否则 Verifier 拒绝)

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

6.3 编译

# 编译 eBPF 程序

clang -target bpf -O2 -g -D__TARGET_ARCH_x86_64 \

-I/usr/include/x86_64-linux-gnu \

-c tc_classifier.c -o tc_classifier.o

# 验证

llvm-objdump -S tc_classifier.o

6.4 挂载到 ingress 和 egress

# 1. 挂载 clsact qdisc

sudo tc qdisc add dev eth0 clsact

# 2. 挂载到 ingress(入站)

sudo tc filter add dev eth0 ingress bpf da obj tc_classifier.o sec tc

# 3. 挂载到 egress(出站)

sudo tc filter add dev eth0 egress bpf da obj tc_classifier.o sec tc

# 4. 验证

sudo tc filter show dev eth0 ingress

sudo tc filter show dev eth0 egress

6.5 测试

# 在另一终端:测试 SSH(应该被阻断)

ssh user@localhost # 连接超时

# 测试 HTTP(应该正常,但 DSCP 被标记)

curl http://example.com

# 查看统计(用 bpftool)

sudo bpftool map show # 找到 pkt_stats 的 ID

sudo bpftool map dump id <MAP_ID>

# 输出:

# [0] = 42 # ingress 包数量

# [1] = 18 # egress 包数量

6.6 卸载

# 卸载 ingress filter

sudo tc filter del dev eth0 ingress

# 卸载 egress filter

sudo tc filter del dev eth0 egress

# 卸载 clsact qdisc

sudo tc qdisc del dev eth0 clsact


七、tc 与 XDP 的协同使用

7.1 分层防御

在实际生产中,XDP 和 tc 通常配合使用:

数据包到达网卡

↓

[XDP] 超高速丢包(DDOS 防护、黑名单过滤)

↓(放过)

[协议栈处理]

↓

[tc ingress] 精细过滤(修改包、复杂逻辑)

↓(放过)

[上层应用]

↓(发出)

[tc egress] 流量整形、QoS、出站过滤

↓

发出网卡

7.2 Cilium 的做法

Cilium(Kubernetes 网络插件)就是 XDP + tc 协同的典范:

功能
使用
DDOS 防护
XDP
Kubernetes Service 负载均衡
XDP(首选)或 tc(fallback)
Pod 出站流量策略
tc egress
Pod 入站流量策略
tc ingress
跨节点流量
XDP(如果网卡支持)

八、常用 tc + eBPF 场景

8.1 场景1:容器网络限速

# 用 htb qdisc 限制容器 veth 的带宽

sudo tc qdisc add dev veth123 root handle 1: htb

sudo tc class add dev veth123 parent 1: classid 1:1 htb rate 100mbit

sudo tc filter add dev veth123 parent 1: bpf da obj rate_limit.o sec tc

8.2 场景2:Kubernetes Service 负载均衡(Cilium)

Cilium 用 eBPF 在 tc 层做 Service 负载均衡:

Pod A 访问 Service IP:Port

↓

[tc egress] eBPF 程序修改目标 IP:Port 为后端 Pod IP:Port

↓

直接发送到后端 Pod(绕过 iptables)

8.3 场景3:流量镜像(Mirror)

用 eBPF 在 tc 层复制数据包,发送到监控端口:

// 在 tc 程序中用 bpf_clone_redirect() 复制包

bpf_clone_redirect(skb, monitor_ifindex, 0); // 0 = ingress, 1 = egress


九、总结

知识点
说明
tc 是什么
Linux 流量控制系统,管进出双向
clsact qdisc
专为 eBPF 设计,无队列,高性能
ingress/egress
tc 的两个挂载点,egress 是 tc 独有
XDP vs tc
XDP 更快但只能 ingress;tc 慢一点但功能更全面
协同使用
XDP 做早期过滤,tc 做精细控制
实战
用 tc + eBPF 实现流量分类、丢包、标记

9.1 下一步

- 第 12 节:eBPF 与 perf_event——如何高效将内核数据传到用户态

- 第 13 节:eBPF 与 tracing(kprobe/uprobe)——用 eBPF 追踪内核和用户态函数


我是 [你的名字],正在连载「eBPF 深度解析」系列(共 100 节)。

下节见 🚀

◆ ◆ ◆

最新文章

随机文章