流量整形与 TC/qdisc

系统讲解 Linux 流量控制与队列规则:从 qdisc、class、filter 三层架构与令牌桶算法,到 TBF、HTB 的限速整形、fq_codel 与 CAKE 的抗缓冲膨胀,再到用 netem 模拟延迟丢包、用 tc 命令落地配置与验证的完整实践。

网络慢不一定是因为带宽不够。一条 1 Gbps 的链路上,如果缓冲区被塞满,一个交互式请求可能要排在几十毫秒的队列后面——这就是缓冲膨胀(bufferbloat):吞吐不低,延迟却高得离谱。带宽是「管道多粗」,延迟与排队才是「体验好不好」,而后者由 Linux 的流量控制子系统决定。

本文先讲清 Linux TC(Traffic Control)的三层架构——qdisc、class、filter,再拆解令牌桶这个核心算法,然后分别讲限速整形(TBF、HTB)与抗缓冲膨胀(fq_codel、CAKE)两类队列规则,最后用 netem 模拟真实网络条件并给出完整的 tc 配置与验证方法。

一、为什么需要流量整形

1.1 缓冲膨胀的根源

路由器的出方向队列在链路拥塞时会缓存数据包,等待发送。缓存本是好事,但缓存过大就成了坏事:TCP 会一路填满缓冲区才察觉拥塞,导致排队延迟(bufferbloat)飙升。经典现象是「下载大文件时网页打不开」。

缓冲膨胀链路:
  应用 → 大缓冲区(几百 ms 的包)→ 链路
  拥塞时:包在缓冲区排队 → RTT 从 10ms 涨到 500ms
  交互请求被排在队尾 → 卡顿

根因:队列太长 + 没有主动丢包信号

1.2 整形与限速的区别

流量控制里两个常被混用的概念:整形(shaping)是「缓存超出的包,稍后发送」,延迟增加但不丢包;限速(policing)是「直接丢弃或标记超出的包」,不增加延迟但会丢包。二者目标不同,选错会让链路行为完全不同。

手段行为效果适用
整形(shaping)缓存后匀速发送平滑、无丢包、增延迟出方向限速
限速(policing)超出即丢弃/标记无延迟、会丢包入方向限制
丢弃(drop)队满即丢触发 TCP 降速拥塞控制
标记(marking)打 DSCP/优先级供后续调度QoS 分类

二、TC 三层架构:qdisc、class、filter

2.1 三个核心概念

Linux TC 由三个对象协作:qdisc(队列规则)决定「包怎么排队、怎么发出」;class(类)在 qdisc 内部做分层限速;filter(过滤器)决定「哪个包归入哪个类」。出方向(egress)天然受控,入方向(ingress)默认无法整形,需要特殊手段。

TC 对象模型:
  qdisc  : 队列规则,挂在网卡上(每个网卡默认 pfifo_fast)
  class  : 分类,仅在「可分类 qdisc」(HTB、CBQ)内存在
  filter : 分类器,把包映射到某个 class(如按 IP、端口、DSCP)

  层次:
    eth0
     └── qdisc (HTB, root)
          ├── class 1:10  (限 10 Mbit)
          │    └── qdisc (fq_codel)  ← 叶子 qdisc
          └── class 1:20  (限 5 Mbit)
               └── qdisc (fq_codel)

2.2 出方向与入方向的不对称

出方向的包由本机决定何时发送,因此可以在发送前排队整形。入方向的包由对端控制,本机无法让对端等待——只能「先接收再丢弃」,或者用 IFB(Intermediate Functional Block)把入方向流量重定向到一个虚拟出方向接口,从而借用出方向的整形能力。

# 入方向限速的思路:ingress 重定向到 IFB,再在 IFB 上整形
modprobe ifb
ip link set ifb0 up
tc qdisc add dev eth0 handle ffff: ingress
tc filter add dev eth0 parent ffff: protocol ip \
  u32 match u32 0 0 action mirred egress redirect dev ifb0
# 之后在 ifb0 上正常用 HTB 整形

2.3 常用 qdisc 速览

