首页 > 代码库 > Linux TC 带宽管理队列规则

Linux TC 带宽管理队列规则

       在着手学习TC之前,请先了解TC采用如下单位来描述带宽:

      mbps = 1024 kbps = 1024 * 1024 bps => byte/s

      mbit = 1024 kbit => kilo bit/s

      mb = 1024 kb = 1024 * 1024 b => byte

      mbit = 1024 kbit => kilo bit

      内定:数值以bps和b方式储存。但当设置tc输出速率时,使用如下表示:

      1Mbit = 1024 Kbit = 1024 * 1024 bps => byte/s

      提醒:bit=1位,相当于一个0 或1(开或关);byte=1字节,相当于一个英文字母的大小。

      8bit=1byte,所以往往听说带宽是10M是指bit,我们看到的下载速度是byte/s =10M/8byte的来。

 

队列和队列规则

      利用队列,我们可以控制数据发送的方式。但我们只能对发送数据进行控制(或称为流量整形)。

      由于网络的工作机制,我们无法直接控制别人向我们发送什么数据。然而,Internet主要依靠TCP/IP,它的一些特性很有用。因为TCP/IP没办法知道两个主机之间的网络容量,所以它会试图越来越快地发送数据(所谓的“慢启动”),在因为网络容量不够而开始丢失数据时,才放慢发送数据的速度。

      这就像家门口的邮箱一样,当邮箱几乎塞满了邮件时,你会希望不要再发送邮件了。在Internet上可以做到这一点。如果你有一个路由器,并且希望防止某些主机下载速度太快,你需要在路由器的内网卡——也就是链接你的网内主机发送数据包的网卡上进行流量控制。

      你还要保证你正在控制的是流量瓶颈环节。如果你有一个100M以太网卡,而你的路由器的链路速度是256k,你必须保证你发送的数据量没有超过路由器的处理能力。否则,就是路由器在控制链路和对带宽进行整形,而不是你所定义的规则在对带宽进行控制。可以说,我们需要拥有的队列必须是一系列链路中最慢的环节。

 

