9.5. 分类排队规则

如果您有不同类型的流量需要区别对待,分类排队规则非常有用。其中一种分类排队规则称为“CBQ”,“基于类的排队”,它被广泛提及,以至于人们将基于类的排队完全等同于 CBQ,但事实并非如此。

CBQ 只是这个领域中最老的成员——也是最复杂的一个。它可能并不总是能满足您的需求。对于许多受“sendmail 效应”影响的人来说,这可能会让他们感到震惊,“sendmail 效应”告诉我们,任何没有文档的复杂技术一定是最好的。

稍后将详细介绍 CBQ 及其替代方案。

9.5.1. 分类排队规则和类中的数据流

当流量进入分类排队规则时,它需要被发送到其中的任何一个类——它需要被“分类”。为了确定如何处理数据包,需要查阅所谓的“过滤器”。重要的是要知道,过滤器是从排队规则内部调用的,而不是相反!

附加到该排队规则的过滤器然后返回一个决策,排队规则使用它将数据包排队到其中一个类中。每个子类可能会尝试其他过滤器,以查看是否适用进一步的指令。如果没有,该类会将数据包排队到它包含的排队规则中。

除了包含其他排队规则外,大多数分类排队规则还执行整形。这对于执行数据包调度(例如使用 SFQ)和速率控制非常有用。在您拥有高速接口(例如,以太网)到较慢设备(电缆调制解调器)的情况下,您需要这样做。

如果您只运行 SFQ,则什么也不会发生,因为数据包进出您的路由器而没有延迟:输出接口比您的实际链路速度快得多。那时没有队列可以调度。

9.5.2. 排队规则族:根、句柄、同级和父级

每个接口都有一个出口“根排队规则”,默认情况下是前面提到的无类 pfifo_fast 排队规则。可以为每个排队规则分配一个句柄,以供以后的配置语句引用该排队规则。除了出口排队规则外,接口还可以有一个入口,用于监管进入的流量。

这些排队规则的句柄由两部分组成,一个主号码和一个次号码。习惯上将根排队规则命名为“1:”,这等于“1:0”。排队规则的次号码始终为 0。

类需要与其父类具有相同的主号码。

9.5.2.1. 如何使用过滤器对流量进行分类

回顾一下,一个典型的层次结构可能如下所示
                    root 1:
                      |
                    _1:1_
                   /  |  \
                  /   |   \
                 /    |    \
               10:   11:   12:
              /   \       /   \
           10:1  10:2   12:1  12:2

但不要让这棵树欺骗了您!您*不*应该想象内核位于树的顶端,网络位于下方,事实并非如此。数据包在根排队规则处排队和出队,这是内核唯一与之对话的东西。

一个数据包可能会像这样在一个链中被分类

1: -> 1:1 -> 12: -> 12:2

数据包现在驻留在附加到类 12:2 的排队规则中的队列中。在本例中,一个过滤器被附加到树中的每个“节点”,每个节点选择一个要走的 branch。这可能是有意义的。但是,这也可能

1: -> 12:2

在这种情况下,附加到根的过滤器决定将数据包直接发送到 12:2。

9.5.2.2. 如何将数据包出队到硬件

当内核决定需要提取数据包发送到接口时,根排队规则 1: 收到一个出队请求,该请求被传递到 1:1,然后依次传递到 10:、11: 和 12:,它们各自查询它们的同级,并尝试从它们那里 dequeue()。在本例中,内核需要遍历整个树,因为只有 12:2 包含数据包。

简而言之,嵌套类只与其父排队规则对话,从不与接口对话。只有根排队规则会被内核出队!

这样做的结果是,类的出队速度永远不会超过其父类允许的速度。这正是我们想要的:这样我们就可以在内部类中使用 SFQ,它不进行任何整形,只进行调度,并有一个整形外部排队规则,它进行整形。

9.5.3. PRIO 排队规则

PRIO 排队规则实际上并不整形,它只是根据您配置过滤器的方式细分流量。您可以将 PRIO 排队规则视为一种增强型的 pfifo_fast,其中每个频带都是一个单独的类,而不是一个简单的 FIFO。