qdisc类型特点
pfifo_fast无类默认,三个优先级带
tbf无类令牌桶限速,简单
htb有类分层限速,支持借用与保障
fq_codel无类公平队列 + CoDel 抗膨胀
cake无类fq_codel 增强,一站式 QoS
netem无类模拟延迟、丢包、乱序

三、令牌桶与核心算法

3.1 令牌桶模型

令牌桶是几乎所有限速算法的基础:桶以固定速率(rate)生成令牌,每个包消耗与其大小相当的令牌;令牌不足则包排队(整形)或丢弃(限速)。桶的容量(burst)决定能容忍多大突发。

令牌桶参数:
  rate  : 令牌生成速率(限速值)
  burst : 桶容量(可突发量)
  limit : 队列长度上限

  行为:
    每 1/rate 时间生成一个字节令牌
    发一个 N 字节的包需消耗 N 个令牌
    令牌足够 → 立即发送
    令牌不足 → 排队等待(tbf)或丢弃

3.2 TBF:最简单的限速

TBF(Token Bucket Filter)只做一件事:把某接口的出方向限到指定速率。它不可分类,配置简单,适合「整条链路限速」的场景。

# 把 eth0 出方向限到 10 Mbit,突发 32kbit,队列 10000 字节
tc qdisc add dev eth0 root tbf \
  rate 10mbit \
  burst 32kbit \
  latency 400ms

# 查看
tc qdisc show dev eth0

# 修改(先删后加)
tc qdisc del dev eth0 root

注意 burst 太小会导致小包也丢,太大则失去限速意义;latency 决定队列最长停留时间,等价于限制缓冲深度。

3.3 HTB:分层限速与借用

HTB(Hierarchical Token Bucket)是生产环境最常用的有类 qdisc。它支持树状分类:每个类有自己的保证速率(rate)与上限速率(ceil),空闲带宽可以被其他类「借用」。这正是「每个租户保底 10M、最多可冲到 100M」这类需求的实现方式。

# 根 qdisc 设总带宽上限
tc qdisc add dev eth0 root handle 1: htb default 30

# 类 1:10 保底 10Mbit,上限 100Mbit
tc class add dev eth0 parent 1: classid 1:10 htb \
  rate 10mbit ceil 100mbit burst 15k

# 类 1:20 保底 20Mbit,上限 100Mbit
tc class add dev eth0 parent 1: classid 1:20 htb \
  rate 20mbit ceil 100mbit burst 15k

# 默认类 1:30
tc class add dev eth0 parent 1: classid 1:30 htb \
  rate 5mbit ceil 100mbit

# 给每个类挂 fq_codel 叶子,抗缓冲膨胀
tc qdisc add dev eth0 parent 1:10 handle 10: fq_codel
tc qdisc add dev eth0 parent 1:20 handle 20: fq_codel
# 用 filter 把流量分到类里
# 按目标网段分类
tc filter add dev eth0 parent 1: protocol ip prio 1 \
  u32 match ip dst 10.0.1.0/24 flowid 1:10

# 按端口分类(SSH 优先)
tc filter add dev eth0 parent 1: protocol ip prio 2 \
  u32 match ip dport 22 0xffff flowid 1:10

3.4 借用的语义

HTB 的 rate 是保证带宽,ceil 是硬上限。当某个类空闲时,它的带宽可以被其他类借用——但借用者不能突破自己的 ceil。这个「保证 + 借用」模型让带宽利用率远高于静态切分。

HTB 带宽分配语义:
  rate : 该类在任何情况下都能拿到的带宽(保证)
  ceil : 该类最多能拿到的带宽(含借用,硬上限)
  父类 rate 之和 = 总保证带宽(建议 = 链路带宽)
  空闲带宽按 ceil 上限被其他类借用

四、主动队列管理与抗缓冲膨胀

4.1 从尾丢弃到 AQM

传统队列是「尾丢弃」(tail drop):队满丢新包。问题在于丢包时队列已经很长,且大量流同时被丢,导致全局同步与延迟飙升。AQM(Active Queue Management,主动队列管理)的核心是「在队列还没满时就发出信号」。