简单的无类队列规则

      如前所述,利用队列,我们决定了数据被发送的方式。无类队列能够接收数据和重新编排、延迟或丢弃数据包。

      这可以用作对于整个网卡的流量进行整形,而不细分各种情况。在我们进一步学习分类的队列规则之前,理解这部分是必不可少的!

      最广泛应用的规则是pfifo_fast队列规则,因为它是缺省配置。还有其他队列规则,后面会有所介绍。每种队列都有它们各自的优势和弱点。

    1.pfifo_fast

      这个队列的特点就像它的名字一样——先进先出(FIFO),也就是说没有任何数据包被特殊处理。这个队列有3个所谓的“频道”(band)。FIFO规则应用于每一个频道。并且:如果在0频道有数据包等待发送,1频道的包就不会被处理,1频道和2频道之间的关系也是如此。

      内核遵照数据包的TOS标记,把带有“最小延迟”标记的包放进0频道。

    *参数与使用*

      pfifo_fast队列规则作为硬性的缺省设置,不能对它进行配置。

      其缺省配置如下:

      priomap:

      内核规则,根据数据包的优先权情况,映射到相应的频道。这个映射过程是根据数据包的TOS字节进行的。

      TOS字段如下:

      

      TOS字段的4个bit是如下定义的:

      

      因为在这4bit的后面还有一个bit(MBZ),所以TOS字段的实际值是上述值的2倍。tcpdump -v -v命令可以让你看到整个TOS字段的情况,而不仅仅是这4个bit,也就是你在下表的第一列看到的值:

      

      第二列写着与4个TOS位相关的数值,接着第三列对应是它们的意义。比如,15表示一个数据包要求最小成本、最大可靠性、最大吞吐量和最小延迟。

      第四列写出了Linux内核对于TOS位的理解,并表明了它们对应哪种优先权。最后一列表明缺省的权限图。在命令行里,缺省的权限图应该是:

      1,2,2,2,1,2,0,0,1,1,1,1,1,1,1,1

      也就是说,比如优先权4将被映射到1频道。权限图允许你列出更高的优先权值(只要小于7),它们不对应TOS映射,但是有其它的意图。

      下表来自RFC 1349,告诉你应用程序可能如何设置它们的TOS:

      

      tx queuelen

      队列的长度来自网卡的配置,你可以用ifconfig和ip命令修改。

      如设置队列长度为10,执行:ifconfig eth0 txqueuelen 10(不能用tc命令设置这个)。

    2.令牌桶过滤器(TBF)

      令牌桶过滤器(TBF,Token Bucket Filter)是一个简单的队列规则:只允许以不超过事先设定的速率到来的数据包通过,但可能允许短暂突发流量超过设定值。

      TBF很精确,对于网络和处理器的影响都很小。所以如果您想对一个网卡限速,它应该是最好选择!

      TBF的实现在于一个缓冲器(桶),该缓冲器(桶)不断地被一些叫做”令牌”的虚拟数据以特定速率(token rate)填充着。桶最重要的参数就是它的大小,也就是它能够存储令牌的数量。

      每个到来的令牌从数据队列中收集一个数据包,然后从桶中被删除。这个算法关联到两个流上——令牌流和数据流,于是我们得到3种情景:

      *数据流以等于令牌流的速率到达TBF。这种情况下,每个到来的数据包都能对应一个令牌,然后无延迟地通过队列。

      *数据流以小于令牌流的速度到达TBF。通过队列的数据包只消耗了一部分令牌,剩下的令牌会在桶里积累下来,直到桶被装满。剩下的令牌可以在需要以高于令牌流速率发送数据流的时候消耗掉,这种情况下会发生突发传输。

      *数据流以大于令牌流的速率到达TBF。这意味着桶里的令牌很快就会被耗尽。导致TBF中断一段时间,称为”越限”(overlimit)。如果数据包持续到来,将发生丢包。

      第三种情景非常重要,因为它会对数据通过过滤器的速率进行整形。令牌的积累可以导致越限的数据进行短时间的突发传输而不必丢包,但是持续越限的话会导致传输延迟直至丢包。实际的实现是针对数据的字节数进行的,而不是针对数据包进行的。

    *参数与使用*

      TBF提供了一些可调控的参数。第一个参数永远可用:

      limit/latency

      limit确定最多有多少数据(字节数)在队列中等待令牌。你也可以通过设置latency来指定这个参数,latency参数确定了一个包在TBF中等待传输的最长等待时间。两者计算决定桶的大小、速率和峰值速率。

      burst/buffer/maxburst

      桶的大小,以字节计。这个参数指定了最多可以有多少个令牌能够即刻被使用。通常,管理的带宽越大,需要的缓冲器就越大。在Intel体系上,10Mbit/s的速率需要至少10k字节的缓冲区才能达到期望的速率。

      如果你的缓冲区太小,就会导致到达的令牌没有地方放(桶满了),这会导致潜在的丢包。

      MPU

      一个零长度的包并不是不耗费带宽。比如以太网,数据帧不会小于64字节。MPU(Minimum Packet Unit,最小分组单元)决定了令牌的最低消耗。

      rate

      速度操纵杆。参见上面的limit。

      如果桶里存在令牌而且允许没有令牌,相当于不限制速率(缺省情况)。如果不希望这样,可以调入以下参数:

      peakrate(峰值速率)

      如果有可用的令牌,数据包一旦到来就会立刻被发送出去,就像光速一样。那可能并不是你希望的,特别是你有一个比较大的桶的时候。

      峰值速率可以用来指定令牌以多快的速度被删除。用书面语言来说,就是:释放一个数据包,然后等待足够的时间后再释放下一个。我们通过计算等待时间来控制峰值速率。例如:UNIX定时器的分辨率是10毫秒,如果平均包长10kb,我们的峰值速率被限制在了1Mbps。

      MTU(Maximum Transmission Unit, 最大传输单元)/minburst

      但是如果你的常规速率比较高,1Mbps的峰值速率就需要调整。要实现更高的峰值速率,可以在一个时钟周期内发送多个数据包。最有效的办法就是:再创建一个令牌桶!这第二个令牌桶缺省情况下为一个单个的数据包,并非一个真正的桶。

      要计算峰值速率,用MTU乘以100就行了。(应该说是乘以HZ数,Intel体系上是100,Alpha体系上是1024)

    *配置范例*

      这是一个非常简单而实用的例子:

      # tc qdisc add dev ppp0 root tbf rate 220kbit latency 50ms burst 1540

      为什么它很实用呢?如果你有一个队列较长的网络设备,比如DSL modem或者cable modem什么的,并通过一个快速设备(如以太网卡)与之相连,你会发现上传数据会破坏交互性。

      这是因为上传数据会充满modem的队列,而这个队列为了改善上载数据的吞吐量而设置的特别大。你可能为了提高交互性只需要一个不太大的队列,也就是说你希望在发送数据的时候干点其他的事情。

      上面的命令行并非直接影响了modem中的队列,而是通过控制Linux中的队列而放慢了发送数据的速度。

      把220kbit修改为你实际的上载速度再减去几个百分点。如果你的modem确实很快,就把“burst”值提高一点。

    3.随机公平队列(SFQ)

      SFQ(Stochastic Fairness Queueing,随机公平队列)简单实现公平队列算法。它的精确性不如其它的方法,但是它在实现高度公平的同时,需要的计算量却很少。

      SFQ的关键词是”会话”或“流”,主要针对一个TCP会话或者UDP流。流量被分成相当多数量的FIFO队列中,每个队列对应一个会话。数据按照简单轮转的方式发送,每个会话都按顺序得到发送机会。

      这种方式非常公平,保证了每一个会话都不会没其它会话所淹没。SFQ之所以被称为“随机”,是因为它并不是真的为每一个会话创建一个队列,而是使用一个散列算法,把所有的会话映射到有限的几个队列中去。因为使用了散列,所以可能多个会话分配在同一个队列里,从而需要共享发包的机会,也就是共享带宽。为了不让这种效应太明显,SFQ会频繁地改变散列算法,以便把这种效应控制在几秒钟之内。

      有很重要的一点需要声明:只有当你的输出网卡确实已经挤满了的时候,SFQ才会起作用!否则在你的Linux机器中根本就不会有队列,SFQ也就不会起作用。稍后我们会描述如何把SFQ与其它的队列规则结合在一起,以保证两种情况下都有比较好的结果。

      特别地,在你使用DSL modem或者cable modem的以太网卡上设置SFQ而不进行任何进一步地流量整形是无谋的!

    *参数与使用*

      SFQ基本上不需要手工调整:

      perturb: 多少秒后重新配置一次散列算法。如果取消设置,散列算法将永远不会重新配置(不建议这样做)。10秒应该是一个合适的值。

      quantum: 一个流至少要传输多少字节后才切换到下一个队列。缺省设置为一个最大包的长度(MTU的大小)。不要设置这个数值低于MTU!

    *配置范例*

      如果你有一个网卡,它的链路速度与实际可用速率一致——比如一个电话Modem——如下配置可以提高公平性:

      # tc qdisc add dev ppp0 root sfq perturb 10

      # tc -s -d qdisc ls

      qdisc sfq 800c: dev ppp0 quantum 1514b limit 128p flows 128/1024 perturb 10sec

      Sent 4812 bytes 62 pkts (dropped 0, overlimits 0)

      “800c:”这个号码是系统自动分配的一个句柄号(handle),“limit”意思是这个队列中可以有128个数据包排队等待。一共可以有1024个散列目标可以用于速率审计,而其中128个可以同时激活。(no more packets fit in the queue!)每隔10秒种散列算法更换一次。

 

