前面几章你能配接口、配路由、配防火墙——决定包"走不走、走哪"。这章是最后一块:决定包"以什么速率、什么顺序走"。这就是流量控制(traffic control,tc)。它在每个接口的出口管着一个队列,能限速、能给特定流量让路、还能治"网卡明明没满却很卡"的玄学问题(bufferbloat)。还记得 0.4 的拥塞控制吗?那是 TCP 在端上的自我克制;tc 是路由器 / 主机在中间对队列的主动管理——两者一起决定你的网络体验。
tc(traffic control):Linux 的流量控制子系统,核心是 qdisc(队列规则)——每个接口的出口都挂着一个 qdisc,决定排队的包以什么顺序、什么速率发出,以及何时丢弃。
1tc:出口的包调度器
一个常被忽略的事实:包不是"算好路由就立刻发出去"。在真正离开网卡前,它进入接口出口(egress)的一个队列。当链路繁忙、发送速度跟不上产生速度时,包就在这里排队。tc 管的就是这个队列。为什么需要管:
- 限速:把某个接口 / 某类流量限制在一定带宽(比如给访客网络限速);
- 优先级 / QoS:让低延迟流量(视频通话、游戏、DNS)优先于大流量下载;
- 治 bufferbloat:队列太大会让延迟暴涨(第 4 节)。
tc 主要作用在 egress(出口)。ingress(入口)能做的有限(包已到本机,难"不收"),所以"入口限速"通常在上游设备的出口做,或用特殊手段(ifb)。本章聚焦 egress。
把一个包的命运拆开三段,正交互不干扰:路由(0.3 / 1.2)决定"往哪个接口出"、防火墙(1.3)决定"放不放行"、tc 决定"以什么速率 / 顺序从这个接口出"。前面几章管前两段,这章补上最后一段。
2qdisc:流量控制的核心
qdisc(queueing discipline,排队规则)是挂在接口上的算法,决定队列里的包怎么调度。每个接口默认就有一个——现代 Linux 通常是 fq_codel(后面讲),老系统是 pfifo_fast。看一眼:
tc qdisc show dev eth0 # 看接口当前挂的 qdisc
qdisc 分两大类:
- 无类(classless):一个整体策略,不能再细分。例:
pfifo_fast(先进先出 + 3 个优先带)、fq_codel(智能公平队列)、tbf(令牌桶限速)。 - 有类(classful):能建一棵"类"的树,把流量分到不同类、各给不同带宽 / 优先级。例:
htb(分层令牌桶)——做 QoS 分级的主力。
FIFO 是最朴素的 ArrayDeque;pfifo_fast 是带 3 个优先级的多队列;htb 是一棵带配额的调度树;fq_codel 是"每条流单独排队 + 主动控制延迟"的智能调度器。选哪个 qdisc,就是选这个出口用什么调度算法。
3整形:给带宽设上限
整形(shaping)就是人为给流量设一个速率上限,多出来的包先排队、而不是一股脑发出去。最简单的限速是 tbf(token bucket filter,令牌桶):令牌以固定速率生成,发一个包消耗令牌,没令牌就等——平均速率被限制在令牌生成速率。
# 把 eth0 出口限速到 10mbit
sudo tc qdisc add dev eth0 root tbf rate 10mbit burst 32kbit latency 400ms
tc qdisc show dev eth0
sudo tc qdisc del dev eth0 root # 撤销,恢复默认
要做"不同流量不同限速 / 优先级",用 htb(有类):建一棵类树,父类设总带宽,子类分配各自带宽和优先级,再用 filter 把流量(可按端口、IP、fwmark)归到对应子类。
# 示意:eth0 总 100mbit,分两类——高优先(实时) 30mbit、其余 70mbit
sudo tc qdisc add dev eth0 root handle 1: htb default 20
sudo tc class add dev eth0 parent 1: classid 1:1 htb rate 100mbit
sudo tc class add dev eth0 parent 1:1 classid 1:10 htb rate 30mbit ceil 100mbit prio 0
sudo tc class add dev eth0 parent 1:1 classid 1:20 htb rate 70mbit ceil 100mbit prio 1
# 把去往 443 的 tcp 归到高优先类
sudo tc filter add dev eth0 parent 1: protocol ip prio 1 u32 \
match ip dport 443 0xffff flowid 1:10
注意:整形要"主动限"在你能控制的那一端。家用场景真正的瓶颈在 ISP 链路上,所以路由器通常把上 / 下行整形到"略低于实际带宽",把排队的控制权从运营商那个又大又笨的缓冲区,夺回到自己这个聪明的 qdisc 手里——这正是治 bufferbloat 的关键(下一节)。
filter 可以按 fwmark 分类(… handle 0x1 fw flowid 1:10),而 fwmark 由 nftables 打(1.3)。于是一条链路:nftables 给某类流量打标 → tc 按标记归类限速 / 提优先级。这就是路由器上"游戏 / 视频优先"QoS 的实现路径之一,也再次接上 1.2 的 ip rule、1.3 的 mark——三者共用同一个 fwmark。
4bufferbloat 与现代默认:fq_codel / CAKE
反直觉的问题:很多人以为"缓冲区越大越好,少丢包"。其实过大的缓冲区会让包排很长的队——尤其一个大下载占满链路时,你的小包(DNS、游戏、网页)被堵在它后面,延迟从十几毫秒涨到几百甚至上千毫秒。网卡利用率 100%、不丢包,但体验极差。这就是 bufferbloat(缓冲区膨胀)。
它和 0.4 的关系很深:TCP 拥塞控制靠"丢包 / 延迟"信号判断该不该减速(0.4)。缓冲区过大时,丢包信号被延迟,TCP 迟迟收不到"该慢下来"的提示,继续猛灌,队列继续涨——恶性循环。巨大的缓冲区,破坏了 0.4 那套拥塞反馈。
解法是 fq_codel(fair queuing + controlled delay):
- fair queuing:给每条流单独排队,雨露均沾——大下载不会把小流量饿死(上图下半);
- controlled delay(CoDel):盯着包在队列里待了多久,一旦持续偏高就主动早丢几个包,给 TCP 发"减速"信号,把队列长度压住。
它是现代 Linux 的默认 qdisc,基本零配置就显著改善延迟。CAKE 更进一步:把整形 + fq_codel + 多种优化打包成一体化 qdisc,是家用路由器做 QoS 的首选。
OpenWRT 的 SQM(Smart Queue Management)本质就是 CAKE(或 fq_codel)+ 整形:你填入略低于实际带宽的上下行数值,它把排队控制权从 ISP 那个臃肿缓冲拿回到路由器,bufferbloat 立竿见影地降下来。这是普通人能感知最明显的一项路由器优化,也是 tc 这章最实用的落点;Phase 2 / 3 配 QoS 时会用到它。
想直观感受 bufferbloat:用 Waveform Bufferbloat Test 或 dslreports 之类,先在大下载时测延迟,再开启 SQM 后对比。延迟从几百毫秒掉回几十毫秒,就是 fq_codel / CAKE 在干活。
本章小结
- tc 在接口出口(egress)管一个队列,决定包以什么顺序 / 速率发、何时丢;与路由(往哪出)、防火墙(放不放)正交。
- qdisc 是核心:无类(fq_codel / pfifo_fast / tbf)= 整体策略;有类(htb)= 分流到类树、各给带宽 / 优先级。
- 整形(shaping)= 设速率上限,多余排队;tbf 简单限速,htb 做分级 QoS;filter 可按端口 / IP / fwmark(接 1.3)归类。
- bufferbloat:缓冲区过大 → 延迟暴涨 → 破坏 0.4 的 TCP 拥塞反馈;fq_codel(公平队列 + 主动早丢)/ CAKE 来治。
- OpenWRT 的 SQM = CAKE / fq_codel + 整形,把排队控制从 ISP 缓冲夺回路由器,是最易感知的路由器优化。
动手练习
tc qdisc show dev <你的接口>:看默认 qdisc 是什么(多半是 fq_codel 或 noqueue / pfifo_fast),说出它属于无类还是有类。- 给一个接口加 tbf 限速到 1mbit,用
iperf3或下载测一下实际速率是否被限住;tc qdisc del … root撤销。(别在你正用的主网卡上误操作把自己网络搞慢。) - 思考题:为什么"缓冲区越大越好"是错的?结合 0.4 的拥塞控制说说,过大的队列怎么破坏了 TCP 的减速反馈。
- 进阶:若有 OpenWRT 路由器,装 SQM(
luci-app-sqm),填入略低于实测带宽的上下行值,用在线 bufferbloat 测试对比开启前后的延迟变化。