网络慢不一定是因为带宽不够。一条 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 + limit | burst 太小丢包、太大失准 |
| 限速 | TBF 简单、HTB 分层 | 多租户用 HTB,叶子挂 fq_codel |
| 抗膨胀 | CoDel 按延迟丢包 | 默认换 fq_codel 或 cake |
| 模拟 | netem 注入延迟丢包 | 弱网压测标配 |
| 排错 | 规则叠加,先 show 再改 | 清空用 tc qdisc del root |
流量控制是网络工程里少数「软件就能显著改变体验」的地方:不需要换硬件,一条 fq_codel 就能把缓冲膨胀带来的几百毫秒延迟降到个位数。理解 qdisc 的三层模型、令牌桶的语义与 AQM 的取舍,就能在带宽、延迟与公平性之间做出有依据的权衡。要把整形与传输层行为联系起来,可以进一步阅读 TCP 拥塞控制算法
;弱网下的多播与实时流量调度则可参考 UDP 与组播
。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。