关于什么时候用哪种队列的建议

      总之,我们有几种简单的队列,分别使用排序、限速和丢包等手段来进行流量整形。

      下列提示可以帮你决定使用哪一种队列。

      *如果想单纯地降低出口速率,使用令牌桶过滤器(tbf, token bucket filter)。调节桶的配置可用于控制很高的带宽。

      *如果你的链路已经塞满了,而你想保证不会有某一个会话独占出口带宽,使用随机公平队列(sfq, stochastical fairness queueing)。

      *如果你有一个很大的骨干带宽,并且了解了相关技术后,可以考虑前向随机丢包(red, random eraly drop)。

      *如果希望对入口流量进行“整形”(不是转发流量),可使用入口流量策略(ingress policer),注意,这不是真正的“整形”。

      *如果你正在转发数据包,在数据流出的网卡上应用TBF。因为入口网卡起决定性作用的时候,除非你希望让数据包从多个网卡流出,在这种情况下还是使用入口策略(ingress policer)。

      *如果你并不希望进行流量整形,只是想看看你的网卡是否有比较高的负载而需要使用队列,使用pfifo队列(不是pfifo_fast)。它缺乏内部频道但是可以统计backlog。(好像更适合用于做流量监控)

      *最后,你可以进行所谓的“社交整形”。你不能通过技术手段解决一切问题。用户的经验技巧永远是不友善的。正确而友好的措辞可能帮助你的正确地分配带宽!

 

术语

      为了正确地理解更多的复杂配置,有必要先解释一些概念。由于这个主题的历史不长和其本身的复杂性,人们经常在说同一件事的时候使用各种词汇。

      以下来自draft-ietf-diffserv-model-06.txt,Diffserv路由器的建议管理模型。关于这些词语的严格定义请参考这个文档。

      队列规则(Queuing Discipline,qdisc)

      管理设备输入(ingress)或输出(egress)的算法。

      根队列规则(Root Qdisc)

      根队列规则就是直接依附于设备的队列规则。

      无类的队列规则(Classless Qdisc)

      一个内部不包含可配置子类的队列规则。

      分类的队列规则(Classiful Qdisc)

      一个分类的队列规则内可以包含更多的类。其中每个类又进一步地包含一个队列规则,这个队列规则可以是分类的,也可以是无类的。

      类(Classes)

      一个分类的队列规则可以拥有很多类,类内包含队列规则。

      分类器(Classifier)

      每个分类的队列规则都需要决定什么样的包使用什么类进行发送。分类器就是做这个用的。

      过滤器(Filter)

      分类是通过过滤器完成的。一个过滤器包含若干的匹配条件,如果符合匹配条件,就按此过滤器分类。

      调度(Scheduling)

      在分类器的帮助下,一个队列规则可以裁定某些数据包可以排在其他数据包之前发送。这种处理叫做”调度”,比如此前提到的pfifo_fast就是这样的。

      整形(Shaping)

      在一个数据包发送之前进行适当的延迟,以免超过事先规则好的最大速率,这种处理叫做”整形”。整形在egress处进行。习惯上,通过丢包来降速也经常被称为整形。

      策略(Policing)

      通过延迟或是丢弃数据包来保证流量不超过事先规则的带宽。在Linux中,策略总是规定丢弃数据包而不是延迟,即不存在ingress队列。

      Work-Conserving

      对于一个work-conserving队列规则,如果得到一个数据包,它总是立刻对它进行分发。换句话说,只要网卡(egress队列规则)允许,它就不会延迟数据包的发送。

      non-Work-Conserving

      有些队列——比如令牌桶过滤器——可能需要暂时停止发包以实现限制带宽。也就是说它们有时候即使有数据包需要处理,也可能拒绝发送。

      现在我们简单了解了一些术语,让我们看看它们的位置:

      

      1、整个大方框表示内核。

      2、最左面的箭头表示从网络上进入计算机的数据包。它们进入Ingress队列规则,并有可能被某些过滤器丢弃即所谓策略(在进入内核之前丢弃数据有利于节约CPU时间)。

      3、数据包顺利通过的话,如果它是发往本地进程的,就会进入IP协议栈处理并提交给该进程。

      4、如果它需要转发而不是进入本地进程,就会发往egress。本地进程也可以发送数据,交给Egress分类器。

      5、然后经过审查,并放入若干队列规则中的一个进行排队。这个过程叫做“入队”。在不进行任何配置的情况下,只有一个egress队列规则——pfifo_fast——总是接收数据包。

      6、数据包进入队列后,就等待内核处理并通过某网卡发送。这个过程叫做“出队”。

      7、这张图仅仅表示了机器上只有一块网卡的情况,图中的箭头不能代表所有情况。每块网卡都有它自己的ingress和egress。

