4. 无类qdisc实战

无类qdisc(classless qdisc)是Linux TC中最容易上手的一类队列规则:它们不提供class子节点,不能通过tc class add继续细分,而是直接挂在某个网络接口的根位置(root)或作为某个类的叶子qdisc使用。正因为结构简单,无类qdisc非常适合用来做“单点行为控制”——要么控制发送顺序,要么控制发送速率,要么控制发送时机。本节围绕五个最常用的无类qdisc展开:pfifo_fast、fq_codel、sfq、tbf、netem,重点讲清楚它们各自“对包做了什么”,以及如何用命令观察这些行为。

一、先建立观察基线:默认qdisc与统计字段

在动手之前,先确认接口当前的qdisc。以eth0为例:

tc qdisc show dev eth0
# 典型输出:
# qdisc pfifo_fast 0: root refcnt 2 bands 3 priomap 1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1

大多数物理网卡的默认根qdisc就是pfifo_fast。它内部有3个band(0、1、2),band 0优先级最高,band 2最低。内核根据skb的priority字段(0~15)查priomap决定入哪个band。默认priomap把priority 6、7映射到band 0,priority 4、5映射到band 1,其余映射到band 2。发送时严格按band 0→1→2的顺序取包,同一band内是FIFO。这意味着:只要band 0有包,band 1和band 2就会被“饿死”。

观察统计用tc -s qdisc show dev eth0,关键字段包括sent(已发送包数/字节)、dropped(丢弃)、overlimits(超限次数)、backlog(当前排队包数/字节)。后续所有qdisc的行为判断,都围绕这几个字段展开。

二、fq_codel:用“流公平+主动队列管理”压低缓冲膨胀

fq_codel(Fair Queueing with Controlled Delay)把进入的包按五元组哈希成多个流队列,每个流独立排队,再以轮询方式公平出队;同时每个流内部运行CoDel算法,当包在队列中的停留时间(sojourn time)持续超过目标值(默认5ms)时,主动丢弃或标记包,从而避免bufferbloat。

tc qdisc replace dev eth0 root fq_codel limit 10240 flows 1024 \
    target 5ms interval 100ms quantum 1514 ecn

tc -s qdisc show dev eth0
# qdisc fq_codel 0: root refcnt 2 limit 10240p flows 1024 quantum 1514
#   target 5ms interval 100ms ecn
#  Sent 1234567 bytes 8901 pkt (dropped 12, overlimits 0 requeues 0)
#  backlog 0b 0p requeues 0
#   maxpacket 1514 drop_overlimit 0 new_flow_count 320 ecn_mark 0
#   new_flows_len 0 old_flows_len 0

观察要点:new_flow_count反映新建流数量,old_flows_len反映当前活跃的“老流”队列数,ecn_mark在启用ecn时统计被标记的包。适用场景:家庭网关、Wi-Fi AP、上行带宽受限的服务器——凡是“多流并发且怕延迟抖动”的地方,fq_codel通常是首选。

三、sfq:随机公平队列,抑制单流霸占

sfq(Stochastic Fairness Queueing)同样按哈希分流,但它的哈希会周期性扰动(perturb),避免固定哈希被恶意流碰撞利用。它不主动丢包,只在队列满时丢弃新到包。

tc qdisc replace dev eth0 root sfq perturb 10 quantum 1514 \
    limit 127 divisor 1024

tc -s qdisc show dev eth0
# qdisc sfq 8001: root refcnt 2 limit 127p quantum 1514b
#   depth 127 divisor 1024 perturb 10sec
#  Sent 987654 bytes 7000 pkt (dropped 0, overlimits 0 requeues 0)
#  backlog 0b 0p requeues 0

与fq_codel相比,sfq没有延迟控制机制,它解决的是“公平性”而非“延迟”。适用场景:多用户共享出口、P2P流量与交互流量混跑、需要防止某条TCP流独占带宽的场合。如果同时关心延迟,应优先选fq_codel。

四、tbf:令牌桶限速,把“平均速率”钉死

tbf(Token Bucket Filter)是经典的整形器。它维护一个令牌桶,令牌以rate速率生成,桶容量为buffer(或burst)。只有拿到令牌的包才能发送,否则排队等待;队列满则丢弃。

tc qdisc replace dev eth0 root tbf rate 10mbit burst 32kbit \
    latency 400ms

