Linux内核TC(Traffic Control)流量控制深度实战:从HTB队列到CAKE拥塞控制的艺术

引言

在现代网络环境中,无论是家庭宽带的Bufferbloat治理、数据中心的多租户QoS保障,还是边缘网关的带宽分配策略,Linux内核的Traffic Control(TC)子系统都是不可或缺的核心工具。然而,TC因其复杂的层级结构、晦涩的术语和繁多的队列规则,被许多运维工程师视为"黑魔法"。本文将从零开始,深入剖析TC子系统的架构原理,并通过实战案例讲解HTB、FQ_CODEL、CAKE三大核心组件的配置与优化。

一、TC子系统架构全景

1.1 数据流路径

理解TC的关键在于理解数据包在内核中的流转路径。当数据包从网卡(NIC)到达后,经过hard_start_xmit()进入网络层的dev_queue_xmit(),然后入队到排队规则(qdisc),最终由设备驱动发送出去。整个过程中,TC负责在入队(ingress)和出队(egress)两个方向上进行流量整形。


┌──────────────────────────────────────────────────────────────┐
│                     数据包发送路径                              │
│                                                              │
│  应用层 → Socket → 传输层(TCP/UDP) → 网络层(IP)               │
│                                   │                          │
│                                   ▼                          │
│                           邻居子系统(ARP)                      │
│                                   │                          │
│                                   ▼                          │
│                          TC Egress (出队)                     │
│                          ┌─────────────┐                     │
│                          │   Root Qdisc │                     │
│                          │  (HTB/FQ)   │                     │
│                          └──────┬──────┘                     │
│                                 │                            │
│                    ┌────────────┼────────────┐               │
│                    ▼            ▼            ▼               │
│               Class A      Class B      Class C              │
│              (子队列)      (子队列)      (子队列)              │
│                    │            │            │               │
│                    ▼            ▼            ▼               │
│              Leaf Qdisc   Leaf Qdisc   Leaf Qdisc            │
│            (FQ_CODEL)   (FQ_CODEL)     (SFQ)                 │
│                    │            │                            │
│                    └────────────┘                            │
│                                 │                            │
│                           dev_hard_start_xmit()              │
│                                 ▼                            │
│                           网卡发送                             │
└──────────────────────────────────────────────────────────────┘

1.2 核心概念术语表

在深入实战之前,先建立一个清晰的概念坐标系:

术语说明
Qdisc排队规则(Queueing Discipline),控制数据包排队和出队算法
Class类(仅classful qdisc支持),用于树形分层结构的带宽分配
Filter分类器,将数据包分流到指定的Class
Classifier分类算法(如u32、bpf、flow),匹配数据包特征
Shaping整形,缓存超额流量以符合速率限制(可延迟发送)
Policing监管,直接丢弃超额流量(ingress只能policing)
CBQClass Based Queueing,早期的基于类的队列
HTBHierarchy Token Bucket,层次令牌桶,最常用的classful qdisc
SCHED_PRIO优先级队列,简单的3-band优先级调度

1.3 TC命令结构

TC命令遵循一致的结构:tc [OBJECT] [COMMAND] dev [IFACE] [PARAMETERS]

# 查看现有qdisc配置
tc qdisc show dev eth0

# 查看分类统计
tc -s class show dev eth0

# 查看过滤器规则
tc filter show dev eth0

# 查看特定队列统计
tc -s qdisc show dev eth0

二、HTB(层次令牌桶):分层带宽分配的基石

2.1 令牌桶算法原理

HTB的核心思想是令牌桶(Token Bucket):系统以固定速率生成令牌,每个令牌代表发送一定字节数据的权限。数据包必须消耗对应数量的令牌才能被发送。当令牌耗尽时,数据包必须等待新令牌生成(整形)或直接丢弃(监管)。

HTB引入了两个关键参数:

  • rate:保证带宽(Committed Rate, CR),每秒分配的令牌速率
  • ceil:峰值带宽(Ceiling Rate, CR),允许突发时的最大借取带宽
  • burst:桶深度,允许一段时间不发送数据后积累的最大令牌数
  • cburst:ceil桶深度,用于突发传输的令牌积累

当某个Class的rate未用完时,多余的带宽可以"借给"其他需要带宽的Class(在不超过其ceil的前提下)。这种带宽借贷机制是HTB相比简单限速方案的核心优势。