分类的队列规则

      如果你有多种数据流需要进行区别对待,分类的队列规则就非常有用了。其中一种叫CBQ(Class Based Queueing,基于类的队列)经常被提起,以至于大家认为CBQ就是鉴别队列是否分类的标准,这是不对的:

      CBQ不过是家族中年纪最大的孩子而已,同时也是最复杂的。它并不能为你做所有你想做的事情。因此很多人认为不可思议,因为他们受“sendmail效应”影响较深,总是认为复杂的且没有文档的技术肯定是最好的。

    1.分类的队列规则及其类中的数据流向

      一旦数据包进入一个分类的队列规则,它就得被送到某一个类中——也就是需要分类。对数据包进行分类的工具是过滤器。一定要记住:“分类器”是从队列规则内部调用的,而不是从其他地方。

      过滤器会返回一个决定,队列规则就根据这个决定把数据包送入相应的类进行排队。每个子类都可以再次使用它们自己的过滤器进行进一步的分类,直到不需要分类为止,数据包才进入该类包含的队列规则等待处理。

      除了能够包含其它队列规则之外,绝大多数分类的队列规则还能够进行流量整形。这对于需要同时进行调度(如使用SFQ)和流量控制的场合非常有用。

      如果你仅仅使用SFQ,那什么用也没有。因为数据包进、出路由器时没有任何延迟。虽然你的输出网卡远远快于实际连接速率,但路由器中却没有队列可以调度。

    2.队列规则家族:根、句柄、兄弟和父辈

      每块网卡都有一个出口“根队列规则”,缺省情况下是前面提到的pfifo_fast队列规则。每个队列规则都指定一个句柄(就是队列代号),以便以后的配置语句能够引用这个队列规则。除了出口队列规则之外,每块网卡还有一个入口队列规则以便对进入的数据流进行策略调整。

      队列规则的句柄有两个部分:一个主号码和一个次号码。习惯上把根队列规则称为“1:”,等价于“1:0”。队列规则的次号码永远是0。

      类的主号码必须与它们父辈的主号码一致。

    *如何用过滤器进行分类*

      下图给出一个典型的分层关系:

      

      数据包是在根队列规则处入队和出队的,而内核只与“根”打交道。

      一个数据包可能是按照下面这个链状流程进行分类的:

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

      数据包现在应该处于12:2下属的某个队列规则中的某个队列中。在这个例子中,树的每个节点都附带着一个过滤器,用来选择下一步进入哪个分支。这样比较直观。然而,这样也是允许的:

      1: -> 12:2

      也就是说,根所附带的一个过滤器要求把数据包直接交给12:2。

    *数据包如何出队并交给硬件

      当内核决定把一个数据包发给网卡的时候,根队列规则1:会得到一个出队请求,然后把它传给1:1,然后依次传给10:、11:和12:,然后试图从它们中进行dequeue(出队)操作。也就是说,内核需要遍历整棵树,因为只有12:2中才有这个数据包。换句话说,类及其兄弟仅仅与其“父队列规则”进行交谈,而不会与网卡进行交谈。只有根队列规则才能由内核操作进行出队操作!

      更进一步,任何类的出队操作都不会比它们的父类更快。这恰恰是你所需要的:我们可以把SFQ作为一个子类,放到一个可以进行流量整形的父类中,从而能够同时得到其父类的流量整形功能和SFQ的调度功能。

    3.PRIO队列规则

      PRIO队列规则并不进行流量整形,它仅仅根据配置的过滤器把流量进一步细分。你可以认为PRIO队列规则是pfifo_fast的一种衍生物,区别在每个频道都是一个单独的类,而非简单的FIFO。

      当数据包进入PRIO队列规则后,根据你给的过滤器设置选择一个类,缺省情况下有三个类。这些类仅包含纯FIFO队列规则而没有更多的内部结构。你可以把它们替换成你需要的任何队列规则。

      每当有一个数据包需要出队时,首先处理:1类。只有当标号更小的类中没有需要处理的数据时,才会处理标号更大的类。

      当你希望不仅仅依靠包的TOS,而是想使用TC所提供的更强大的功能来进行数据包的优先权划分时,可以使用这个队列规则。它也可以包含更多的队列规则,而pfifo_fast却只能包含简单的fifo队列规则。

      因为它不进行整形,所以使用时与SFQ有相同的考虑:要么确保这个网卡的带宽确实已经占满,要么把它包含在一个能够整形的分类的队列规则的内部。严格地说,PRIO队列规则是一种Work-Conserving调度。

    *PRIO的参数与使用*

      PRIO识别下列参数:

      bands
      创建频道的数目。每个频道实际上就是一个类。如果你修改了这个数值,你必须同时修改:

      priomap

      如果你不给TC提供任何过滤器,PRIO队列规则将参考TC_PRIO的优先级来决定如何给数据包入队。

      它的行为就像前面提到过的pfifo_fast队列规则(先入先出)

      其实频道是类,缺省情况下命名为“主标号:1”到“主标号:3”。如果你的PRIO队列规则是“12: ”,把数据包过滤到“12:1”将得到最高优先级。注意:0频道的次标号是1;1频道的次标号是2,以此类推。

    *配置范例*

      我们想创建这个树:

      

      大批量数据使用30:交互数据使用20:或10:。

      命令如下:

      #tc qdisc add dev eth0 root handle 1: prio

      ##这个命令立即创建了类: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不同,没有数据经过最低优先级的队列规则。

    4.著名的CBQ队列规则

      如前所述,CBQ是最复杂、最琐碎、最难以理解、最刁钻的队列规则。这并不是因为其作者的恶毒或者不称职,而是因为CBQ算法本身的不精确,而且与Linux的内在机制不协调造成的。

      除了可以分类之外,CBQ也是一个整形器,但是从表面上看来工作得并不好。它应该是这样的:如果你试图把一个10Mbps的连接整形成1Mbps的速率,就应该让链路90%的时间处于闲置状态,必要的话我们就强制,以保证90%的闲置时间。

      但闲置时间的测量非常困难,所以CBQ就采用了它一个近似值——来自硬件层的两个传输请求之间的毫秒数来代替它。这个参数可以近似地表现这个链路的繁忙程度。

      这样做相当慎重,而且不一定能够得到正确的结论。比如,由于驱动程序方面或者其它原因造成一块网卡的实际传输速率不能够达到它的标称速率,该怎么办?由于总线设计的原因,PCMCIA网卡永远也不会达到100Mbps。那么我们该怎么计算闲置时间呢?

      如果我们引入非物理网卡——像PPPoE、PPTP——情况会变得更糟糕。因为相当一部分有效带宽耗费在了链路维护上。那些做过了测试的人们都发现CBQ总是不精确甚至完全失去了其本来意义。但是,在很多场合下它还是能够很好地工作。根据下面的文档,你应该能够较好地配置CBQ来解决大多数问题。

    *CBQ整形的细节:*

      CBQ的工作机制是确认链路的闲置时间足够长,以达到降低链路实际带宽的目的。为此,它要计算两个数据包的平均发送间隔。

      操作期间,有效闲置时间的测量使用EWMA(exponential weighted moving average,指数加权移动均值)算法,也就是说最近处理的数据包的权值比以前的数据包按指数增加。UNIX的平均负载也是这样算出来的。

      计算出来的平均时间值减去EWMA测量值,得出的结果叫做“avgidle”。最佳的链路负载情况下,这个值应当是0:数据包严格按照计算出来的时间间隔到来。

      在一个过载的链路上,avgidle值应当是负的。如果这个负值太严重,CBQ就会暂时禁止发包,称为”overlimit”(越限)。

      相反地,一个闲置的链路应该有很大的avgidle值,这样闲置几个小时后,会造成链路允许非常大的带宽通过。为了避免这种局面,我们用maxidle来限制avgidle的值不能太大。

      理论上讲,如果发生越限,CBQ就会禁止发包一段时间(长度就是事先计算出来的传输数据包之间的时间间隔),然后通过一个数据包后再次禁止发包。但是最好参照下面的minburst参数。

      下面是配置流量整形时需要指定的一些参数:

      avpkt

      平均包大小,单位是字节。计算maxidle(最大闲置)时需要,maxidle从maxburst得出。

      bandwidth

      网卡的物理带宽,用来计算闲置时间。

      cell

      一个数据包被发送出去的时间可以是基于包长度而阶梯增长的。一个800字节的包和一个806字节的包可以认为耗费相同的时间。也就是说它用作设置时间力度。通常设置为8,必须是2的整数次幂。

      maxburst

      这个参数的值决定了计算maxidle所使用的数据包的个数。在avgidle跌落到0之前,这么多的数据包可以突发传输出去。这个值越高,越能够容纳突发传输。你无法直接设置maxidle的值,必须通过这个参数来控制。

      minburst

      如前所述,发生越限时CBQ会禁止发包。实现这个的理想方案是根据事先计算出的闲置时间进行延迟之后,发一个数据包。然而,UNIX的内核一般来说都有一个固定的调度周期(一般不大于10ms),所以最好是这样:禁止发包的时间稍长一些,然后突发性地传输minburst个数据包,而不是一个一个地传输。等待的时间叫做offtime。

      从大的时间尺度上说,minburst值越大,整形越精确。但是,从毫秒级的时间尺度上说,就会有越多的突发传输。

      minidle

      如果avgidle值降到0,也就是发生了越限,就需要等待,直到avgidle的值足够大才发送数据包。为避免因关闭链路太久而引起的以外突发传输,在avgidle的值太低的时候会被强制设置为minidle的值。

      参数minidle的值是以负微秒记的。所以10代表avgidle被限制在-10us上。

      mpu

      最小分组大小——因为即使是0长度的数据包,在以太网上也要生成封装成64字节的帧,而需要一定时间去传输。为了精确计算闲置时间,CBQ需要知道这个值。

      rate

      期望中的传输速率。也就是“速度操纵杆”!

      在CBQ的内部有很多的微调参数。比如,那些已知队列中没有数据的类就不参加计算、越限的类将被惩罚性地降低优先级等等。都非常巧妙和复杂。

    *CBQ在分类方面的行为*

      除了使用上述idletime近似值进行整形之外,CBQ还可以象PRIO队列那样,把各种类赋予不同的优先级,优先权数值小的类会比优先权值大的类被优先处理。

      每当网卡请求把数据包发送到网络上时,都会开始一个WRR(weighted round robin,加权轮转)过程,从优先权值小的类开始。

      那些队列中有数据的类就会被分组并被请求出队。在一个类收到允许若干字节数据出队的请求之后,再处理下一个相同优先权值的类。

    *下面是控制WRR过程的一些参数:*

      allot

      当从外部请求一个CBQ发包的时候,它就会按照“priority(prio)”参数指定的顺序轮流尝试其内部的每一个类的队列规则。当轮到一个类发数据时,它只能发送一定量的数据。“allot”参数就是这个量的基值。更多细节请参照“weight”参数。

      prio

      CBQ可以象PRIO设备那样工作。其中“prio”值较低的类只要有数据就必须先服务,其他类要延后处理。

      weight

      “weight”参数控制WRR过程。每个类都轮流取得发包的机会。如果其中一个类要求的带宽显著地高于其他的类,就应该让它每次比其他的类发送更多的数据(以字节为单位,可以理解为偏袒数量,例如weight 200Kbit就相当于每次处理优先级的数据比普通数据多处理200Kbit)。

      CBQ会把一个类下面所有的weight值加起来后归一化,所以数值可以任意定,只要保持比例合适就可以。人们常把“速率/10”作为参数的值来使用,实际工作得很好。归一化值后的值乘以“allot”参数后,决定了每次传输多少数据。

    *决定链路的共享和借用的CBQ参数*

      除了纯粹地对某种数据流进行限速之外,CBQ还可以指定哪些类可以向其它哪些类借用或者出借一部分带宽。

      Isolated/Sharing (isolated字面意思:独立,单独的)

      凡是使用“isolated”选项配置的类,就不会向其兄弟类借出带宽。如果你的链路上同时存在着不友好的人,你就可以使用这个选项。

      选项“sharing”是“isolated”的反义选项。

      Bounded/Borrow (bounded字面意思:受限制的,有限的;borrow=借入)

      一个类也可以用“bounded”选项配置,意味着它不会向其兄弟类借入带宽。选项“borrow”是“bounded”的反义选项。

      一个典型的情况就是你的一个链路上有多个客户都设置成了“isolated”和“bounded”,那就是说他们都被限制在其要求的速率之下,且互相之间不会借用带宽(就是我们常说的带宽独享)。在这样的一个类的内部的子类之间是可以互相借用带宽的。

    *配置范例*

      

      这个配置把WEB服务器的流量控制为5mbit、SMTP流量控制在3mbit上。而且二者一共不得超过6mbit,互相之间允许借用带宽。我们的网卡是100Mbps的。