当数据包排队到 PRIO 排队规则时,会根据您给出的过滤器命令选择一个类。默认情况下,创建三个类。这些类默认包含没有内部结构的纯 FIFO 排队规则,但是您可以将这些替换为您拥有的任何排队规则。

每当需要将数据包出队时,首先尝试类 :1。只有当较低的频带都没有放弃数据包时,才会使用较高的类。

如果您想在不只使用 TOS 标志的情况下,而是使用 tc 过滤器的所有功能来优先处理某些类型的流量,则此排队规则非常有用。它还可以包含更多所有排队规则,而 pfifo_fast 仅限于简单的 fifo 排队规则。

因为它实际上并不整形,所以与 SFQ 相同的警告适用:要么仅在您的物理链路真正满载时使用它,要么将其包裹在执行整形的分类排队规则中。最后一点几乎适用于所有电缆调制解调器和 DSL 设备。

用正式的话来说,PRIO 排队规则是一个工作守恒调度器。

9.5.3.1. PRIO 参数和用法

以下参数被 tc 识别

bands

要创建的频带数量。每个频带实际上都是一个类。如果您更改此数字,您还必须更改

priomap

如果您不提供 tc 过滤器来分类流量,PRIO 排队规则会查看 TC_PRIO 优先级来决定如何对流量进行排队。

这就像前面提到的 pfifo_fast 排队规则一样工作,有关更多详细信息,请参见那里。

频带是类,默认情况下称为 major:1 到 major:3,因此如果您的 PRIO 排队规则称为 12:,则 tc 过滤器流量到 12:1 以赋予其更高的优先级。

重申一下,频带 0 转到次号码 1!频带 1 转到次号码 2,依此类推。

9.5.3.2. 示例配置

我们将创建这棵树
     root 1: prio
       /   |   \
     1:1  1:2  1:3
      |    |    |
     10:  20:  30:
     sfq  tbf  sfq
band  0    1    2

批量流量将转到 30:,交互式流量将转到 20: 或 10:。

命令行
# tc qdisc add dev eth0 root handle 1: prio 
## This *instantly* creates classes 1:1, 1:2, 1:3
  
# tc qdisc add dev eth0 parent 1:1 handle 10: sfq
# tc qdisc add dev eth0 parent 1:2 handle 20: tbf rate 20kbit buffer 1600 limit 3000
# tc qdisc add dev eth0 parent 1:3 handle 30: sfq                                

现在让我们看看我们创建了什么
# tc -s qdisc ls dev eth0 
qdisc sfq 30: quantum 1514b 
 Sent 0 bytes 0 pkts (dropped 0, overlimits 0) 

 qdisc tbf 20: rate 20Kbit burst 1599b lat 667.6ms 
 Sent 0 bytes 0 pkts (dropped 0, overlimits 0) 

 qdisc sfq 10: quantum 1514b 
 Sent 132 bytes 2 pkts (dropped 0, overlimits 0) 

 qdisc prio 1: bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
 Sent 174 bytes 3 pkts (dropped 0, overlimits 0) 
如您所见,频带 0 已经有一些流量,并且在运行此命令时发送了一个数据包!

我们现在使用一个正确设置 TOS 标志的工具进行一些批量数据传输,并再次查看
# scp tc ahu@10.0.0.11:./
ahu@10.0.0.11's password: 
tc                   100% |*****************************|   353 KB    00:00    
# tc -s qdisc ls dev eth0
qdisc sfq 30: quantum 1514b 
 Sent 384228 bytes 274 pkts (dropped 0, overlimits 0) 

 qdisc tbf 20: rate 20Kbit burst 1599b lat 667.6ms 
 Sent 2640 bytes 20 pkts (dropped 0, overlimits 0) 

 qdisc sfq 10: quantum 1514b 
 Sent 2230 bytes 31 pkts (dropped 0, overlimits 0) 

 qdisc prio 1: bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
 Sent 389140 bytes 326 pkts (dropped 0, overlimits 0) 
如您所见,所有流量都转到了句柄 30:,这是最低优先级频带,正如预期的那样。现在为了验证交互式流量是否转到更高的频带,我们创建一些交互式流量