令牌桶工作机制:
┌─────────────────────────────────────────────────┐
│  时间 →    T=0     T=1     T=2     T=3     T=4   │
│  令牌生成:  +10     +10     +10     +10     +10   │
│                                                  │
│  Case A: 恰好满速率发送                            │
│  消耗:     -10     -10     -10     -10     -10   │
│  桶深度:    0       0       0       0       0     │
│                                                  │
│  Case B: T=1,T=2空闲 积累令牌                      │
│  消耗:     -10      0      0      -20     -10    │
│  桶深度:    0      +10    +20      0       0     │
│                                                  │
│  Case C: 突发超出桶深度                            │
│  消耗:     -10    -20      0      -10     -10    │
│  桶深度:    0    →等待令牌  -10     -10     -10   │
│            (缓冲区等待整形)                         │
└─────────────────────────────────────────────────┘

2.2 HTB Class层级结构

HTB支持树形层级结构,每个Class可以有子Class。根Class连接到一个默认的qdisc,子Class可以有自己的leaf qdisc。典型的三层结构:根Class → 多个部门Class → 各部门内部的子Class。


HTB层级结构示例:
                 Root (1:)
                 rate=1G ceil=1G
                 │
        ┌────────┼─────────┐
        │        │         │
      1:1      1:2       1:3
    研发部    市场部      默认
rate=500M   rate=300M   rate=100M
ceil=800M   ceil=500M   ceil=1G
        │                 │
    ┌───┴───┐            ...
    │       │
  1:10    1:11
 前端组   后端组
rate  rate
200M   300M
ceil   ceil
400M   500M

三、FQ_CODEL:公平队列与智能拥塞控制的完美结合

3.1 CODEL算法:基于延迟的AQM

FQ_CODEL是Linux内核默认推荐的qdisc,它结合了Fair Queuing(FQ,公平队列)和Controlled Delay(CODEL,受控延迟)两大技术。CODEL解决的是Bufferbloat问题——网络设备中过大的缓冲区导致排队延迟从毫秒级暴涨到秒级。

CODEL的核心参数:

  • target:目标排队延迟(默认5ms)。如果某个流的排队延迟超过target,CODEL认为该流占用了过多带宽
  • interval:滑动窗口(默认100ms)。CODEL不会在一个interval内连续丢包,避免过度惩罚

CODEL的状态机逻辑:


CODEL状态机:
┌─────────────────────────────────────────────┐
│ 1. 计算当前所有流的排队延迟(sojourn time)    │
│ 2. 如果延迟 < target → 状态OK,继续转发      │
│ 3. 如果延迟 > target → 进入DROPPING状态    │
│    3a. 选择"最差流"(延迟最高的流)           │
│    3b. 向该流发送一个包减少信号               │
│    3c. 记录drop_next时间 = now + control_func │
│ 4. 后续包根据drop_next判断是否继续drop        │
│    - 如果延迟仍高 → drop并更新drop_next       │
│    - 如果延迟恢复正常 → 退出DROPPING状态      │
│                                              │
│ control_next = interval / sqrt(drop_count)   │
│ (连续丢包时,间隔缩短,惩罚加速)              │
└─────────────────────────────────────────────┘

3.2 公平队列(FQ)调度

FQ将不同流(flow)的数据包分配到不同的子队列,按轮询(Round-Robin)方式确保每个流获得公平的带宽份额。这里的"流"由五元组(src IP, dst IP, src Port, dst Port, Protocol)或flow hash决定。

FQ还实现了流优先级队列:活跃少于5个包的流进入"新流队列"(New Flow Queue)优先发送,老流进入"旧流队列"(Old Flow Queue)。这确保了短HTTP请求、DNS查询等不会被大文件下载阻塞。

四、CAKE:家庭路由器终极队列算法

4.1 CAKE的设计哲学

CAKE(Common Applications Kept Enhanced)是为解决家庭网络Bufferbloat问题设计的综合性AQM/qdisc。它融合了最先进的流量整形、智能拥塞控制和流量分类能力,被誉为"家庭路由器的最佳默认配置"。