# 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。也就是说整个带宽不能超过6Mbps。

      如前所述,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

      我们建立了2个类。注意我们如何根据带宽来调整weight参数的。两个类都没有配置成“bounded”,但它们都连接到了类1:1上,而1:1设置了 “bounded”。所以两个类的总带宽不会超过6Mbps。别忘了,同一个CBQ下面的子类的主号码都必须与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的总带宽需求大于6Mbps,那么这6M带宽将按照两个类的weight参数的比例情况进行分割:WEB服务器得到5/8的带宽,SMTP得到3/8的带宽。从这个例子来说,可以这么认为:WEB数据流总是会得到5/8*6Mbps=3.75Mbps的带宽。

    *其它CBQ参数:split和defmap (split:分离、分裂)*

      如前所述,一个分类的队列规则需要调用过滤器来决定一个数据包应该发往哪个类去排队。

      除了调用过滤器,CBQ还提供了其他方式,defmap和split。很难掌握,但好在无关大局。但是现在是解释defmap和split的最佳时机,我会尽力解释。

      因为你经常是仅仅需要根据TOS来进行分类,所以提供了一种特殊的语法。当CBQ需要决定了数据包要在哪里入队时,要检查这个节点是否为“split节点”。如果是,子队列规则中的一个应该指出它接收所有带有某种优先权值的数据包,权值可以来自TOS字段或者应用程序设置的套接字选项。

      数据包的优先权位与defmap字段的值进行”或”运算来决定是否存在这样的匹配。换句话说,这是一个可以快捷创建仅仅匹配某种优先权值数据包的过滤器的方法。如果defmap等于0xff,就会匹配所有包,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..的数值对应它右面的bit。关于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现在应该有了这样一个表格:

      

      为了更有趣,你还可以传递一个“change掩码”,确切地指出你想改变哪个优先权值。你只有在使用了“tc class change”的时候才需要。比如,往1:2中添加best effort数据流,应该执行:

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

      现在,1:0上的优先权分布应该是:

      

    5.HTB(Hierarchical Token Bucket, 分层的令牌桶)

      Martin Devera意识到CBQ太复杂,而且并没有按照多数常见情况进行优化。他的Hierarchical能够很好地满足这样一种情况:你有一个固定速率的链路,希望分割给多种不同的用途使用。为每种用途做出带宽承诺并实现定量的带宽借用。

      HTB就象CBQ一样工作,但是并不靠计算闲置时间来整形。它是一个分类的令牌桶过滤器。它只有很少的参数,并且在它的网站能够找到很好的文档。

      随着你的HTB配置越来越复杂,你的配置工作也会变得复杂。但是使用CBQ的话,即使在很简单的情况下配置也会非常复杂!HTB3 (关于它的版本情况,请参阅它的网站)已经成了官方内核的一部分(2.4.20-pre1、2.5.31及其后)。然而,你可能仍然要为你的TC命令打上HTB3支持补丁,否则你的TC命令不理解HTB3。

      如果你已经有了一个新版内核或者已经打了补丁,请尽量考虑使用HTB。

    *配置范例*

      环境与要求与上述CBQ的例子一样:

      把WEB服务器的流量控制为5mbit、SMTP流量控制在3mbit上。而且二者一共不得超过6mbit,互相之间允许借用带宽。我们的网卡是100Mbps的。

      # 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,而可以公平发包。