# tc -s qdisc ls dev eth0
qdisc sfq 30: quantum 1514b 
 Sent 384228 bytes 274 pkts (dropped 0, overlimits 0) 

 qdisc tbf 20: rate 20Kbit burst 1599b lat 667.6ms 
 Sent 2640 bytes 20 pkts (dropped 0, overlimits 0) 

 qdisc sfq 10: quantum 1514b 
 Sent 14926 bytes 193 pkts (dropped 0, overlimits 0) 

 qdisc prio 1: bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
 Sent 401836 bytes 488 pkts (dropped 0, overlimits 0) 

它工作了——所有额外的流量都转到了 10:,这是我们最高优先级的排队规则。没有流量发送到之前接收我们整个 scp 的最低优先级。

9.5.4. 著名的 CBQ 排队规则

如前所述,CBQ 是最复杂的可用排队规则,炒作最多,最不被理解,并且可能是最难正确配置的。这并不是因为作者是邪恶的或不称职的,远非如此,只是 CBQ 算法并不是那么精确,并且与 Linux 的工作方式不太匹配。

除了是分类的之外,CBQ 也是一个整形器,正是在这方面它真的不能很好地工作。它应该像这样工作。如果您尝试将 10mbit/s 的连接整形为 1mbit/s,则链路应该有 90% 的时间处于空闲状态。如果不是,我们需要节流,使其 90% 的时间处于空闲状态。

这很难测量,因此 CBQ 而是从硬件层请求更多数据之间经过的微秒数来推导出空闲时间。结合起来,这可以用来近似链路的满或空程度。

这是相当迂回的,并且并不总是能得出正确的结果。例如,如果一个实际上无法传输 100mbit/s 全部数据的接口的实际链路速度是多少,可能是因为驱动程序实现得很糟糕?PCMCIA 网卡也永远无法实现 100mbit/s,因为总线的设计方式——再说一次,我们如何计算空闲时间?

如果我们考虑不太真实的网路设备,例如以太网上的 PPP 或 TCP/IP 上的 PPTP,情况会变得更糟。在这种情况下,有效带宽可能由管道到用户空间的效率决定——这非常巨大。

进行过测量的人发现 CBQ 并不总是非常准确,有时会完全偏离目标。

但是在许多情况下,它工作良好。有了这里提供的文档,您应该能够将其配置为在大多数情况下都能良好工作。

9.5.4.1. CBQ 整形详解

如前所述,CBQ 的工作原理是确保链路空闲足够长的时间,以将实际带宽降至配置的速率。为此,它计算平均数据包之间应该经过的时间。

在操作期间,有效空闲时间是使用指数加权移动平均 (EWMA) 测量的,它认为最近的数据包比过去的数据包重要得多。UNIX 负载平均值以相同的方式计算。

计算出的空闲时间从 EWMA 测量的空闲时间中减去,结果数字称为“avgidle”。一个完美加载的链路的 avgidle 为零:数据包正好在每个计算的时间间隔到达一次。

过载的链路具有负 avgidle,如果它变得太负,CBQ 会关闭一段时间,然后“超限”。

相反,空闲链路可能会积累巨大的 avgidle,这将在沉默几个小时后允许无限带宽。为了防止这种情况,avgidle 被限制在 maxidle。

如果超限,理论上,CBQ 可以将自身节流正好是计算出的数据包之间经过的时间量,然后传递一个数据包,然后再次节流。但请参阅下面的“minburst”参数。

以下是您可以指定的参数,以配置整形

avpkt

数据包的平均大小,以字节为单位。计算 maxidle 所需,maxidle 来源于 maxburst,maxburst 以数据包为单位指定。

bandwidth

您设备的物理带宽,空闲时间计算所需。

cell

数据包通过设备传输所需的时间可能会根据数据包大小以步长增长。例如,一个 800 和一个 806 大小的数据包可能需要相同的时间才能发送——这设置了粒度。最常设置为“8”。必须是 2 的整数幂。

maxburst

此数据包数量用于计算 maxidle,以便当 avgidle 处于 maxidle 时,可以在 avgidle 降至 0 之前突发此平均数据包数量。将其设置得更高可以更容忍突发。您不能直接设置 maxidle,只能通过此参数设置。

minburst