尾丢弃的问题:
  1. 队列长期满 → 排队延迟高(缓冲膨胀)
  2. 多流同时丢包 → 同时降速 → 带宽利用率震荡
  3. 突发流量被整批丢弃

AQM 的思路:提前、按比例丢包,让 TCP 平滑降速

4.2 CoDel 与 fq_codel

CoDel(Controlled Delay)不看队列长度,而看「包在队列里停留了多久」:若某个包停留时间超过目标(默认 5ms)持续一段时间,就开始按比例丢包。fq_codel 在此之上加了公平队列(fq):为每条流分配独立子队列,避免某条流把队列占满。

CoDel 关键参数:
  target  : 目标排队延迟(默认 5ms)
  interval: 观察窗口(默认 100ms)
  行为:包停留 > target 且持续 > interval → 进入丢包模式

fq_codel:
  按五元组哈希分到 1024 个子队列 → 各流公平
  每个子队列跑 CoDel → 谁超时就丢谁
  效果:交互流量不再被大流量饿死
# 把默认队列换成 fq_codel(最常见的抗膨胀配置)
tc qdisc replace dev eth0 root fq_codel

# 查看统计:看 backlog 与 drop
tc -s qdisc show dev eth0

4.3 CAKE:一站式 QoS

CAKE(Common Applications Kept Enhanced)是 fq_codel 的增强版,把限速、公平队列、CoDel、Diffserv 分类揉进一个 qdisc。它对家用/边缘场景特别友好:一条命令就能同时解决缓冲膨胀、上行限速与主机公平。

# 下行 100Mbit、上行 20Mbit 的一站式配置
tc qdisc replace dev eth0 root cake bandwidth 100mbit \
  diffserv4 \
  nat \
  ack-filter

# 参数含义:
#   bandwidth  限速值(CAKE 会自动计算排队延迟)
#   diffserv4  四档优先级(配合 DSCP 标记)
#   nat        识别 NAT 后的内网主机,做每主机公平
#   ack-filter 抑制冗余 ACK,提升上行效率
方案抗膨胀限速公平性适用
pfifo_fast无无优先级带默认
tbf部分有无简单限速
htb + fq_codel强分层按类服务器多租户
cake强有按主机/流家用/边缘

五、netem:模拟真实网络

5.1 模拟延迟、抖动与丢包

netem(Network Emulator)是测试的利器:在本地注入延迟、抖动、丢包、乱序、重复,验证应用在弱网下的表现。它常被用来复现「只在跨洲链路才出现」的问题。

# 固定延迟 100ms
tc qdisc add dev eth0 root netem delay 100ms

# 延迟 100ms ± 20ms 抖动,带相关性
tc qdisc add dev eth0 root netem delay 100ms 20ms 25%

# 丢包 5%
tc qdisc add dev eth0 root netem loss 5%

# 乱序 25%,重复 1%
tc qdisc add dev eth0 root netem reorder 25% 50% duplicate 1%

# 组合:延迟 + 丢包
tc qdisc add dev eth0 root netem delay 50ms loss 2%

5.2 与 HTB 组合模拟弱网链路

把 netem 挂在 HTB 的叶子类下,就能模拟「带宽受限 + 高延迟」的真实链路,是压测与协议调优的标准做法。

# 限速 1Mbit + 100ms 延迟
tc qdisc add dev eth0 root handle 1: htb default 1
tc class add dev eth0 parent 1: classid 1:1 htb rate 1mbit
tc qdisc add dev eth0 parent 1:1 handle 10: netem delay 100ms

# 验证:用 iperf3 看实际吞吐,用 ping 看 RTT 与抖动
iperf3 -c <peer> -t 10
ping -c 20 <peer>

六、排错与验证

6.1 查看与清理

TC 配置是「叠罗汉」式的:每次 add 都往上加一层。忘记删除旧 qdisc 会导致规则不生效或行为异常。排错第一步永远是 tc qdisc show 看清当前栈。

# 查看所有 qdisc、class、filter
tc qdisc show dev eth0
tc class show dev eth0
tc filter show dev eth0

# 带统计(backlog、dropped、overlimits)
tc -s qdisc show dev eth0
tc -s class show dev eth0