使用过滤器对数据包进行分类

      为了决定用哪个类处理数据包,必须调用所谓的“分类器链” 进行选择。这个链中包含了这个分类队列规则所需的所有过滤器。

      重复前面那棵树:

      

      当一个数据包入队的时候,每一个分支处都会咨询过滤器链如何进行下一步。典型的配置是在1:1处有一个过滤器把数据包交给12:,然后12:处的过滤器在把包交给12:2。

      你可以把后一个过滤器同时放在1:1处,但是你可以通过在链路上进行更多特定的测试来使效率得以提高。

      另外,你不能用过滤器把数据包向“上”送。而且,使用HTB的时候应该把所有的规则放到根上!

      再次强调:数据包只能向“下”进行入队操作!只有出队的时候才会上到网卡所在的位置来。它们不会落到树的最底层后送到网卡!

    *过滤器的一些简单范例*

      下面,我们开始简单地匹配一些比较有明显特征的语法。

      比方说,我们有一个PRIO队列规则,叫做“10:”,包含3个类,我们希望把去往22口的数据流发送到最优先的频道中去。应该这样设置过滤器:

      # tc filter add dev eth0 protocol ip parent 10: prio 1 u32 match ip dport 22 0xffff flowid 10:1

      # tc filter add dev eth0 protocol ip parent 10: prio 1 u32 match ip sport 80 0xffff flowid 10:1

      # tc filter add dev eth0 protocol ip parent 10: prio 2 flowid 10:2

      意思是说:

      向eth0上的10:节点添加一个u32过滤规则,它的优先权是1:凡是去往22口(精确匹配)的IP数据包,发送到频道10:1。

      向eth0上的10:节点添加一个u32过滤规则,它的优先权是1:凡是来自80口(精确匹配)的IP数据包,发送到频道10:1。

      向eth0上的10:节点添加一个过滤规则,它的优先权是2:凡是上面未匹配的IP数据包,发送到频道10:2。

      别忘了添加“dev eth0”(你的网卡或许叫别的名字),因为每个网卡的句柄都有完全相同的命名空间。

      想通过IP地址进行筛选的话,可以使用以下命令:

      # tc filter add dev eth0 parent 10:0 protocol ip prio 1 u32 match ip dst 4.3.2.1/32 flowid 10:1

      # tc filter add dev eth0 parent 10:0 protocol ip prio 1 u32 match ip src 1.2.3.4/32 flowid 10:1

      # tc filter add dev eth0 protocol ip parent 10: prio 2 flowid 10:2

      这个例子把去往4.3.2.1和来自1.2.3.4的数据包送到了最高优先的队列,其它的则送到次高权限的队列。

      你可以连续使用match,想匹配来自1.2.3.4的80口的数据包的话,可以使用命令:

# tc filter add dev eth0 parent 10:0 protocol ip prio 1 u32 match ip src 4.3.2.1/32 match ip sport 80 0xffff flowid 10:1

    *常用到的过滤命令一览*

      这里列出的绝大多数命令都根据这个命令改编而来:

      # tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32

      这些是所谓的“u32”匹配,可以匹配数据包的任意部分。

      根据源/目的地址

      源地址段 match ip src 1.2.3.0/24

      目的地址段 match ip dst 4.3.2.0/24

      单个IP地址使用“/32”作为掩码即可。

      根据源/目的端口,所有IP协议

      源端口 match ip sport 80 0xffff

      目的端口 match ip dport 80 0xffff

      根据IP协议(tcp, udp, icmp, gre, ipsec)

      使用/etc/protocols所指定的数字。

      比如:icmp是 match ip protocol 1 0xff

      根据fwmark(防火墙标记功能)

      你可以使用ipchains/iptables给数据包做上标记,并且这个标记会在穿过网卡的路由过程中保留下来。如果你希望对来自eth0并从eth1发出的数据包做整形,这就很有用了。语法是这样的:

      #tc filter add dev eth1 protocol ip parent 1:0 prio 1 handle 6 fw flowid 1:1

      注意,这不是一个u32匹配!

      你可以象这样给数据包打标记:

      # iptables -A PREROUTING -t mangle -i eth0 -j MARK --set-mark 6(数字6是可以任意指定的)

      如果你不想去学习所有的tc语法,就可以与iptables结合,仅仅学习按fwmark匹配就行了。

      按TOS字段选择交互和最小延迟的数据流:

      # tc filter add dev ppp0 parent 1:0 protocol ip prio 10 u32 match ip tos 0x10 0xff flowid 1:4

      想匹配大量传输的话,使用“0x08 0xff”。