如前所述,在超限的情况下,CBQ 需要节流。理想的解决方案是在正好计算出的空闲时间内这样做,并传递 1 个数据包。但是,Unix 内核通常很难调度短于 10 毫秒的事件,因此最好节流更长的时间,然后一次传递 minburst 个数据包,然后休眠 minburst 倍的时间。

等待的时间称为 offtime。minburst 的值越高,从长远来看整形越准确,但在毫秒时间尺度上突发越大。

minidle

如果 avgidle 低于 0,我们超限了,需要等到 avgidle 足够大以发送一个数据包。为了防止突然的突发导致链路长时间关闭,如果 avgidle 变得太低,则会将其重置为 minidle。

Minidle 以负微秒为单位指定,因此 10 表示 avgidle 被限制为 -10us。

mpu

最小数据包大小——这是必需的,因为即使是零大小的数据包也会在以太网上填充到 64 字节,因此需要一定的时间才能传输。CBQ 需要知道这一点才能准确计算空闲时间。

rate

离开此排队规则的所需流量速率——这是“速度旋钮”!

在内部,CBQ 有很多微调。例如,已知没有数据排队到其中的类不会被查询。超限类会因降低其有效优先级而受到惩罚。所有这些都非常智能和复杂。

9.5.4.2. CBQ 分类行为

除了整形之外,使用前面提到的空闲时间近似值,CBQ 的行为也类似于 PRIO 队列,因为类可以具有不同的优先级,并且较低优先级的类将在较高优先级的类之前被轮询。

每次硬件层请求发送到网络的数据包时,都会启动加权循环轮询过程(“WRR”),从较低优先级的类开始。

然后将它们分组并查询它们是否有可用数据。如果有,则返回。在一个类被允许出队一定数量的字节后,将尝试该优先级内的下一个类。

以下参数控制 WRR 过程

allot

当外部 CBQ 被要求在接口上发送数据包时,它将依次尝试所有内部排队规则(在类中),按“priority”参数的顺序排列。每次一个类轮到它时,它只能发送有限数量的数据。“Allot”是此数量的基本单位。有关更多信息,请参见“weight”参数。

prio

CBQ 也可以像 PRIO 设备一样工作。首先尝试优先级较低的内部类,只要它们有流量,就不会轮询其他类以获取流量。

weight

权重有助于加权循环轮询过程。每个类都有机会轮流发送。如果您的某些类的带宽明显高于其他类,则允许它们在一轮中发送比其他类更多的数据是有意义的。

CBQ 会将一个类下的所有权重加起来,并对其进行归一化,因此您可以使用任意数字:只有比率很重要。人们一直在使用“rate/10”作为经验法则,并且它似乎运行良好。重新归一化的权重乘以“allot”参数以确定在一轮中可以发送多少数据。

请注意,CBQ 层次结构中的所有类都需要共享相同的主号码!

9.5.4.3. 确定链路共享和借用的 CBQ 参数

除了纯粹限制某些类型的流量外,还可以指定哪些类可以从其他类借用容量,或者反过来,借出带宽。

Isolated/sharing

配置为“isolated”的类不会将带宽借给同级类。如果您在链路上有竞争或互不友好的机构,他们不想互相赠送免费带宽,请使用此选项。

控制程序 tc 也知道“sharing”,它是“isolated”的反义词。

bounded/borrow

一个类也可以是“bounded”,这意味着它不会尝试从同级类借用带宽。tc 也知道“borrow”,它是“bounded”的反义词。

一个典型的情况可能是,您的链路上有两个机构,它们都是“isolated”和“bounded”,这意味着它们真的被限制在分配的速率内,并且也不允许彼此借用。

在这样一个机构类中,可能还有其他类被允许交换带宽。

9.5.4.4. 示例配置

此配置将 Web 服务器流量限制为 5mbit,将 SMTP 流量限制为 3 mbit。它们加在一起可能不会超过 6mbit。我们有一个 100mbit NIC,并且这些类可以相互借用带宽。
# tc qdisc add dev eth0 root handle 1:0 cbq bandwidth 100Mbit         \
  avpkt 1000 cell 8
# tc class add dev eth0 parent 1:0 classid 1:1 cbq bandwidth 100Mbit  \
  rate 6Mbit weight 0.6Mbit prio 8 allot 1514 cell 8 maxburst 20      \
  avpkt 1000 bounded
