引言:为什么 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
cburstceil 令牌桶容量,控制借用时的突发幅度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 丢包但计数器为0TC_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-latencytc 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/ — 经典学习文档
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部