# 清空根 qdisc(恢复默认)
tc qdisc del dev eth0 root

6.2 常见故障对照

现象原因排查
限速不生效规则挂在错误方向确认 egress;入方向需 IFB
小包全丢burst 太小调大 burst
延迟异常高队列过长/未用 AQM换 fq_codel,检查 latency
分类不生效filter 优先级或匹配写错tc -s filter show 看命中计数
重启后失效配置未持久化写入 systemd 单元或 rc.local

6.3 与拥塞控制的协同

流量整形在链路层制造排队,而拥塞控制在传输层决定发送速率。二者必须协同:若整形用尾丢弃,会与 TCP 的拥塞控制「打架」,出现锯齿;改用 fq_codel 后,提前丢包信号让 TCP 平滑降速。理解这层交互,可以结合 TCP 拥塞控制对 BBR、CUBIC 的讨论,并用抓包与延迟测量手段来验证整形效果。

6.4 让配置持久化

tc 命令配置的规则在重启后会全部消失,生产环境必须持久化。最简单的方式是写成一个幂等脚本,用 systemd 单元在接口就绪后执行;脚本开头先 tc qdisc del ... root 清空,避免规则叠加。

#!/usr/bin/env bash
# /usr/local/sbin/tc-setup.sh —— 幂等应用整形规则
set -e
DEV=eth0
tc qdisc del dev $DEV root 2>/dev/null || true

tc qdisc add dev $DEV root handle 1: htb default 30
tc class add dev $DEV parent 1: classid 1:10 htb rate 10mbit ceil 100mbit
tc class add dev $DEV parent 1: classid 1:20 htb rate 20mbit ceil 100mbit
tc class add dev $DEV parent 1: classid 1:30 htb rate 5mbit  ceil 100mbit
tc qdisc add dev $DEV parent 1:10 handle 10: fq_codel
tc qdisc add dev $DEV parent 1:20 handle 20: fq_codel
tc filter add dev $DEV parent 1: protocol ip prio 1 \
  u32 match ip dst 10.0.1.0/24 flowid 1:10
echo "tc rules applied on $DEV"
# /etc/systemd/system/tc-setup.service
[Unit]
Description=Apply traffic shaping rules
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/tc-setup.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

6.5 与 ECN 的协同

AQM 除了丢包,还可以用 ECN(显式拥塞通知)标记而不是丢弃:队列将满时把包的 ECN 位打上 CE,接收方回送 ECE,发送方据此降速而不必丢包。启用 ECN 需要两端与中间设备都支持,能进一步降低延迟与重传。

# 在 fq_codel 上启用 ECN
tc qdisc replace dev eth0 root fq_codel ecn

# 系统级开关(发送方使用 ECN)
sysctl -w net.ipv4.tcp_ecn=1
拥塞信号机制特点
丢包队列满即丢通用,但引发重传
ECN标记 CE 位无丢包,需端到端支持
CoDel 提前丢按排队延迟丢抗缓冲膨胀

七、小结

主题核心要点落地建议
架构qdisc/class/filter 三层出方向整形,入方向用 IFB
令牌桶rate + burst + limitburst 太小丢包、太大失准
限速TBF 简单、HTB 分层多租户用 HTB,叶子挂 fq_codel
抗膨胀CoDel 按延迟丢包默认换 fq_codel 或 cake
模拟netem 注入延迟丢包弱网压测标配
排错规则叠加,先 show 再改清空用 tc qdisc del root

流量控制是网络工程里少数「软件就能显著改变体验」的地方:不需要换硬件,一条 fq_codel 就能把缓冲膨胀带来的几百毫秒延迟降到个位数。理解 qdisc 的三层模型、令牌桶的语义与 AQM 的取舍,就能在带宽、延迟与公平性之间做出有依据的权衡。要把整形与传输层行为联系起来,可以进一步阅读 TCP 拥塞控制算法 ;弱网下的多播与实时流量调度则可参考 UDP 与组播 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「network」更多文章

  1. MPTCP 与多路径传输
  2. 网络命名空间与容器网络底层
  3. WireGuard 与现代 VPN