引言:为什么 Traffic Control 是网络栈最被低估的子系统
Linux 网络栈中最复杂、最强大但也最令人生畏的子系统,不是 netfilter,不是 TCP 拥塞控制,而是 Traffic Control(TC)。它控制着数据包何时离开网卡——是延迟、丢弃、重新排队,还是加速发送。这个"出门决策"的质量,直接决定了分布式系统的吞吐、延迟和公平性。
在大规模云原生场景中,TC 是 Kubernetes Bandwidth Cilium mesh的关键基础设施。但 TC 文档稀少、API 晦涩——它也是 Stack Overflow 上问答比最低的内核子系统之一。本文将拆解 TC 的全栈架构,从内核数据结构到 eBPF 编程接口,给出一个完整的工程视角。
TC 的本质:在精确的时间,对特定条件的包,执行你定义的动作。
第一节:TC 核心架构全景图
1.1 Qdisc/Class/Filter 三层模型
TC 采用了分层的 Qdisc → Class → Filter 模型:
┌─────────────────────────────────────────────────────┐
│ NIC (eth0 / ens3) │
└──────────────────────┬──────────────────────────────┘
│ 出口 (egress)
┌────────────▼────────────┐
│ Root Qdisc ( root ) │ ← 根排队规则
│ 决定调度策略和整体带宽上限 │
└──┬──────────┬───────────┘
│ │
┌────────▼──┐ ┌───▼────────┐
│ Class 1: │ │ Class 2: │ ← 流量分类(共享/独占带宽)
│ Default │ │ Guaranteed │
└──┬────────┘ └──┬─────────┘
│ │
┌──▼────────┐ ┌──▼─────────┐
│Leaf Qdisc │ │Leaf Qdisc │ ← 叶节点真实调度器
│ (FQ_C) │ │ (FQ_C) │
└──┬────────┘ └──┬─────────┘
│ │
Filters decide Filters decide
which packets which packets
go to which class go to which class
1.2 内核数据结构关系
// TC 在内核中的核心数据结构关系
struct Qdisc {
struct Qdisc_ops *ops; // 调度器操作函数表(enqueue/dequeue/change...)
struct Qdisc *next; // 下一个 Qdisc(用于 ingress)
struct netdev_queue *dev_queue; // 关联的网络设备队列
struct rb_root [classes]; // Class 树(用于 HTB/CBQ)
...
};
struct Qdisc_ops {
struct Qdisc_ops *next; // 全局 Qdisc 链表
const struct Qdisc_class_ops *cl_ops; // Class 操作函数表
int (*enqueue)(struct sk_buff *, struct Qdisc *); // 入队
struct sk_buff *(*peek)(struct Qdisc *); // 偷看下一个包
struct sk_buff *(*dequeue)(struct Qdisc *); // 出队
int (*change)(struct Qdisc *, struct nlattr **); // 修改参数
};
struct tcf_proto { // 分类器链
struct tcf_proto *next;
int (*classify)(struct sk_buff *, const struct tcf_proto *,
struct tcf_result *);
__be16 protocol;
u32 prio, classid;
};
1.3 数据路径:从 dev_queue_xmit 到驱动
数据包从协议栈到网卡发送的完整路径:
// 核心发送路径 (net/core/dev.c)
int dev_queue_xmit(struct sk_buff *skb) {
struct netdev_queue *txq;
struct Qdisc *q;
int rc = -ENOMEM;
// 选择发送队列(多队列网卡由 XPS/Flow Director 决定)
txq = netdev_core_pick_tx(dev, skb);
q = rcu_dereference_bh(txq->qdisc); // 获取关联的 Qdisc
// 如果 Qdisc 支持直接发送(如 noqueue)
if (q->enqueue) {
rc = __dev_xmit_skb(skb, q, dev, txq);
goto out;
}
out:
return rc;
}
// __dev_xmit_skb 中的关键逻辑
static inline int __dev_xmit_skb(struct sk_buff *skb, struct Qdisc *q,
struct net_device *dev, struct netdev_queue *txq) {
// TRY 1: 尝试直接入队+出队(乐观路径,无锁)
if (!(dev->features & NETIF_F_LLTX)) {
HARD_TX_LOCK(dev, txq, smp_processor_id());
}
// 如果 Qdisc 不忙(__qdisc_run 启动中),或 qlen < limit
if (q->flags & TCQ_F_NOLOCK) {
// NOLOCK Qdisc (如 BPF): Fast enqueue
rc = q->enqueue(skb, q) & NET_XMIT_MASK;
} else {
spin_lock(&q->busylock);
spin_lock(&q->lock);
rc = q->enqueue(skb, q);
if (likely(dequeue_skb(q, &skb_intr))) {
// 立即出队并从驱动发送
}
spin_unlock(&q->lock);
spin_unlock(&q->busylock);
}
// TRY 2: 如果失败(被丢或入队),触发 __qdisc_run 调度软中断
if (unlikely(rc == NET_XMIT_DROP)) {
// 丢包统计 + drop kfree
} else if (unlikely(rc != NET_XMIT_SUCCESS)) {
__qdisc_run(q); // 启动 NAPI 轮询回收
}
}
第二节:Qdisc 调度算法深度解析
2.1 HTB (Hierarchical Token Bucket) — 分层令牌桶
HTB 是生产环境中最常用的带宽管理 Qdisc,允许树状层次结构,每层有自己的速率和突发容量:
// HTB Class 的核心参数
struct htb_class {
struct Qdisc_class_common common; // 基类
struct gnet_stats_rate_est64 rate; // 速率统计
struct gnet_stats_basic_packets bstats; // 包统计
s64 tokens; // 当前令牌数 (bytes)
s64 ctokens; // 当前 ceil 令牌数
s64 t_c; // 上次令牌计算时间
s64 buffer; // 令牌不足时借用缓冲
s64 cbuffer; // ceil 缓冲
u32 rate_bytes_per_sec; // rate (保证带宽)
u32 ceil_bytes_per_sec; // ceil (最大可借带宽)
u32 burst; // 令牌桶容量 (rate令牌桶)
u32 cburst; // ceil 令牌桶容量
struct Qdisc *leaf_qdisc; // 叶子 Qdisc (通常是 FQ_Codel)
...
};
// HTB 调度核心: htb_dequeue()
static struct sk_buff *htb_dequeue(struct Qdisc *sch) {
struct htb_sched *q = qdisc_priv(sch);
struct sk_buff *skb;
struct htb_class *cl;
int level;
// level 0: 从类树中选择最高优先级的活跃类
cl = htb_lookup_leaf(q->row, 0); // 在 Level 0 找第一个活跃类
// ...沿层次向下到最底层(Level 0 叶子类)
while (cl) {
if (cl->level == 0) break;
cl = htb_lookup_leaf(cl->children, cl->level);
}
// 取出此 Leaf Qdisc 的下一个包
skb = qdisc_dequeue_head(cl->leaf_qdisc);
// 调整配额(从 tokens/ctokens 减去包的字节数)
return skb;
}
HTB 的关键设计思想:每个 Class 被赋予 rate(保证带宽)和 ceil(最大可借用带宽)。当 parent Class 自身有空闲带宽时,子类可以从 parent 那里“借用”带宽(rate < actual < ceil),实现带宽的动态分配:
| HTB 参数 | 含义 | 单位 |
|---|---|---|
| rate | 保证带宽,绝对会分配这么多 | bit/s |
| ceil | 最大带宽(可借用超过 rate 的部分) | bit/s |
| burst | 令牌桶容量(rate 令牌桶),允许短时突发 | bytes |
| cburst | ceil 令牌桶容量,控制借用时的突发幅度 | bytes |
| prio | 优先级(数字越小优先级越高),同级竞争带宽时使用 | 0-7 |
2.2 FQ_Codel — 公平队列 + CoDel 主动队列管理
FQ_Codel 是 Linux 默认推荐的 SQM (Smart Queue Management) 算法,结合多流公平队列与延迟控制:
// FQ_Codel 的核心机制
struct fq_codel_flow {
struct sk_buff *head; // 每个 Flow 的包队列
struct sk_buff *tail;
struct list_head flowchain; // 全局 Flow 链表
int deficit; // 每个 Flow 的赤字计数器(关键!)
u32 dropped; // 此 Flow 的丢包计数(用于隔离)
struct codel_vars cv; // CoDel 延迟控制状态
};
// 1. 5元组哈希分片:将包分到 ~1024 个 Flow 中的一个
static u32 fq_codel_hash(struct fq_codel_sched_data *q, struct sk_buff *skb) {
return skb_get_hash(skb) % q->flows_cnt;
}
// 2. DRR (Deficit Round Robin) 轮询调度:保证每个 Flow 带宽公平
// deficit: 每次轮询可发送 deficit + QUANTUM 字节,允许小包多轮累积
static struct sk_buff *fq_codel_dequeue(struct Qdisc *sch) {
struct fq_codel_sched_data *q = qdisc_priv(sch);
struct sk_buff *skb;
struct fq_codel_flow *flow;
// 3. CoDel 延迟控制: 如果某 Flow 的包在队列中停留 > 5ms 连续超过 100ms, // 直接丢该 Flow 的尾部包,迫使发送方降低速率
flow = list_first_entry(&q->,new_flows, ...);
skb = codel_dequeue(skb, ...); // CoDel 算法(见下)
...
}
// CoDel 核心逻辑: 基于驻留时间而非队列长度做丢包决策
static struct sk_buff *codel_dequeue(struct codel_vars *vars, ...) {
// 如果驻留时间 > target(默认 5ms) 且持续 > interval(100ms)
if (codel_time_after(vars->dropping, vars->drop_next) &&
codel_time_after(now - get_packet_interval)) {
// 丢包 + 计算下次丢包时间(丢包间隔递减)
while (vars->count > 1 && codel_time_after(eq, vars->drop_next)) {
vars->count--;
vars->drop_next = control_law(vars->drop_next);
}
return DROPPED;
}
return DEQUEUED;
}
2.3 RED (Random Early Detection) — 早期随机丢包
RED 在队列未满时就开始随机丢包,迫使 TCP 发送方提前减速,避免全局同步:
// RED 核心参数
struct red_sched_data {
u32 limit; // 最大队列长度
u32 qth_min; // 最小阈值(到达此长度开始丢包)
u32 qth_max; // 最大阈值(到达此长度全部丢弃)
u32 probability; // 最大丢包概率(通常为 0.02)
u32 EWMA; // 指数加权移动平均队列长度
};
// RED 丢包决策(当 avg 在 [qth_min, qth_max] 之间时)
// P(丢包) = max_p * (avg - qth_min) / (qth_max - qth_min)
// 随着 avg 增大,丢包概率线性增加,形成"随机早期探测"
第三节:Filter 分类器 — 决定包的去向
3.1 u32 分类器 — 按协议字段精确匹配
u32 是最常用的 TC Filter,通过位掩码直接匹配 IP/TCP/UDP 头部字段:
// u32 匹配的核心:struct tc_u_knode
struct tc_u_knode {
struct tc_u_knode *next;
u32 handle;
struct tc_u32_sel sel; // 匹配选择器
struct tc_u32_key keys[U32_MAXDEPTH]; // 匹配键数组
// ...
};
struct tc_u32_key {
__be32 mask; // 掩码
__be32 val; // 值
int off; // 偏移量 (skb_data 中的偏移)
int offmask; // 偏移掩码(掩码对齐)
};
// 匹配 IP 目标端口 80 的 u32 rule:
// match ip dst_port 0x0050
// 等价于: sel(keys){ off=22, val=0x0050, mask=0xFFFF }
// skb_data 偏移 22 处是 TCP/UDP 目的端口(IP 头部 20B + 源端口后 2B)
3.2 bpf 分类器 — 高级可编程过滤
TC 的 bpf 分类器允许将 eBPF 程序挂载到 Qdisc 的任意节点,直接决定包的 Class:
// TC BPF 分类器: define BPF Program type
// BPF 程序返回值:
// -1 → 不匹配,尝试下一个 filter
// 0 → 不匹配
// classid → 匹配成功,返回 classid (major:minor)
SEC("classifier")
int tc_egress(struct __sk_buff *skb) {
// 读取目的 IP
struct iphdr *ip = (struct iphdr *)(skb->data + 14); // 跳过以太网头
__u32 dst_ip = bpf_ntohl(ip->daddr);
// 根据目的 IP hash 决定分流
if (dst_ip & 0xFF000000) {
// 情况 1: A类地址 → Class 1:10
return 0x10010; // major=1, minor=10
}
// 默认: Class 1:20(deftracleflow)
return 0x10020;
}
第四节:eBPF TC 编程 — clsact 数据面
4.1 clsact Qdisc:TC 的极简 hook 点
clsact 是专门用于 eBPF 挂载的"假 Qdisc"——它不排队,只允许 BPF 程序拦截每个包:
# 只需两条命令即可挂载 TC BPF (相比 HTB+FQ_Codel 减少 10x 命令)
tc qdisc add dev eth0 clsact
tc filter add dev eth0 egress bpf obj tc_egress.o section classifier
# 等价 eBPF 方式 (通过 bpftool):
bpf(BPF_PROG_LOAD, &attr, ...) // 加载 BPF prog
bpf(BPF_LINK_CREATE, &{attach_type=BPF_TCX_EGR, target_ifindex=ifidx}) // tcx 单程序
# 或传统方式 (tc 命令):
tc filter add dev eth0 ingress bpf da obj tc_prog.o sec ingress
4.2 BPF_PROG_TYPE_SCHED_CLS — 完整的包操作能力
TC cls BPF 程序拥有比 XDP 更丰富的上下文(__sk_buff),但性能略低(需 skb 已分配):
// BPF 辅助函数在 TC cls 中的关键能力
// 1. 直接数据包操作 (direct-action)
int bpf_skb_vlan_push(struct sk_buff *skb, __be16 vlan_proto, u16 vlan_tci);
int bpf_skb_vlan_pop(struct sk_buff *skb, __be16 vlan_proto, u16 vlan_tci);
int bpf_skb_adjust_room(struct sk_buff *skb, s32 len_diff, u32 mode, u64 flags);
// 2. 重定向与转发
int bpf_redirect(__u32 ifindex, __u32 flags);
int bpf_redirect_map(struct bpf_map *map, __u32 key, __u64 flags);
int bpf_sk_redirect_map(struct sk_buff *skb, struct bpf_map *map, __u32 key, __u64 flags);
// 3. 数据包元数据标记
bpf_skb_set_tunnel_key(skb, &key, size, flags); // 设置 tunnel key
bpf_skb_set_tunnel_opt(skb, &opt, size); // GRE/VXLAN 选项
bpf_set_hash(skb, hash); // 设置硬件 offload hash
// 4. NAT 封装
int bpf_lwt_encap(skb, type, hdr, len); // 轻量隧道封装 (IPIP/GRE/VXLAN)
int bpf_lwtunnel_encap_seg6(skb, &srh, len); // SRv6 Segment Routing
// 5. 标记传递 (传递给 netfilter 链栈)
bpf_skb_set_tclass(skb, tclass); // SO_PRIORITY (setsockopt 映射)
bpf_skb_cgroup_id(skb); // 关联到 cgroup (用于 kubectl --pod-bw)
bpf_skb_ecn_set_ce(skb); // 设置 ECN-CE 标记 (拥塞通知)
4.3 TC Direct-Action 模式 — 决定包命运的关键
TC BPF 程序返回特殊值控制包的处理:
// TC BPF 返回值语义
// ============================================================
// 返回值 行为
// ============================================================
// TC_ACT_OK (0) 包正常进入 Qdisc 的入队/出队流程 (类似 filter classid=-1)
// TC_ACT_RECLASSIFY 包头部可能已改变,重新从头开始分类
// TC_ACT_SHOT 直接丢包(性能最高,等同 XDP_DROP 但不是驱动层)
// TC_ACT_PIPE 下一个 filter 继续处理 (类似 filter classid=-1)
// TC_ACT_QUEUED 包进入 per-CPU 队列(自动出队后处理)
// TC_ACT_STOLEN 包从系统中消失,稍后可能需要重注入
// TC_ACT_REDIRECT 包被重定向到另一个 iface(BPF helper bpf_redirect())
// TC_ACT_TRAP 包发送到 CPU 的 backlog (高负载时)
// TC_ACT_VALUE(0xff) 用户自定义返回值(通过 skb->tc_index 传递)
// ============================================================
// 典型 TC BPF 程序结构
SEC("classifier")
int tc_main(struct __sk_buff *skb) {
void *data_end = (void *)(long)skb->data_end;
void *data = (void *)(long)skb->data;
struct ethhdr *eth = data;
struct iphdr *ip;
// 边界检查
if ((void *)eth + sizeof(*eth) > data_end)
return TC_ACT_OK;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return TC_ACT_OK;
ip = (struct iphdr *)((void *)eth + sizeof(*eth));
if ((void *)ip + sizeof(*ip) > data_end)
return TC_ACT_OK;
__u8 proto = ip->protocol;
if (proto == IPPROTO_UDP) {
struct udphdr *udp = (struct udphdr *)((void *)ip + sizeof(*ip));
if ((void *)udp + sizeof(*udp) > data_end)
return TC_ACT_OK;
// DNS 查询: 限速
if (udp->dest == bpf_htons(53)) {
__u64 *tokens = bpf_map_lookup_elem(&dns_limit_map, &ip->saddr);
if (tokens && *tokens == 0)
return TC_ACT_SHOT; // 限速丢包
if (tokens)
__sync_fetch_and_sub(tokens, 1);
}
// HTTP : QoS 标记
if (udp->dest == bpf_htons(443)) {
bpf_skb_set_tclass(skb, 0x1); // SO_PRIORITY 映射到 sk_prio
}
}
return TC_ACT_OK;
}
第五节:TC eBPF 与 XDP 对比 — 选型决策树
| 特性 | XDP (bpf_xdp) | TC clsact (bpf_cls) |
|---|---|---|
| 挂载位置 | 网卡驱动层 (最早,NAPI poll之前) | 内核 TC 栈 (skb 已分配) |
| 性能 | 更高(~最高 24M pps/核) | 稍低(~最高 8M pps/核) |
| 包上下文 | xdp_md (数据帧,无 skb) | __sk_buff (有 skb) |
| 修改包 | 有限(不能添加/删除 > few bytes) | 完整 (adjust_room, vlan_push/pop) |
| skb 操作 | 无 | 全部 (cgroup, tclass, ECN, skb->mark) |
| 输出方向 | 仅入口 (可 redirect 到 TX via devmap) | 入口+出口 (egress TC hook 极强) |
| 加密/隧道 | 有限 (只能少量操作) | 完整 (bpf_lwt_encap, SRv6, VXLAN) |
| 复用 netfilter | 无(独立) | 可以输出到 netfilter 链栈 |
| TLS 解密 | 无需 skb 开销 | 需 skb(但有更多上下文) |
| 典型场景 | DDoS 防护/极速 ACL | 流量整形、NAT、Service Mesh、微分段 |
第六节:生产级实战场景
6.1 Kubernetes Pod 带宽管理 — TcPodBandwidth 机制
Cilium 等 CNI 插件使用 TC eBPF 实现 Pod 级别的 kubernetes.io/ingress-bandwidth 和 egress-bandwidth annotation:
// 每个 veth pair 的 egress 挂载 TC BPF
# Cilium 自动执行:
tc qdisc add dev lxcxxx clsact # 在 Pod 的 veth 虚拟网卡上
tc filter add dev lxcxxx egress bpf obj cilium_lxc.o # 挂载管理的 BPF
// BPF 程序核心逻辑 (cilium/bpf/util.c 简化版)
SEC("classifier")
int tc_egress_bw(struct __sk_buff *skb) {
// 查找此 Pod 对应的带宽限制配置
__u64 pod_id = bpf_skb_cgroup_id(skb); // 由 cgroup_bpf 提供
struct bw_limit *limit = bpf_map_lookup_elem(&pod_bw_map, &pod_id);
if (!limit)
return TC_ACT_OK; // 无配置则放行
// 令牌桶限速 (如 Pod 被限制为 100Mbps)
__u64 now = bpf_ktime_get_ns();
__u64 elapsed = now - limit->last_tokens_fill;
__u64 new_tokens = (elapsed * limit->rate) / NSEC_PER_SEC;
if (new_tokens > 0) {
limit->tokens = min(limit->tokens + new_tokens, limit->burst);
limit->last_tokens_fill = now;
}
// 检查是否有足够的 token
if (limit->tokens >= skb->len) {
limit->tokens -= skb->len;
return TC_ACT_OK; // 允许发送
}
return TC_ACT_SHOT; // 超带宽: 丢包 (TCP 会降速)
}
6.2 微分段安全策略 — 容器级别的零信任
使用 TC BPF 实现容器间通信的零信任白名单(禁止 north-south 方向的未授权访问):
// 每个容器 veth 挂载的微分段 BPF
// from-pod (egress): 哪些 IP 可以访问?
// to-pod (ingress): 哪些 IP 可以进来?
SEC("classifier")
class = match_policy(skb);
switch (action) {
case ACTION_ALLOW:
return TC_ACT_OK;
case ACTION_DENY:
// 记入审计日志
struct event evt = { .src_ip=..., .dst_ip=..., .action=DENIED };
bpf_perf_event_output(skb, &events, BPF_F_CURRENT_CPU, &evt, sizeof(evt));
return TC_ACT_SHOT;
case ACTION_L7_CHECK:
// 放行达上层 7 层代理 (Envoy/Cilium)
bpf_skb_set_tclass(skb, 0x2);
return TC_ACT_OK;
}
6.3 TC BPF 实现透明 NAT — 无需 iptables
在 TC 层完成 SNAT/DNAT 操作,完全绕过 netfilter,显著降低 iptables 在大规模规则下的延迟:
// TC BPF SNAT: 在包发出去之前改写 src_ip
SEC("classifier")
int tc_snat(struct __sk_buff *skb) {
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
struct ethhdr *eth = data;
if ((void *)(eth+1) > data_end) return TC_ACT_OK;
struct iphdr *ip = (void *)(eth+1);
if ((void *)(ip+1) > data_end) return TC_ACT_OK;
// 查找 SNAT 映射 (由用户态 agent 维护)
__u32 orig_dst = ip->daddr;
struct nat_key key = { orig_dst, ip->saddr };
struct nat_val *val = bpf_map_lookup_elem(&nat_map, &key);
if (val) {
// DNAT: 改写目的 IP (Service→Pod IP)
ip->daddr = val->new_addr;
#pragma unroll
for (int i = 0; i < 5; i++)
update_ip_csum_l4(ip); // 增量 L4 CKSUM 优化
}
// 同时改写 Ethernet 头 (若跨越二层)
if (skb->tc_index == TC_INDEX_MAGIC)
__builtin_memcpy(eth->h_dest, val->next_hop_mac, 6);
return TC_ACT_RECLASSIFY; // 重新分类(因 IP 头修改)
}
第七节:内核 TC 性能调优与避坑指南
7.1 吞吐量调优参数
# 关闭 GRO/LRO (特别是 XDP/TC BPF 场景,GRO 影响观测)
ethtool -K eth0 gro off
ethtool -K eth0 lro off
# 增大 RX/TX ring buffer
ethtool -G eth0 rx 4096 tx 4096
# 开启 MQ (多队列) 绑定 CPU
echo "ff" > /sys/class/net/eth0/queues/rx-0/rps_cpus
# 或更好: 使用 XPS 确保 TX queue 绑定 CPU
echo 1 > /sys/class/net/eth0/queues/tx-0/xps_cpus
# TC clsact BPF 不用清理 Qdisc 锁
# BPF clsact 自动获得 per-CPU 队列,无需 spinlock
# 建议: tc filter add dev eth0 egress bpf ... direct-action 模式
# net.core 参数调优 (大流量场景)
sysctl -w net.core.rmem_max=134217728 # receive buffer
sysctl -w net.core.wmem_max=134217728 # send buffer
sysctl -w net.core.netdev_max_backlog=10000 # 单核 NAPI 处理队列
sysctl -w net.core.somaxconn=65535 # 监听 backlog
7.2 TC 常见陷阱与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| tc qdisc 越删越多 | 未先 del root,新旧 qdisc 叠加 | tc qdisc del dev eth0 root (先删根) |
| BPF 丢包但计数器为0 | TC_ACT_SHOT 不经过 qdisc 统计,从 ingress_ifif 看 | tc -s filter show dev eth0 egress 查看 hits |
| HTB rate 不等于实际速率 | 未配置 burst/cburst,令牌桶过小无法利用突发 | burst 至少 = rate/8000 (ms 粒度令牌填充) |
| FQ_Codel 延迟仍然过高 | target/interval 默认 5ms/100ms 不适合 DC low-latency | tc qdisc ... fq_codel target 2ms interval 40ms |
| BPF 包修改后校验和错误 | 修改了 IP/TP 头但未更新 csum | 使用 bpf_skb_store_bytes + BPF_F_RECOMPUTE_CSUM 自动修复 |
| clsact BPF 看不到 ECN | 需要 skb->tcphdr 检查 (有 GRO) | ethtool -K eth0 tso off gso off |
| iptables 与 TC 计数冲突 | TC metric 优先考虑 tc_index,netfilter 用 mark | 通过 bpf_skb_set_tclass 区分路径 |
第八节:TC BPF 在 Cilium 中的实战实现
Cilium 是 TC BPF 在生产中最成功的应用之一,在每个容器 veth pair 的 egress/ingress 挂载 BPF,实现:
// Cilium TC BPF 挂载方式 (net_device 级别)
// ======== Outbound (from pod) ========
// veth egress: tc_cls_egress_main() (cilium/bpf_lxc.c)
// 1. 查 Pod cid (cgroup)
// 2. 查目的 IP/ID 是否在同节点? 若是,直接 veth pair 到对端
// 3. 不同节点? 走 IPIP/Tunnel 封装
// 4. 防火墙检查 (L3/L4/L7 白名单)
// 5. 带宽限制 (Token Bucket)
// 6. NAT (若 destination 为 0.0.0.0/0)
// ======== Inbound (to pod) ========
// veth ingress: tc_cls_ingress_main()
// 1. 检查是否是 tunnel 封包 (外层 IPIP/Geneve)
// 2. 若是则解封装,得到内层 Pod IP
// 3. 查目的 Pod ID 是否被允许访问 (deny by default)
// 4. 若经过 L7 代理 (Envoy),redirect 到 proxy socket (bpf_redirect_peer)
// ======== HOST 接口处理 ========
// Host egress: HOST_EP_ID 匹配
// - 跳过所有 Istio/Envoy 代理 (passthrough)
// - 检查主机防火墙规则
// - 主机 DNS 拦截统计
第九节:从 FQ_Codel 到 EDT — TC 调度器的未来
Linux 5.6+ 引入了 EDT (Earliest Departure Time) BPF 调度器,彻底改变带宽限制的方式:
// EDT BPF: 不再基于丢包做限速! 而是基于"何时可以发送"
// 传统令牌桶: 有令牌就发,无令牌就丢 → TCP 全局同步、队头阻塞
// EDT BPF: 计算每个包的 departure_time = now + (packet_bits / rate)
// Cilium EDT BPF 核心逻辑 (简化版)
SEC("classifier")
int tc_edt(struct __sk_buff *skb) {
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
struct edt_id *edt_id = bpf_map_lookup_elem(&edt_id_map, &key);
if (!edt_id)
return TC_ACT_OK;
// 计算此包的 departure_time
__u64 tsk = bpf_ktime_get_ns();
__u64 t_now = bpf_jiffies64() * (1000000000 / HZ);
__u64 pkt_len = skb->len;
__u64 delay_ns = (pkt_len * 8 * NSEC_PER_SEC) / edt_id->bps; // bits → ns
__u64 t_horizon = 100 * USEC_PER_SEC; // 100μs 容忍带宽抖动窗口
__u64 t_schedule = edt_id->t_last + delay_ns; // 期望发送时间
if (t_schedule > t_now) {
// 延迟发送 (FQ_Codel 在内部 tx_time_enqueue 处)
if (t_schedule - t_now > t_horizon) {
return TC_ACT_SHOT; // 超时太多,丢包
}
// 在 BPF 设置 skb->tstamp 指向 departure_time
bpf_set_tstamp(skb, t_schedule, BPF_SKB_TSTAMP_DELIVERY);
return TC_ACT_OK;
} else {
edt_id->t_last = t_now; // 队列已空,更新 last = now
return TC_ACT_OK;
}
}
// EDT 优势:
// 1. 零丢包 (长期) — 总是发送,只是推迟
// 2. TCP 友好 — 配合 BBR/DCTCP 优化
// 3. P99延迟可预测 — 不再是丢包后重传
// 4. 下游 FQ_Codel 天然平滑 burst (E2E 时延保证)
第十节:总结与选型最佳实践
TC Traffic Control 是 Linux 网络栈中体系最完整也最复杂的子系统。与 eBPF 结合后,它从"shell 配置工具"进化为可编程的数据面引擎。选型图谱:
需求 → 推荐方案
────────────────────────────────────────────────────────────────────
极速 DDoS 防护 (百万 pps) → XDP_DROP + XDP_REDIRECT
Pod 带宽限速 (K8s) → TC clsact + EDT BPF
容器微分段访问控制 → TC clsact BPF (allow/deny)
Service Mesh (Envoy sidecar 路由) → TC clsact + sockmap redirect
拥塞管理 (BBR over 网关) → FQ_Codel target=1ms
多租户带宽公平分配 → HTB + FQ_Codel 混合队列
SRv6 可编程报文路由 → TC clsact bpf_lwtunnel_encap_seg6()
透明 L4 NAT (替代 iptables) → TC bpF SNAT/DNAT
ECN / 拥塞传递到 TCP → TC clsact bpf_skb_ecn_set_ce()
TLS 拦截/优化 → TC clsact + sockmap + strp
────────────────────────────────────────────────────────────────────
参考文献与深度阅读
- Linux Traffic Control 官方文档:man tc(8), man tc-htb(8), man tc-fq_codel(8)
- Cilium eBPF 数据面源码:github.com/cilium/cilium (bpf/ 目录)
- Linux TC 实现内核源码:net/sched/ — sch_htb.c sch_fq_codel.c cls_bpf.c
- TC BPF 直接操作 API:include/uapi/linux/bpf.h — TC_ACT_* 定义
- EDT Bandwidth Manager:lore.kernel.org/bpf/20200501083727.26358
- "Linux Traffic Control" 系列:lartc.org/howto/ — 经典学习文档

发表评论 取消回复