如果您有不同类型的流量需要区别对待,分类排队规则非常有用。其中一种分类排队规则称为“CBQ”,“基于类的排队”,它被广泛提及,以至于人们将基于类的排队完全等同于 CBQ,但事实并非如此。
CBQ 只是这个领域中最老的成员——也是最复杂的一个。它可能并不总是能满足您的需求。对于许多受“sendmail 效应”影响的人来说,这可能会让他们感到震惊,“sendmail 效应”告诉我们,任何没有文档的复杂技术一定是最好的。
稍后将详细介绍 CBQ 及其替代方案。
当流量进入分类排队规则时,它需要被发送到其中的任何一个类——它需要被“分类”。为了确定如何处理数据包,需要查阅所谓的“过滤器”。重要的是要知道,过滤器是从排队规则内部调用的,而不是相反!
附加到该排队规则的过滤器然后返回一个决策,排队规则使用它将数据包排队到其中一个类中。每个子类可能会尝试其他过滤器,以查看是否适用进一步的指令。如果没有,该类会将数据包排队到它包含的排队规则中。
除了包含其他排队规则外,大多数分类排队规则还执行整形。这对于执行数据包调度(例如使用 SFQ)和速率控制非常有用。在您拥有高速接口(例如,以太网)到较慢设备(电缆调制解调器)的情况下,您需要这样做。
如果您只运行 SFQ,则什么也不会发生,因为数据包进出您的路由器而没有延迟:输出接口比您的实际链路速度快得多。那时没有队列可以调度。
每个接口都有一个出口“根排队规则”,默认情况下是前面提到的无类 pfifo_fast 排队规则。可以为每个排队规则分配一个句柄,以供以后的配置语句引用该排队规则。除了出口排队规则外,接口还可以有一个入口,用于监管进入的流量。
这些排队规则的句柄由两部分组成,一个主号码和一个次号码。习惯上将根排队规则命名为“1:”,这等于“1:0”。排队规则的次号码始终为 0。
类需要与其父类具有相同的主号码。
回顾一下,一个典型的层次结构可能如下所示
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。
PRIO 排队规则实际上并不整形,它只是根据您配置过滤器的方式细分流量。您可以将 PRIO 排队规则视为一种增强型的 pfifo_fast,其中每个频带都是一个单独的类,而不是一个简单的 FIFO。
当数据包排队到 PRIO 排队规则时,会根据您给出的过滤器命令选择一个类。默认情况下,创建三个类。这些类默认包含没有内部结构的纯 FIFO 排队规则,但是您可以将这些替换为您拥有的任何排队规则。
每当需要将数据包出队时,首先尝试类 :1。只有当较低的频带都没有放弃数据包时,才会使用较高的类。
如果您想在不只使用 TOS 标志的情况下,而是使用 tc 过滤器的所有功能来优先处理某些类型的流量,则此排队规则非常有用。它还可以包含更多所有排队规则,而 pfifo_fast 仅限于简单的 fifo 排队规则。
因为它实际上并不整形,所以与 SFQ 相同的警告适用:要么仅在您的物理链路真正满载时使用它,要么将其包裹在执行整形的分类排队规则中。最后一点几乎适用于所有电缆调制解调器和 DSL 设备。
用正式的话来说,PRIO 排队规则是一个工作守恒调度器。
以下参数被 tc 识别
要创建的频带数量。每个频带实际上都是一个类。如果您更改此数字,您还必须更改
如果您不提供 tc 过滤器来分类流量,PRIO 排队规则会查看 TC_PRIO 优先级来决定如何对流量进行排队。
这就像前面提到的 pfifo_fast 排队规则一样工作,有关更多详细信息,请参见那里。
重申一下,频带 0 转到次号码 1!频带 1 转到次号码 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) |
我们现在使用一个正确设置 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) |
# 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 的最低优先级。
如前所述,CBQ 是最复杂的可用排队规则,炒作最多,最不被理解,并且可能是最难正确配置的。这并不是因为作者是邪恶的或不称职的,远非如此,只是 CBQ 算法并不是那么精确,并且与 Linux 的工作方式不太匹配。
除了是分类的之外,CBQ 也是一个整形器,正是在这方面它真的不能很好地工作。它应该像这样工作。如果您尝试将 10mbit/s 的连接整形为 1mbit/s,则链路应该有 90% 的时间处于空闲状态。如果不是,我们需要节流,使其 90% 的时间处于空闲状态。
这很难测量,因此 CBQ 而是从硬件层请求更多数据之间经过的微秒数来推导出空闲时间。结合起来,这可以用来近似链路的满或空程度。
这是相当迂回的,并且并不总是能得出正确的结果。例如,如果一个实际上无法传输 100mbit/s 全部数据的接口的实际链路速度是多少,可能是因为驱动程序实现得很糟糕?PCMCIA 网卡也永远无法实现 100mbit/s,因为总线的设计方式——再说一次,我们如何计算空闲时间?
如果我们考虑不太真实的网路设备,例如以太网上的 PPP 或 TCP/IP 上的 PPTP,情况会变得更糟。在这种情况下,有效带宽可能由管道到用户空间的效率决定——这非常巨大。
进行过测量的人发现 CBQ 并不总是非常准确,有时会完全偏离目标。
但是在许多情况下,它工作良好。有了这里提供的文档,您应该能够将其配置为在大多数情况下都能良好工作。
如前所述,CBQ 的工作原理是确保链路空闲足够长的时间,以将实际带宽降至配置的速率。为此,它计算平均数据包之间应该经过的时间。
在操作期间,有效空闲时间是使用指数加权移动平均 (EWMA) 测量的,它认为最近的数据包比过去的数据包重要得多。UNIX 负载平均值以相同的方式计算。
计算出的空闲时间从 EWMA 测量的空闲时间中减去,结果数字称为“avgidle”。一个完美加载的链路的 avgidle 为零:数据包正好在每个计算的时间间隔到达一次。
过载的链路具有负 avgidle,如果它变得太负,CBQ 会关闭一段时间,然后“超限”。
相反,空闲链路可能会积累巨大的 avgidle,这将在沉默几个小时后允许无限带宽。为了防止这种情况,avgidle 被限制在 maxidle。
如果超限,理论上,CBQ 可以将自身节流正好是计算出的数据包之间经过的时间量,然后传递一个数据包,然后再次节流。但请参阅下面的“minburst”参数。
以下是您可以指定的参数,以配置整形
数据包的平均大小,以字节为单位。计算 maxidle 所需,maxidle 来源于 maxburst,maxburst 以数据包为单位指定。
您设备的物理带宽,空闲时间计算所需。
数据包通过设备传输所需的时间可能会根据数据包大小以步长增长。例如,一个 800 和一个 806 大小的数据包可能需要相同的时间才能发送——这设置了粒度。最常设置为“8”。必须是 2 的整数幂。
此数据包数量用于计算 maxidle,以便当 avgidle 处于 maxidle 时,可以在 avgidle 降至 0 之前突发此平均数据包数量。将其设置得更高可以更容忍突发。您不能直接设置 maxidle,只能通过此参数设置。
如前所述,在超限的情况下,CBQ 需要节流。理想的解决方案是在正好计算出的空闲时间内这样做,并传递 1 个数据包。但是,Unix 内核通常很难调度短于 10 毫秒的事件,因此最好节流更长的时间,然后一次传递 minburst 个数据包,然后休眠 minburst 倍的时间。
等待的时间称为 offtime。minburst 的值越高,从长远来看整形越准确,但在毫秒时间尺度上突发越大。
如果 avgidle 低于 0,我们超限了,需要等到 avgidle 足够大以发送一个数据包。为了防止突然的突发导致链路长时间关闭,如果 avgidle 变得太低,则会将其重置为 minidle。
Minidle 以负微秒为单位指定,因此 10 表示 avgidle 被限制为 -10us。
最小数据包大小——这是必需的,因为即使是零大小的数据包也会在以太网上填充到 64 字节,因此需要一定的时间才能传输。CBQ 需要知道这一点才能准确计算空闲时间。
离开此排队规则的所需流量速率——这是“速度旋钮”!
在内部,CBQ 有很多微调。例如,已知没有数据排队到其中的类不会被查询。超限类会因降低其有效优先级而受到惩罚。所有这些都非常智能和复杂。
除了整形之外,使用前面提到的空闲时间近似值,CBQ 的行为也类似于 PRIO 队列,因为类可以具有不同的优先级,并且较低优先级的类将在较高优先级的类之前被轮询。
每次硬件层请求发送到网络的数据包时,都会启动加权循环轮询过程(“WRR”),从较低优先级的类开始。
然后将它们分组并查询它们是否有可用数据。如果有,则返回。在一个类被允许出队一定数量的字节后,将尝试该优先级内的下一个类。
以下参数控制 WRR 过程
当外部 CBQ 被要求在接口上发送数据包时,它将依次尝试所有内部排队规则(在类中),按“priority”参数的顺序排列。每次一个类轮到它时,它只能发送有限数量的数据。“Allot”是此数量的基本单位。有关更多信息,请参见“weight”参数。
CBQ 也可以像 PRIO 设备一样工作。首先尝试优先级较低的内部类,只要它们有流量,就不会轮询其他类以获取流量。
权重有助于加权循环轮询过程。每个类都有机会轮流发送。如果您的某些类的带宽明显高于其他类,则允许它们在一轮中发送比其他类更多的数据是有意义的。
CBQ 会将一个类下的所有权重加起来,并对其进行归一化,因此您可以使用任意数字:只有比率很重要。人们一直在使用“rate/10”作为经验法则,并且它似乎运行良好。重新归一化的权重乘以“allot”参数以确定在一轮中可以发送多少数据。
请注意,CBQ 层次结构中的所有类都需要共享相同的主号码!
除了纯粹限制某些类型的流量外,还可以指定哪些类可以从其他类借用容量,或者反过来,借出带宽。
配置为“isolated”的类不会将带宽借给同级类。如果您在链路上有竞争或互不友好的机构,他们不想互相赠送免费带宽,请使用此选项。
控制程序 tc 也知道“sharing”,它是“isolated”的反义词。
一个类也可以是“bounded”,这意味着它不会尝试从同级类借用带宽。tc 也知道“borrow”,它是“bounded”的反义词。
在这样一个机构类中,可能还有其他类被允许交换带宽。
此配置将 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 |
如前所述,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。
如前所述,分类排队规则需要调用过滤器来确定数据包将被排队到哪个类。
除了调用过滤器之外,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 |
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”,仅查看了源代码。
Martin Devera (<devik>) 正确地认识到 CBQ 很复杂,并且似乎没有针对许多典型情况进行优化。他的分层方法非常适合您拥有固定带宽量的情况,您想将带宽量划分为不同的用途,为每个用途提供保证的带宽,并有可能指定可以借用多少带宽。
HTB 的工作方式与 CBQ 完全相同,但不求助于空闲时间计算来进行整形。相反,它是一个分类的令牌桶过滤器——因此得名。它只有几个参数,在他的 网站上有详细的文档。
随着您的 HTB 配置变得越来越复杂,您的配置可以很好地扩展。使用 CBQ,即使在简单的情况下,它也已经很复杂了!HTB 尚未成为标准内核的一部分,但它应该很快就会成为!
如果您能够修补您的内核,请务必考虑 HTB。
功能上几乎与上面的 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,所以我们免费获得了公平性!