这部分安装了根和自定义的 1:0 类。1:1 类是 bounded 的,因此总带宽不能超过 6mbit。

如前所述,CBQ 需要*很多*旋钮。但是,上面解释了所有参数。相应的 HTB 配置要简单得多。

# tc class add dev eth0 parent 1:1 classid 1:3 cbq bandwidth 100Mbit  \
  rate 5Mbit weight 0.5Mbit prio 5 allot 1514 cell 8 maxburst 20      \
  avpkt 1000                       
# tc class add dev eth0 parent 1:1 classid 1:4 cbq bandwidth 100Mbit  \
  rate 3Mbit weight 0.3Mbit prio 5 allot 1514 cell 8 maxburst 20      \
  avpkt 1000

这是我们的两个类。请注意我们如何使用配置的速率来缩放权重。这两个类都不是 bounded 的,但它们连接到 bounded 的类 1:1。因此,这两个类的带宽总和永远不会超过 6mbit。顺便说一句,类 ID 需要与父 CBQ 位于同一个主号码中!

# tc qdisc add dev eth0 parent 1:3 handle 30: sfq
# tc qdisc add dev eth0 parent 1:4 handle 40: sfq

这两个类默认都有一个 FIFO 排队规则。但是我们将这些替换为 SFQ 队列,以便公平对待每个数据流。
# tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 match ip \
  sport 80 0xffff flowid 1:3
# tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 match ip \
  sport 25 0xffff flowid 1:4

这些命令直接附加到根,将流量发送到正确的排队规则。

请注意,我们使用“tc class add”来在排队规则中创建类,但是我们使用“tc qdisc add”来实际向这些类添加排队规则。

您可能想知道对于未被任何两条规则分类的流量会发生什么情况。在这种情况下,数据似乎将在 1:0 中处理,并且不受限制。

如果 SMTP+web 一起尝试超过 6mbit/s 的设定限制,则带宽将根据权重参数进行分配,将 5/8 的流量分配给 Web 服务器,将 3/8 的流量分配给邮件服务器。

使用此配置,您还可以说 Web 服务器流量将始终至少获得 5/8 * 6 mbit = 3.75 mbit。

9.5.4.5. 其他 CBQ 参数:split 和 defmap

如前所述,分类排队规则需要调用过滤器来确定数据包将被排队到哪个类。

除了调用过滤器之外,CBQ 还提供了其他选项,defmap 和 split。这很难理解,而且不是至关重要的。但是,由于这是唯一已知正确解释 defmap 和 split 的地方,所以我正在尽力而为。

由于您通常只想按服务类型字段进行过滤,因此提供了一种特殊的语法。每当 CBQ 需要弄清楚数据包需要排队到哪里时,它都会检查此节点是否为“split 节点”。如果是,则其中一个子排队规则已指示它希望接收具有特定配置优先级的​​所有数据包,这可以从 TOS 字段或应用程序设置的套接字选项中得出。

数据包的优先级位与 defmap 字段进行或运算,以查看是否存在匹配项。换句话说,这是一种创建非常快速过滤器的简写方式,它仅匹配某些优先级。defmap 为 ff(十六进制)将匹配所有内容,map 为 0 将不匹配任何内容。一个示例配置可能有助于使事情更清楚

# tc qdisc add dev eth1 root handle 1: cbq bandwidth 10Mbit allot 1514 \
  cell 8 avpkt 1000 mpu 64
 
# tc class add dev eth1 parent 1:0 classid 1:1 cbq bandwidth 10Mbit    \
  rate 10Mbit allot 1514 cell 8 weight 1Mbit prio 8 maxburst 20        \
  avpkt 1000
标准 CBQ 前言。我永远不习惯所需的大量数字!

Defmap 指的是 TC_PRIO 位,其定义如下

TC_PRIO..          Num  Corresponds to TOS
-------------------------------------------------
BESTEFFORT         0    Maximize Reliablity        
FILLER             1    Minimize Cost              
BULK               2    Maximize Throughput (0x8)  
INTERACTIVE_BULK   4                               
INTERACTIVE        6    Minimize Delay (0x10)      
CONTROL            7                               