CAKE的核心特性:

  • 自动带宽检测(ACK Filter):自动给予TCP ACK包最高优先级,避免上行带宽饱和时反向ACK被阻塞,导致下行性能骤降
  • 多流公平(Flow Isolation / Host Fairness):支持按源IP/目的IP隔离流,防止单一设备垄断带宽
  • 内置SQM(Smart Queue Management):整合CODEL、FQ和HTB的优点
  • 支持 ack-filter 和 atm/vc 补偿模式,适配ADSL/VDSL等链路环境

4.2 CAKE的参数调优

# 查看CAKE可用参数
tc qdisc add dev eth0 root cake bandwidth 100mbit

# 常用参数组合
cake [bandwidth RATE]          # 链路带宽(必须精确设置)
cake [ack-filter]              # 自动优先ACK包(推荐启用)
cake [dual-srchost]            # 基于源IP的流隔离
cake [dual-dsthost]            # 基于目的IP的流隔离
cake [nat]                     # 启用NAT感知(根据连接跟踪匹配)
cake [docsis]                  # DOCSIS链路模式(补偿FEC开销)
cake [pppoe-ptm]               # PPPoE + 分片补偿
cake [atm]                     # ATM信元补偿(53字节/信元)
cake [mpu N]                   # 最小分组单元大小
cake [ethernet]                # 默认以太网帧开销
cake [raw]                     # 无链路开销补偿

五、实战案例:完整的家庭网关QoS配置

5.1 场景分析

假设一个典型的家庭宽带场景:

  • 下行带宽:500Mbps,上行带宽:50Mbps
  • 需要保障:工作电脑 > 视频会议设备 > 普通设备 > IoT设备
  • 目标:消除Bufferbloat,游戏延迟低于20ms,视频会议不卡

5.2 出站流量配置(eth0 为WAN口)

#!/bin/bash
# ============================================
# 家庭网关QoS出站配置(eth0下行)
# ============================================

WAN="eth0"
DOWNLINK="500mbit"
UPLINK="50mbit"

# 清除旧配置
tc qdisc del dev $WAN root 2>/dev/null

# ========== 出站方向(eth0出队,控制上传) ==========
tc qdisc add dev $WAN root handle 1: htb default 40

# 根类 - 总带宽限制
tc class add dev $WAN parent 1: classid 1:1 htb rate $UPLINK ceil $Uplink

# 1:10 高优先级 - 视频会议(Zoom/Teams/TcMeet)
tc class add dev $WAN parent 1:1 classid 1:10 htb \
    rate 15mbit ceil $Uplink burst 15k cburst 15k prio 1
tc qdisc add dev $WAN parent 1:10 handle 10: \
    cake bandwidth 20mbit ack-filter nat dual-srchost

# 1:20 中优先级 - 网页浏览与普通下载
tc class add dev $WAN parent 1:1 classid 1:20 htb \
    rate 20mbit ceil $Uplink burst 15k cburst 15k prio 2
tc qdisc add dev $WAN parent 1:20 handle 20: \
    cake bandwidth 25mbit ack-filter dual-srchost

# 1:30 低优先级 - BT下载/更新包
tc class add dev $WAN parent 1:1 classid 1:30 htb \
    rate 5mbit ceil $Uplink burst 30k cburst 30k prio 4
tc qdisc add dev $WAN parent 1:30 handle 30: \
    cake bandwidth 10mbit ack-filter

# 1:40 默认类 - 其他流量
tc class add dev $WAN parent 1:1 classid 1:40 htb \
    rate 10mbit ceil $Uplink burst 15k cburst 15k prio 3
tc qdisc add dev $WAN parent 1:40 handle 40: \
    cake bandwidth 15mbit ack-filter dual-srchost

# ========== 分类器(使用u32匹配端口) ==========
# 视频会议类
tc filter add dev $WAN parent 1:0 protocol ip prio 1 u32 \
    match ip dport 3478 0xffff flowid 1:10  # STUN
tc filter add dev $WAN parent 1:0 protocol ip prio 1 u32 \
    match ip dport 8801 0xff00 flowid 1:10  # Zoom
tc filter add dev $WAN parent 1:0 protocol ip prio 1 u32 \
    match ip dport 5004 0xfffe flowid 1:10  # Teams media

# 默认类
tc filter add dev $WAN parent 1:0 protocol ip prio 100 \
    match ip src 0.0.0.0/0 flowid 1:40

