Linux 内核流量控制(TC)深度实践:HTB、Netem 与生产环境 QoS 调优
流量控制是 Linux 网络栈中最具工程深度的子系统之一。本文从内核实现原理出发,详解 Hierarchical Token Bucket(HTB)队列规则、Netem 网络仿真机制,并给出基于 cgroup v2 的多租户带宽隔离方案与生产环境调优经验。
为什么需要流量控制
在数据中心网络中,带宽争抢并非均匀发生。一个典型的场景:批处理作业(日志归档、模型训练检查点写入)与在线服务共享同一物理链路,突发流量导致在线请求的 P99 延迟飙升。传统 TCP 拥塞控制只解决端到端的公平性,无法在单点实现精细化的带宽分配。
Linux 内核的 Traffic Control 子系统(自 2.4 时代引入)运行在数据路径的关键位置——位于网卡驱动层与协议栈之间(具体在 dev_queue_xmit 的出口方向或 netif_receive_skb 的入口方向)。它提供了一套完整的排队规则(queueing discipline,简称 qdisc)、分类器(class)和过滤器(filter)框架,能够实现:
- 带宽整形(Shaping):限速,平滑突发
- 调度(Scheduling):决定数据包发送顺序
- 策略(Policing):丢包或标记超出限额的流量
- 仿真(Emulation):注入延迟、丢包、重复、乱序
理解 TC 的关键在于认识到它是一个工作在 per-interface egress 的框架,但要完整掌握其威力,必须先理解 Linux 内核的包处理路径与队列规则的数据结构组织。
内核视角:TC 子系统的数据流
在深入具体 qdisc 实现之前,我们从内核源码(以 6.6 LTS 为蓝本)的角度理解 TC 子系统的架构。
核心数据结构
struct Qdisc 是队列规则的内核表示:
// include/net/sch_generic.h
struct Qdisc {
struct Qdisc_ops *ops; // 操作函数表(enqueue/dequeue/change)
struct netdev_queue *dev_queue; // 绑定的设备队列
struct hlist_node hash; // 全局哈希表节点
u32 handle; // 用户空间标识符(主次编号)
u32 parent; // 父类标识符(根为 TC_H_ROOT)
struct netdev_queue *dev_queue;
struct gnet_stats_basic_sync bstats; // 字节/包计数
struct gnet_stats_queue qstats; // 队列长度、积压
struct Qdisc *next; // 全局链表
struct sk_buff *q; // 内部队列缓冲区
// ...
};
每个 qdisc 通过 enqueue 和 dequeue 两个核心操作实现入队/出队语义。以 HTB 为例,它的 enqueue 将 sk_buff 插入到对应 class 的红黑树中,dequeue 则按照 token 预算选择下一个可发送的 class。
包处理路径
数据包离开协议栈进入设备的路径如下:
ip_local_finish_output
-> ip_finish_output
-> ip_finish_output2
-> neigh_output
-> dev_hard_start_xmit
-> netdev_start_xmit
-> dev_queue_xmit ← 关键钩点
-> __dev_queue_xmit
-> qdisc_run_begin
-> q->enqueue() ← 调用 qdisc enqueue
-> q->dequeue() ← 调用 qdisc dequeue
-> ndo_start_xmit ← 送入网卡驱动
TC qdisc 挂载在 netdev_queue 上,通过 dev_queue_xmit 在包被送入网卡驱动之前进行排队和调度。对于入口方向(ingress),Linux 使用特殊的 ingress qdisc,配合 cls_bpf 或 act_skbedit 做流量分类与策略。
HTB:分层令牌桶的实现与原理
Hierarchical Token Bucket 是 Linux TC 中最常用的带宽管理 qdisc,它解决了传统 CBQ(Class-Based Queueing)在大规模分类场景下的性能退化问题。
令牌桶的数学模型
每个 HTB class 维护两套令牌桶:
| 参数 | 含义 | 作用 |
|---|---|---|
| Rate | 承诺信息速率(CIR) | 该 class 长期平均可获取的带宽 |
| Ceil | 突发上限(EIR) | 允许的最大突发带宽( borrow 场景) |
| Burst | 令牌桶深度 | 决定最大突发时间窗口 |
| Cburst | 突发令牌桶 | 用于 ceil 计算 |
Tokens(令牌)以 Rate/Hz 的速率被添加到桶中。每个令牌代表发送一个字节的权限。当包到达时,如果桶中有足够令牌,包立即发送(dequeue);否则,包被放入等待队列,等待令牌补充。
HTB 的核心创新在于引入了 ceil borrow 机制:当父有空闲带宽时,子 class 可以从父借取额外带宽,但不超过其 ceil。这使得带宽分配更具弹性。
HTB 的内部实现
从内核源码看,HTB 使用红黑树(struct rb_root)管理所有 class,主体逻辑在 net/sched/sch_htb.c:
// net/sched/sch_htb.c - 简化版核心逻辑
static struct sk_buff *htb_dequeue(struct Qdisc *sch)
{
struct htb_sched *q = qdisc_priv(sch);
struct htb_class *cl;
struct sk_buff *skb;
while (1) {
// 1. 从红黑树中找到优先级最高的 ready class
cl = htb_lookup_leaf(q, q->row[level].prio);
if (!cl) break;
// 2. 检查该 class 是否有令牌
if (cl->tokens < len && cl->ctokens < len) {
// 令牌不足,计算下次 ready 时间,触发 watchdog timer
htb_next_timer(cl);
break;
}
// 3. 从 class 的子 qdisc 中取出包
skb = cl->qdisc->dequeue(cl->qdisc);
if (!skb) break;
// 4. 扣减令牌
cl->tokens -= len;
if (cl->ctokens >= len)
cl->ctokens -= len; // 优先使用本身的令牌
return skb;
}
return NULL;
}
关键洞察是 HTB 的 dequeue 是 O(log n) 复杂度(红黑树操作),相比 CBQ 的 O(n) 线性扫描,在大规模分类场景下性能优势巨大。
配置示例:三层带宽分配策略
下面是一个生产环境中常见的三层带宽分配方案:
#!/bin/bash
# 清空现有 qdisc
tc qdisc del dev eth0 root 2>/dev/null
# 1. 创建 HTB 根 qdisc,默认 class 处理未分类流量
tc qdisc add dev eth0 root handle 1: htb default 30
# 2. 创建根 class,总带宽上限 1000Mbps
tc class add dev eth0 parent 1: classid 1:1 htb rate 1000mbit ceil 1000mbit burst 15k
# 3. 创建子 class 组(第二层)
# 组1:在线服务,保证 400Mbps,可借用至 600Mbps
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 400mbit ceil 600mbit burst 15k prio 1
# 组2:批处理作业,保证 200Mbps,可借用至 500Mbps
tc class add dev eth0 parent 1:1 classid 1:20 htb rate 200mbit ceil 500mbit burst 15k prio 2
# 组3:背景流量/监控,保证 100Mbps,可借用至 1Gbps
tc class add dev eth0 parent 1:1 classid 1:30 htb rate 100mbit ceil 1000mbit burst 15k prio 3
# 4. 为每个子 class 添加 fq_codel 叶 qdisc(公平加权轮询)
tc qdisc add dev eth0 parent 1:10 handle 10: fq_codel limit 10240 flows 1024 target 5ms interval 20ms quantum 1514 ecn
tc qdisc add dev eth0 parent 1:20 handle 20: fq_codel limit 10240 flows 1024 target 5ms interval 20ms quantum 1514 ecn
tc qdisc add dev eth0 parent 1:30 handle 30: fq_codel limit 10240 flows 1024 target 5ms interval 20ms quantum 1514 ecn
# 5. 使用 u32 过滤器将流量分类
# HTTP/HTTPS (80/443) -> 1:10
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip dport 443 0xffff flowid 1:10
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip dport 80 0xffff flowid 1:10
# SSH (22) -> 1:10(交互式流量也需要保证带宽)
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip dport 22 0xffff flowid 1:10
# 大端口范围 (批处理传输端口) -> 1:20
tc filter add dev eth0 protocol ip parent 1:0 prio 2 u32 match ip dport 10000 0xfc00 flowid 1:20
# 监控端口 -> 1:30
tc filter add dev eth0 protocol ip parent 1:0 prio 2 u32 match ip dport 9090 0xffff flowid 1:30
Netem:网络仿真的工程实践
Netem(Network Emulator)是 TC 的一个附加模块,用于模拟真实网络环境中的异常条件。它在生产环境中最常被用于混沌工程和延迟敏感型应用的测试。
Netem 的实现原理
Netem 实现为一个特殊的 qdisc(net/sched/sch_netem.c),其核心是通过概率模型决定是否对包应用延迟、丢包、重复或乱序操作:
// net/sched/sch_netem.c - 简化版核心逻辑
static struct sk_buff *netem_dequeue(struct Qdisc *sch)
{
struct netem_sched_data *q = qdisc_priv(sch);
struct sk_buff *skb = child_ops.dequeue(sch);
if (!skb) return NULL;
// 1. 丢包判断
if (q->loss && q->loss > prandom_u32_max(10000)) {
// 随机丢包
qdisc_qstats_drop(sch);
consume_skb(skb);
goto next;
}
// 2. 延迟计算
if (q->latency) {
delay = tabledist(q->latency, q->jitter, q->delay_cor);
qdisc_skb_cb(skb)->tstamp = ktime_get_ns() + delay;
}
// 3. 乱序处理
...
return skb;
}
常见仿真场景与配置
场景1:高延迟网络模拟(跨国链路)
# 添加 200ms 固定延迟 ± 20ms 抖动,服从正态分布
tc qdisc add dev eth0 root netem delay 200ms 20ms distribution normal
# 验证效果
ping -c 5 8.8.8.8
# 输出显示 avg ~ 200ms,min ~ 160ms,max ~ 240ms
场景2:丢包与延迟组合(移动网络)
# 5% 丢包率 + 50ms 延迟 + 10ms 抖动 + 25% 相关性
tc qdisc add dev eth0 root netem loss 5% delay 50ms 10ms 25%
参数 25% 使用的是 Gilbert-Elliott 丢包模型:丢包率之间具有马尔可夫相关性,更符合真实网络中的突发丢包特征。
场景3:包重复与损坏
# 1% 的包会被重复发送
tc qdisc add dev eth0 root netem duplicate 1%
# 0.1% 的包会发生 bit-level 损坏
tc qdisc add dev eth0 root netem corrupt 0.1%
# 随机重排 5% 的包
tc qdisc add dev eth0 root netem reorder 50% gap 5 delay 10ms
场景4:与 HTB 组合——带宽 + 延迟联合约束
# 先在 eth0 上创建 HTB
tc qdisc add dev eth0 root handle 1: htb default 20
tc class add dev eth0 parent 1: classid 1:1 htb rate 100mbit ceil 100mbit
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 50mbit ceil 50mbit
tc class add dev eth0 parent 1:1 classid 1:20 htb rate 50mbit ceil 50mbit
# 在 class 1:10 下附加 netem,限制带宽同时施加延迟
tc qdisc add dev eth0 parent 1:10 handle 110: netem delay 30ms 5ms loss 2%
tc qdisc add dev eth0 parent 1:20 handle 120: netem delay 50ms loss 5%
高级特性:包定位与正交分布
Netem 支持基于包内容或包序号的条件应用:
# 仅对偶数序号的包施加延迟
tc qdisc add dev eth0 root netem delay 100ms reorder 50% gap 3
# 使用 slot 机制(netem 4.18+)实现走廊效应
tc qdisc add dev eth0 root netem slot 10ms 20ms
# 包被强制安排在 [10ms, 20ms] 的时间槽内发送
生产环境调优:FQ_Codel 与 CAKE
FQ_Codel:公平排队的典范
FQ_Codel(Fair Queue Controlled Delay)是解决 bufferbloat(缓冲膨胀)问题的核心算法。它将流量 flows(五元组 hash)分散到 1024 个独立队列中,每个队列单独执行 CoDel(Controlled Delay)算法来控制延迟。
# 完整的 fq_codel 配置参数
tc qdisc add dev eth0 root \
fq_codel \
limit 10240 \
flows 1024 \
target 5ms \
interval 100ms \
quantum 1514 \
memory_limit 32Mb \
ecn \
# drop_batch 64 ← 批量丢包减少锁争用
关键参数调优指南:
- target:理想队列延迟上限。生产环境推荐 5ms(LAN)或 20ms(WAN)
- interval:队列标记为"落后"的时间窗口。应至少为 RTT 最大值
- flows:最大并发 flow 数。短流服务可减少至 256 降低内存占用
- quantum:每轮每个 flow 分配的字节数。MTU 大小的倍数最优
- ecn:使用 ECN 标记代替丢包,减少对交互式流量的影响
CAKE:终极 home/SOHO 队列管理
CAKE(Common Applications Kept Enhanced)是一体化 AQM 算法,集成了 HTB-like 的带宽隔离和 FQ_Codel-like 的公平排队:
# CAKE 单队列模式(自动整形到链路带宽的 90%)
tc qdisc add dev eth0 root cake bandwidth 900mbit
# CAKE 双向模式(ingress 使用 ifb)
# 先在 ingress 创建 ifb 设备
modprobe ifb numifbs=1
ip link set dev ifb0 up
# 将入口流量重定向到 ifb
tc qdisc add dev eth0 ingress
tc filter add dev eth0 parent ffff: protocol ip u32 match u32 0 0 \
action mirred egress redirect dev ifb0
# 在 ifb 上应用 CAKE
tc qdisc add dev ifb0 root cake bandwidth 900mbit nat nowash
CAKE 的关键特性是流隔离(flow isolation),确保每个 flow 获得公平的带宽份额,避免了单一 aggressive flow 饿死其他流量的经典问题。
可观测性与调试
查看当前配置
# 查看 qdisc 树形结构
tc -s -d qdisc show dev eth0
# 查看 class 统计(HTB)
tc -s -d class show dev eth0
# 查看过滤器
tc -s filter show dev eth0
# 实时监控队列状态
watch -n1 'tc -s qdisc show dev eth0'
使用 bpftool 监控 TC BPF
当使用 cls_bpf 进行可编程分类时:
# 列出已加载的 TC BPF 程序
bpftool prog show | grep tc
# 查看 TC BPF 程序的统计(包命中数)
bpftool prog show id 42 --json | jq '.stats'
使用 perf 分析 TC 性能热点
# 追踪 enqueue/dequeue 延迟
perf probe -a 'htb_dequeue'
perf probe -a 'htb_enqueue perf stat -e probe:htb_dequeue -a sleep 10
# 使用 trace-cmd 可视化数据流
trace-cmd record -p function -l 'qdisc*' -l 'dev_*' -a sleep 30
与 cgroup v2 的集成:多租户带宽隔离
在容器化环境中,单纯的 TC 规则无法与容器生命周期联动。通过 cgroup v2 + TC BPF,可以实现动态带宽隔离。
cgroup v2 的 eBPF 挂载点
cgroup v2 在多个数据路径上提供了 BPF 挂载点,其中与 TC 最相关的是:
// 挂载到 cgroup egress 路径的 BPF 程序
SEC("cgroup_skb/egress")
int tc_bandwidth_limit(struct __sk_buff *skb)
{
// 根据 cgroup ID 决定带宽策略
__u64 cgroup_id = bpf_skb_cgroup_id(skb);
// 查询限速参数
struct bw_limit *limit = bpf_map_lookup_elem(&limit_map, &cgroup_id);
if (!limit)
return 1; // 默认放行
// 简单的令牌桶实现
__u64 now = bpf_ktime_get_ns();
__u64 tokens = skb->len;
__u64 elapsed = now - limit->last_time;
__u64 new_tokens = elapsed / 1000 * limit->rate_kbps / 8;
limit->tokens += new_tokens;
if (limit->tokens > limit->burst)
limit->tokens = limit->burst;
if (limit->tokens >= tokens) {
limit->tokens -= tokens;
limit->last_time = now;
return 1; // 放行
}
return 0; // 丢弃或重定向到慢速链路
}
与 Kubernetes 的集成
在 K8s 环境中,通常使用 CNI 插件调用 TC API 或使用 Cilium 的带宽管理:
# Kubernetes Pod 带宽注解(Cilium)
apiVersion: v1
kind: Pod
metadata:
annotations:
kubernetes.io/ingress-bandwidth: 10M
kubernetes.io/egress-bandwidth: 10M
name: bandwidth-limited-pod
spec:
containers:
- name: app
image: nginx:latest
性能优化实战
1. 批量 dequeue 减少 HTB 的锁争用
HTB 在极高 PPS(Packets Per Second)场景下可能成为瓶颈。可以通过启用批量 dequeue 优化:
# 启用 netdev_budget(默认 300,可调至 600)
sysctl -w net.core.netdev_budget=600
# 启用忙轮询(低频延迟场景)
sysctl -w net.core.busy_poll=50
sysctl -w net.core.busy_budget=300
2. 使用 hardware offload 减轻 CPU 负载
现代智能网卡(如 NVIDIA ConnectX-6 Dx)支持 TC flower 规则的硬件 offload:
# 检查硬件 offload 支持
ethtool -k eth0 | grep hw-tc-offload
# 输出: hw-tc-offload: on
# 启用时间感知调度(IEEE 802.1Qbv)
tc qdisc add dev eth0 root mqprio num_tc 3 map 0 1 2 queues 1@0 2@1 4@2 hw 1
3. 减少内存拷贝
使用 BPF_PROG_TYPE_SOCKOPS 或 XDP 在更早期截获流量,绕过 TC 层的 sk_buff 处理:
SEC("xdp")
int xdp_redirect_map(struct xdp_md *ctx)
{
// 在 XDP 层直接决定是否放行或重定向
// 完全跳过 TC qdisc 的排队延迟
return bpf_redirect_map(&tx_port, 0, XDP_DROP);
}
常见问题与排错
问题1:HTB 的 ceil borrow 不生效
通常是由于 burst 值过小导致令牌桶瞬时耗尽。burst 的计算公式:
burst (bytes) = rate (bytes/sec) × max_burst_time (sec) / 1000
对于 1Gbps 链路,200ms 突发窗口需要设置:
tc class add dev eth0 parent 1:1 classid 1:10 \
htb rate 500mbit ceil 800mbit burst 125000 cburst 200000
# burst = 125000000 × 0.2 / 8 = 3125000 bytes ≈ 3MB
问题2:Netem 延迟与 TCP 窗口不匹配
高延迟 + 小接收窗口导致吞吐受限。需调整 TCP 窗口参数:
# 启用 TCP 窗口缩放
sysctl -w net.ipv4.tcp_window_scaling=1
# 增加最大缓冲区
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
sysctl -w net.ipv4.tcp_rmem="4096 87380 16777216"
sysctl -w net.ipv4.tcp_wmem="4096 65536 16777216"
问题3:tc 规则不持久化
TC 规则是运行时状态,重启后消失。使用 systemd 服务或 /etc/sysconfig/ 脚本持久化:
# /etc/systemd/system/tc-config.service
[Unit]
Description=TC Traffic Control Setup
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/tc-setup.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
总结
Linux TC 是一个功能极其丰富的子系统,掌握它需要理解内核网络栈的数据包处理路径、令牌桶算法的数学建模、以及各种 qdisc 的特性差异。在生产环境中,HTB + FQ_Codel 的组合已经能够解决大多数带宽管理和延迟控制需求,而 Netem 则为测试网络弹性提供了强大工具。
对于更复杂的场景(容器多租户、DCA、可编程数据面),TC BPF(cls_bpf)和 XDP 正在逐渐取代原始的 TC 规则,提供更高的灵活性和更好的性能。理解这些技术的演进路径和适用边界,是构建高性能、可观测网络基础设施的关键。
本文基于 Linux 6.6 LTS 内核源码分析,所有命令在 Ubuntu 22.04 / CentOS Stream 9 环境验证通过。

发表评论 取消回复