TC_PRIO.. 数字对应于从右侧计数的位。有关 TOS 位如何转换为优先级的更多详细信息,请参见 pfifo_fast 部分。

现在是交互式类和批量类

# tc class add dev eth1 parent 1:1 classid 1:2 cbq bandwidth 10Mbit     \
  rate 1Mbit allot 1514 cell 8 weight 100Kbit prio 3 maxburst 20        \
  avpkt 1000 split 1:0 defmap c0

# tc class add dev eth1 parent 1:1 classid 1:3 cbq bandwidth 10Mbit     \
  rate 8Mbit allot 1514 cell 8 weight 800Kbit prio 7 maxburst 20        \
  avpkt 1000 split 1:0 defmap 3f

“split 排队规则”是 1:0,这是将做出选择的地方。C0 是二进制的 11000000,3F 是 00111111,因此这两个加在一起将匹配所有内容。第一个类匹配位 7 和 6,因此对应于“交互式”和“控制”流量。第二个类匹配其余部分。

节点 1:0 现在有一个如下所示的表
priority	send to
0		1:3
1		1:3
2		1:3
3		1:3
4		1:3
5		1:3
6		1:2
7		1:2

为了增加乐趣,您还可以传递一个“change mask”,它精确地指示您希望更改哪些优先级。仅当您运行“tc class change”时才需要使用它。例如,要将尽力而为流量添加到 1:2,我们可以运行此命令

# tc class change dev eth1 classid 1:2 cbq defmap 01/01

1:0 处的优先级映射现在看起来像这样

priority	send to
0		1:2
1		1:3
2		1:3
3		1:3
4		1:3
5		1:3
6		1:2
7		1:2

FIXME:未测试“tc class change”,仅查看了源代码。

9.5.5. 分层令牌桶

Martin Devera (<devik>) 正确地认识到 CBQ 很复杂,并且似乎没有针对许多典型情况进行优化。他的分层方法非常适合您拥有固定带宽量的情况,您想将带宽量划分为不同的用途,为每个用途提供保证的带宽,并有可能指定可以借用多少带宽。

HTB 的工作方式与 CBQ 完全相同,但不求助于空闲时间计算来进行整形。相反,它是一个分类的令牌桶过滤器——因此得名。它只有几个参数,在他的 网站上有详细的文档。

随着您的 HTB 配置变得越来越复杂,您的配置可以很好地扩展。使用 CBQ,即使在简单的情况下,它也已经很复杂了!HTB 尚未成为标准内核的一部分,但它应该很快就会成为!

如果您能够修补您的内核,请务必考虑 HTB。

9.5.5.1. 示例配置

功能上几乎与上面的 CBQ 示例配置相同

# tc qdisc add dev eth0 root handle 1: htb default 30

# tc class add dev eth0 parent 1: classid 1:1 htb rate 6mbit burst 15k

# tc class add dev eth0 parent 1:1 classid 1:10 htb rate 5mbit burst 15k
# tc class add dev eth0 parent 1:1 classid 1:20 htb rate 3mbit ceil 6mbit burst 15k
# tc class add dev eth0 parent 1:1 classid 1:30 htb rate 1kbit ceil 6mbit burst 15k

作者然后建议在这些类下使用 SFQ
# tc qdisc add dev eth0 parent 1:10 handle 10: sfq perturb 10
# tc qdisc add dev eth0 parent 1:20 handle 20: sfq perturb 10
# tc qdisc add dev eth0 parent 1:30 handle 30: sfq perturb 10

添加将流量定向到正确类的过滤器
# U32="tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32"
# $U32 match ip dport 80 0xffff flowid 1:10
# $U32 match ip sport 25 0xffff flowid 1:20
就是这样——没有难看的无法解释的数字,没有未记录的参数。

HTB 看起来确实很棒——如果 10: 和 20: 都具有其保证的带宽,并且还有更多带宽可以划分,则它们以 5:3 的比例借用,正如您所期望的那样。

未分类的流量被路由到 30:,它自己的带宽很少,但可以借用所有剩余的带宽。因为我们在内部选择了 SFQ,所以我们免费获得了公平性!