5.3 入站流量配置(下载方向使用Ingress Policing)

由于入队流量无法直接整形(包已经到达),需要使用ifb(Intermediate Functional Block)设备进行反向重定向,在ifb上为入站流量创建出站队列。

# ============================================
# 入站流量QoS(通过ifb设备将下载流量变成可控的出队)
# ============================================

# 加载ifb模块
modprobe ifb numifbs=1
ip link set dev ifb0 up

# 将eth0的ingress流量重定向到ifb0的egress
tc qdisc add dev $WAN ingress
tc filter add dev $WAN parent ffff: protocol ip prio 1 \
    u32 match u32 0 0 action mirred egress redirect dev ifb0

# 在ifb0上创建HTB + FQ_CODEL
tc qdisc add dev ifb0 root handle 2: htb default 40
tc class add dev ifb0 parent 2: classid 2:1 htb rate $DOWNLINK ceil $DOWNLINK

# 多优先级类(复用出站配置的结构)
tc class add dev ifb0 parent 2:1 classid 2:10 htb \
    rate 150mbit ceil $DOWNLINK burst 100k cburst 100k prio 1
tc qdisc add dev ifb0 parent 2:10 handle 10: \
    cake bandwidth 150mbit ack-filter dual-dsthost

tc class add dev ifb0 parent 2:1 classid 2:20 htb \
    rate 200mbit ceil $DOWNLINK burst 100k cburst 100k prio 2
tc qdisc add dev ifb0 parent 2:20 handle 20: \
    cake bandwidth 250mbit ack-filter dual-dsthost

tc class add dev ifb0 parent 2:1 classid 2:30 htb \
    rate 50mbit ceil $DOWNLINK burst 200k cburst 200k prio 4
tc qdisc add dev ifb0 parent 2:30 handle 30: \
    cake bandwidth 80mbit ack-filter

tc class add dev ifb0 parent 2:1 classid 2:40 htb \
    rate 100mbit ceil $DOWNLINK burst 100k cburst 100k prio 3
tc qdisc add dev ifb0 parent 2:40 handle 40: \
    cake bandwidth 120mbit ack-filter dual-dsthost

# 入站分类器
tc filter add dev ifb0 parent 2:0 protocol ip prio 1 u32 \
    match ip sport 3478 0xffff flowid 2:10

六、TC BPF:用eBPF实现自定义包分类

6.1 BPF Classifier入门

除了u32传统分类器,TC支持BPF程序实现自定义分类逻辑。相比u32,BPF分类器更灵活、更高效,特别适合复杂的流量分类需求。

// tc_bpf_classifier.c - 自定义分类BPF程序
#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include "bpf_helpers.h"

SEC("classifier")
int tc_classify(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;
    
    if (eth->h_proto != htons(ETH_P_IP))
        return TC_ACT_OK;
    
    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end)
        return TC_ACT_OK;
    
    __u32 dst_ip = ip->daddr;
    
    // 将游戏服务器IP映射到高优先级类
    // 假设游戏服务器IP段 1.2.3.0/24
    if ((dst_ip & htonl(0xFFFFFF00)) == htonl(0x01020300)) {
        // 需要将skb重定向到高优先级流量控制类
        // 实际中通过返回classid实现
        // 返回 classid (handle) 1:10 = 0x10000 | 0x10 = 0x10010
        // 注意 TC_ACT_OK 会继续处理,需要使用 skb->tc_classid
        return TC_ACT_OK; // 简化示例
    }
    
    return TC_ACT_OK;
}

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

6.2 编译与加载BPF分类器

# 编译BPF程序(使用clang)
clang -O2 -target bpf -c tc_bpf_classifier.c -o tc_bpf.o

# 通过tc命令加载BPF分类器
tc filter add dev eth0 parent 1:0 bpf obj tc_bpf.o \
    section classifier flowid 1:10

# 或使用direct-action模式(允许drop/redirect等动作)
tc filter add dev eth0 ingress bpf da obj tc_bpf.o \
    section classifier

# 查看已加载的BPF程序
tc filter show dev eth0

七、性能诊断与调优技巧

7.1 Bufferbloat检测方法

Bufferbloat是指在带宽饱和时排队延迟急剧增加的现象。使用flent工具可以精确检测:

# 安装flent
sudo apt install flent

# RRUL测试(Real-Time Response Under Load)
flent rrul -H target_host -t "Bufferbloat测试" -o result.png

# 查看排队延迟统计
tc -s qdisc show dev eth0

# 输出示例(关注backlog和drops):
# qdisc htb 1: root refcnt 9 r2q 10 default 40 direct_packets_stat 0 direct_qlen 1000
#  Sent 123456789 bytes 891234 pkt (dropped 42, overlimits 12 requeues 0)
#  backlog 23456b 156p requeues 0
#
# 解读:dropped=42表示有42个包因超延迟被丢弃
#       backlog=156p表示当前队列中有156个包排队

7.2 CAKE带宽参数精确校准

CAKE的带宽参数必须准确设置,否则会导致带宽浪费或拥塞。校准方法:

# 使用speedtest-cli获取实际带宽
speedtest-cli --simple

# 考虑链路层开销,通常设置为实际测速的95-97%
# 例如测速500Mbps,则设置为:
cake bandwidth 480mbit

# 对于PPPoE链路,增加overhead补偿
cake bandwidth 480mbit ethernet pppoe-ptm mpu 84

# 对于DOCSIS链路
cake bandwidth 480mbit docsis ethernet ack-filter-aggressive

7.3 常见性能问题与解决方案

问题现象诊断方法解决方案
视频会议卡顿tc -s qdisc show 看高优先级类drops确保ACK filter启用,降低总体带宽限制
游戏延迟波动大ping -D host 观察延迟变化启用cake的dual-srchost隔离,避免下载抢占
限速后总带宽不达标对比有/无TC的speedtest结果降低burst/cburst值,或增大interval
特定网站访问慢tc -s filter show 检查分类命中率检查filter规则是否正确匹配目标流量
CPU占用异常高top看内核CPU开销减少flow数量(flows参数),使用host-aware模式

八、TC与TCP BBR的协同优化

TCP BBR(Bottleneck Bandwidth and RTT)与TC流量控制可以协同工作,达到最佳的网络性能。BBR通过主动测量瓶颈带宽和RTT来调整发送速率,而TC队列管理则确保低排队延迟。

重要的协同点:

  • 必须保持BBR pacing rate < TC queue rate,否则BBR会持续填充队列导致BBR无法收敛到低延迟状态
  • CAKE/FQ_CODEL将排队延迟控制在5ms以内,为BBR提供准确的RTT测量
  • 在HTB中为BBR流分配足够的rate和ceil,避免limiting BBR的探测能力
# 启用BBR
echo "net.core.default_qdisc = fq" >> /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control = bbr" >> /etc/sysctl.conf
sysctl -p

# 配合TC使用时的推荐配置
# TC使用HTB + CAKE,CAKE内部target=5ms
# BBR v2可以更高效地配合AQM工作

九、总结与最佳实践

Linux TC是一个功能强大但配置复杂的子系统。通过本文的学习,你应该掌握以下核心要点:

  1. HTB用于分层带宽分配 — 创建树形Class结构,rate保障带宽,ceil允许借用
  2. FQ_CODEL是默认通用qdisc — 消除Bufferbloat,保证流间公平
  3. CAKE是家庭路由器的最佳选择 — 自动ACK过滤 + 流隔离 + 智能AQM
  4. 入站方向使用ifb重定向 — 通过中间设备将ingress转为可整形的egress
  5. BPF分类器实现灵活流量控制 — 用自定义程序替代传统u32 filter

建议在家用OpenWrt或企业级网关中优先使用CAKE qdisc,它涵盖了80%的用例需求。只有在需要复杂的多租户分层带宽分配时,才需要HTB + 自定义Leaf Qdisc的组合方案。最后记住:带宽参数必须精确校准,过高的设置浪费带宽,过低则导致拥塞,这是TC调优的黄金法则。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
/* 跳过导航链接 (无障碍) */ .skip-link { position: absolute; top: -100px; left: 15px; z-index: 99999; padding: 8px 16px; background: #007bff; color: #fff; font-size: 14px; border-radius: 0 0 4px 4px; text-decoration: none; transition: top 0.2s; } .skip-link:focus { top: 0; outline: 3px solid #0056b3; }