tc -s qdisc show dev eth0
# qdisc tbf 8002: root refcnt 2 rate 10Mbit burst 4Kb/8 mpu 0b
#   lat 400.0ms linklayer ethernet
#  Sent 5000000 bytes 4000 pkt (dropped 25, overlimits 1200 requeues 0)
#  backlog 0b 0p requeues 0

关键参数关系:burst至少应能容纳一个最大包(如1514字节≈12kbit),否则小包也会被误丢;latency决定最大排队时延,它和rate共同约束了队列深度。观察overlimits:它表示因令牌不足而进入等待的次数,是限速正在生效的直接证据。适用场景:给某台主机或某个接口做硬性带宽上限,例如把测试机出口限制在10Mbit。

五、netem:人为制造延迟、抖动、丢包与乱序

netem(Network Emulator)不用于“优化”,而用于“模拟恶劣网络”。它可以在发送路径上注入延迟、抖动、丢包、重复、乱序和损坏。

tc qdisc replace dev eth0 root netem delay 100ms 20ms \
    distribution normal loss 2% duplicate 1% reorder 25% 50%

tc -s qdisc show dev eth0
# qdisc netem 8003: root refcnt 2 limit 1000
#   delay 100.0ms 20.0ms distribution normal loss 2% 1%
#   duplicate 1% reorder 25% 50% gap 2
#  Sent 300000 bytes 200 pkt (dropped 4, overlimits 0 requeues 0)
#  backlog 0b 0p requeues 0

参数含义:delay 100ms 20ms表示基础延迟100ms、抖动±20ms;loss 2%表示随机丢包2%;reorder 25% 50%表示25%的包有50%概率被乱序发送。观察dropped可确认丢包是否生效。适用场景:弱网测试、音视频抗抖动验证、TCP拥塞控制算法对比实验。

无类qdisc:入队 → 排队/整形 → 出队 行为对照 应用发包 skb + priority pfifo_fast 3 band 严格优先 band0 可饿死 band2 fq_codel 哈希分流 + CoDel 控延迟,抗 bufferbloat sfq 哈希扰动分流 保公平,不控延迟 tbf 令牌桶整形 rate/burst/latency netem 延迟/抖动/丢包 乱序/重复/损坏 出队 → 网卡驱动 → 物理链路 tc -s qdisc show 关键统计字段 sent 已发送包/字节 dropped 队列满/CoDel 丢弃 overlimits tbf 令牌不足等待 backlog 当前排队包/字节 ecn_mark fq_codel 标记数 选型速查 默认/简单优先 pfifo_fast 低延迟多流 fq_codel 公平共享出口 sfq 硬性限速 tbf 弱网模拟 netem

六、组合使用与验证方法

无类qdisc可以嵌套:例如先挂tbf做限速,再在其下挂netem做延迟模拟,形成“限速+弱网”的复合场景。命令如下:

tc qdisc add dev eth0 root handle 1: tbf rate 5mbit burst 16kbit latency 200ms
tc qdisc add dev eth0 parent 1:1 handle 10: netem delay 50ms loss 1%

tc -s qdisc show dev eth0

验证时建议用ping观察RTT变化、用iperf3观察吞吐是否被限制到目标速率、用tc -s观察dropped和overlimits是否随流量增长。判断标准很直接:如果iperf3稳定在5Mbit附近且overlimits持续增长,说明tbf限速生效;如果ping的RTT从0.2ms跳到50ms左右且抖动明显,说明netem延迟生效。

清理qdisc用tc qdisc del dev eth0 root即可恢复默认。实验时建议在独立网络命名空间(ip netns)或虚拟接口(veth)中进行,避免影响生产流量。

七、常见误区与排障要点

  • 把tbf的burst设得过小:小于一个MTU时,大包永远拿不到足够令牌,表现为“限速远低于rate”。建议burst至少为rate/100与MTU的较大者。
  • 在ingress方向直接挂这些qdisc:无类qdisc默认作用于egress。若要控入向,需要配合ingress qdisc + ifb重定向。
  • fq_codel的target设得过大:例如设成100ms,CoDel几乎不触发,延迟控制失效。默认5ms对多数场景已足够。
  • netem与tbf顺序颠倒:先netem后tbf会导致延迟被整形队列放大,建议先tbf后netem,让延迟作用在已整形的流上。

掌握这五个无类qdisc后,你已经能覆盖“优先级、公平、限速、弱网”四类最常见的单点QoS需求。下一阶段再引入有类qdisc(HTB、CBQ)时,就可以把它们作为叶子挂到类下面,形成更精细的分层控制体系。