IMQ(Intermediate queueing device,中介队列设备)

      中介队列设备不是一个队列规则,但它的使用与队列规则是紧密相连的。

      就Linux而言,队列规则是附带在网卡上的,所有在这个网卡上排队的数据都排进这个队列规则。所以出现了两个局限:

      1.只能进行出口整形(虽然也存在入口队列规则,但在上面实现分类的队列规则的可能性非常小)。

      2.一个队列规则只能处理一块网卡的流量,无法设置全局的限速。

      IMQ就是用来解决上述两个局限的。简单地说,你可以往一个队列规则中放任何东西。被打了特定标记的数据包在netfilter的 NF_IP_PRE_ROUTING和NF_IP_POST_ROUTING两个钩子函数处被拦截,并被送到一个队列规则中,该队列规则附加到一个IMQ设备上。对数据包打标记要用到iptables的一种处理方法。

      这样你就可以对刚刚进入网卡的数据包打上标记进行入口整形,或者把网卡们当成一个个的类来看待而进行全局整形设置。你还可以做很多事情,比如:把http流量放到一个队列规则中去、把新的连接请求放到一个队列规则中去。

    *配置范例*

      我们首先想到的是进行入口整形,以便让你自己得到高保证的带宽。就像配置其它网卡一样:

      #tc qdisc add dev imq0 root handle 1: htb default 20

      #tc class add dev imq0 parent 1: classid 1:1 htb rate 2mbit burst 15k

      #tc class add dev imq0 parent 1:1 classid 1:10 htb rate 1mbit

      #tc class add dev imq0 parent 1:1 classid 1:20 htb rate 1mbit

      #tc qdisc add dev imq0 parent 1:10 handle 10: pfifo

      #tc qdisc add dev imq0 parent 1:20 handle 20: sfq

      #tc filter add dev imq0 parent 10:0 protocol ip prio 1 u32 match ip dst 10.0.0.230/32 flowid 1:10

      在这个例子中,使用了u32进行分类。其它的分类器应该也能实现。然后,被打上标记的包被送到imq0排队。

      #iptables -t mangle -A PREROUTING -i eth0 -j IMQ --todev 0

      #ip link set imq0 up

      iptables的IMQ处理方法只能用在PREROUTING和POSTROUTING链的mangle表中。语法是:

      IMQ [ --todev n ]

      n: imq设备的编号

      注:ip6tables也提供了这种处理方法。

      请注意,如果数据流是事后才匹配到IMQ处理方法上的,数据就不会入队。数据流进入imq的确切位置取决于这个数据流究竟是流进的还是流出的。下面是netfilter(也就是iptables)在内核中预先定义优先级:

enum nf_ip_hook_priorities {
    NF_IP_PRI_FIRST = INT_MIN,
    NF_IP_PRI_CONNTRACK = -200,
    NF_IP_PRI_MANGLE = -150,
    NF_IP_PRI_NAT_DST = -100,
    NF_IP_PRI_FILTER = 0,
    NF_IP_PRI_NAT_SRC = http://www.mamicode.com/100,>       对于流入的包,imq把自己注册为优先权等于NF_IP_PRI_MANGLE+1。也就是说数据包在经过了PREROUTING链的mangle表之后才进入imq设备。

      对于流出的包,imq使用优先权等于NF_IP_PRI_LAST,也就是说不会白白处理本应该被filter表丢弃的数据包。

Linux TC 带宽管理队列规则