排队准则(QDisc)
我们已经了解到,驱动队列只是简单的先入先出队列,它不能将来自不同数据流的包区分开来。这样的设计能使网卡驱动软件变得小巧并且有更高的效率。需要注意的是,一些更加先进的以太网和无线网卡可以支持多种相互独立的传送队列,但是它们实际上都是非常类似的先进先出队列而已。系统中更高层负责在其中选择一种队列进行使用。
夹在IP数据栈和驱动队列中间的,是排队准则(QDisc)层(见Figure1),它负责实现Linux内核中的(数据传输)交通管理功能,比如交通分类,优先级别和速率协商。QDisc层的配置比起其他层要更加困难一些。要了解QDisc层,必须了解三个非常重要的概念:排队规则,类和过滤器。
排队准则(QDisc)是Linux对交通队列的抽象,但是比“先入先出”要复杂一些。通过网络接口,QDisc能独立地进行复杂的队列管理,而不需要改变IP数据栈或者网卡驱动的运行模式。每个网络接口都会默认被设为pfifo_fast QDisc ,pfifo_fast QDisc在TOS的基础上,实现了简单的三带优先机制(three band prioritization scheme)。虽然是默认机制,但是这并不代表它是最好的机制,因为pfifo_fast QDisc的队列深度很大(在下面txqueuelen中会有解释),并且它也不能区分不同数据流。
第二个和QDisc紧密相关的概念是类。单个QDisc能通过实现不同类,对数据中的不同部分进行不同处理。例如,层级表示桶(the Hierarchical Token Bucket (HTB))QDisc,能使用户配置500Kbps 和 300Kbps的类,并且控制数据使之进入用户希望其进入的类中。并不是所有QDisc都实现这种功能,我们一般把能够这样分类的QDisc称为——可分类的QDiscs。
过滤器(也叫分类器)是一种分类的机制,用来将数据分类到特定QDisc或类中。过滤器的种类很多,它们的复杂度也都不相同。u32 是其中最普通也最容易使用的一种。现在还没有针对u32的文档,但是你可以在这里找到一个例子one of my QoS scripts。
在LARTC HOWTO and the tc man pages,你可以了解到更多关于QDiscs、类和过滤器的细节。
传输层和排队准则间的缓存
你可能已经注意到了,在原来的图例中,排队准则层之上就没有数据包的队列了。这意味着网络数据栈要么直接把数据包放入排队准则中,要么把数据包推回它的上一层(比如套接字缓存的情况),如果这时队列已满的话。接下来一个很明显的问题是,当栈有很多数据包要发送的时候该怎么办?当TCP的拥塞窗口很大,或者某个应用正在快速发送UDP数据包时,就会出现要发送很多数据包的情况。对于只有一个队列的QDisc来说,类似问题在图例4中就已经出现过了。这里的问题是,一个带宽很大或者包速率很大的数据流会把队列里的所有空间占满,造成数据包丢失和延时的问题。更糟的是,这样很可能会使另一个缓存产生,进而产生一个静止队列(standing queue),造成更严重的延时并使TCP的RTT和拥塞窗口的计算出现问题。由于Linux默认使用仅有一个队列的pfifo_fast QDisc(因为大多数数据流的TOS=0),所以这样的问题非常常见。
Linux3.6.0(2012-09-30)出现后,Linux内核增加了TCP小队列(TCP small queue)的机制,用于解决该问题。TCP小队列对每个TCP数据流中,能够同时参与排队的字节数做出了限制。这样会产生意想不到的作用,能使内核将数据推回原先的应用,使得应用能更有效地优先处理写入套接字的请求。目前(2012-12-28),除TCP之外的其他传输协议的单个数据流,还是有可能阻塞QDsic层的。
另一种能部分解决传输层阻塞的办法,是使用有许多队列的QDsic层,最好是对每一个数据流都能有一个队列。SFQ和fq_codel的QDsic都能够很好地解决这个问题,使每个数据流都分到一个队列。
如何控制Linux中的队列长度
驱动队列
对于以太网设备,可以用ethtool命令来控制队列长度。ethtool提供底层的接口数据,并能控制IP数据栈和驱动的各种特性。
-g 能够将驱动队列的参数展示出来:
1 2 3 4 5 6 7 8 9 10 11 12 |
[root@alpha net-next]# ethtool -g eth0 Ring parameters for eth0: Pre-set maximums: RX: 16384 RX Mini: 0 RX Jumbo: 0 TX: 16384 Current hardware settings: RX: 512 RX Mini: 0 RX Jumbo: 0 TX: 256 |
从上面可以看出,网卡驱动默认在传输队列中有256个描述符。前面我们提到过,为了免缓存丢包等情况,应该减小驱动队列的大小,这同时也能减小延时。引入BQL后,就没有必要再去调整驱动队列的大小了(下文中有配置BQL的介绍)。
ethtool也能用于调整优化特性,如TSO、UFO和GSO。-k 指令能展示出现在卸货(offload)状态,-K能调整这些状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[dan@alpha ~]$ ethtool -k eth0 Offload parameters for eth0: rx-checksumming: off tx-checksumming: off scatter-gather: off tcp-segmentation-offload: off udp-fragmentation-offload: off generic-segmentation-offload: off generic-receive-offload: on large-receive-offload: off rx-vlan-offload: off tx-vlan-offload: off ntuple-filters: off receive-hashing: off |
因为TSO, GSO, UFO 和 GRO大大增加了能够参与排队的字节数,如果相比于吞吐量你更在意延时的话,你就应该禁用这些优化。一般来说禁用这些优化,不会使你感到CPU和吞吐量收到了影响,除非你的系统要处理的数据率很大。
字节队列限制(BQL)
BQL算法能够自我适应,所以应该没什么必要去经常调整它。但是,如果你很关注最大在低码率上的最大延时的话,可能你会想要比现有LIMIT更大的上限值。在/sys 的目录中可以找到配置BQL的文件,目录的具体地址取决于网卡的位置和名字。在我的服务器上,eth0的目录是:
1 |
/sys/devices/pci0000:00/0000:00:14.0/net/eth0/queues/tx-0/byte_queue_limits |
里面的文件有:
hold_time:修改LIMIT间的时间(millisecond为单位)。
inflight:参与排队但没有发送的字节数。
limit:BQL计算出的LIMIT值。如果该网卡不支持BQL机制,则为0。
limit_max:能够修改的LIMIT最大值。调低该值能减小延时。
limit_min:能够修改的LIMIT最小值。调高该值能增大吞吐量。
为能够参与排队的字节数设置一个上限,改一下limit_max 文件就行了:
1 |
echo "3000" > limit_max |
什么是txqueuelen?
在前面我们已经提到了减小网卡传输队列大小的作用。当前的队列大小能够通过ip和ifconfig命令获得。但是令人困惑地是,两个命令得到的队列长度不同:
1 2 3 4 5 6 7 8 9 10 |
[dan@alpha ~]$ ifconfig eth0 eth0 Link encap:Ethernet HWaddr 00:18:F3:51:44:10 inet addr:69.41.199.58 Bcast:69.41.199.63 Mask:255.255.255.248 inet6 addr: fe80::218:f3ff:fe51:4410/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:435033 errors:0 dropped:0 overruns:0 frame:0 TX packets:429919 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:65651219 (62.6 MiB) TX bytes:132143593 (126.0 MiB) Interrupt:23 |
1 2 3 4 5 |
[dan@alpha ~]$ ip link 1: lo: mtu 16436 qdisc noqueue state UNKNOWN link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000 link/ether 00:18:f3:51:44:10 brd ff:ff:ff:ff:ff:ff |
默认的传输队列长度为1000包,在低带宽下这已经是很大的缓存了。
有趣的是,这个变量到底控制着什么东西呢?我以前也不知道,于是花了很多时间阅读内核代码。就我所知,txqueuelen只是丢某些排队准则的默认队列长度。这些排队准则有:
pfifo_fast (Linux default queueing discipline)
sch_fifo
sch_gred
sch_htb (only for the default queue)
sch_plug
sch_sfb
sch_teql
再来看图例1,txqueuelen参数控制着上面准则的队列大小。对于大多数其中的队列,tc命令行中的limit参数,都超过了txqueuelen的默认大小。总之,如果你不使用上面的队列准则,或者队列长度超过了exqueuelen大小,txqueuelen都是没有意义的。
再说一句,还有一点使我非常困惑,ifconfig命令显示的是如mac地址一类的底层网络接口细节,但是txqueuelen显示的却是更高层的QDisc层的信息。如果ifconfig显示驱动队列的大小可能会更合理些。
通过ip或者ifconfig命令能够控制传输队列的长度:
1 |
[root@alpha dan]# ip link set txqueuelen 500 dev eth0 |
注意,ip命令使用 ‘txqueuelen’,但在显示接口细节时使用的是‘qlen’。
队列准则
前面介绍过,Linux 内核有很多QDsic,每个QDisc有自己的包队列排队方法。在这篇文章里描述清楚如何配置这些QDisc的细节是不可能的。想要了解所有细节,可以参考man page(man tc)。在‘man tc qdisc-name’ (ex: ‘man tc htb’ or ‘man tc fq_codel’)中,能够找到每个QDisc的细节。LARTC也能找到许多有用的资源,但是缺少一些特性的资料。
下面是一些使用的tc命令的小技巧:
如果数据包没有经过过滤器分类,HTBQDisc的默认队列会接收所有的数据包。其他QDisc比如DRR会把所有未分类的数据都接收进来。可以通过“tc qdisc show”中的direct_packets_stat查看到所有没有分类然后直接进入到队列中的数据包。
HTB类仅对没有带宽分配的分类起作用。所有带宽分配会在查看它们的叶子和相关的优先级时发生。
QDisc设施(infrastructure)会鉴别出带有大小数目(major and minor numbers )QDisc和类,它们被冒号分开。大数(major number)是QDisc的标识符,小数(minor number)标识QDisc中的类。tc命令使用十六进制来表示这些数字。因为很多字符串在十六和十进制中都一样(10以内),所以许多用户一直不知道tc使用十六进制表示法。可以在my tcscripts里看看我是如何对付它的。
如果你使用的是ADSL,并且基于ATM(几乎所有DSL服务都是基于ATM的,但也有例外,比如基于VDSL2的),你很可能会想要加入“linklayer adsl”选项。它的意思是将IP数据包分解成53个字节大小的ATM块的上限。
如果你正在使用PPPoE,那你很可能会想通过‘overhead’参数来调整PPPoE的上限。
TCP 小队列
每个套接字TCP队列的大小限制,可以通过下面的/proc文件来查看和控制:
1 |
/proc/sys/net/ipv4/tcp_limit_output_bytes |
我觉得一般情况下都不需要修改这个文件。
你控制不了的超大队列
不幸的是,并不是所有影响网络性能超大队列都能够被我们控制。很常见的是,问题往往出在服务提供商提供的装置和其他配套的设备(比如DSL或者电缆调制器)上。当服务提供商的装置本身有问题时,就没什么办法了,因为你不可能控制向你传输的数据。但是,在逆向上,你可以控制数据流使之比链路率(link rate)稍微低些。这会让装置中的队列不会有几对数据包出现。很多家庭路由器都有链路率限制的功能,你能够通过它来控制你的数据流低于链路率。如果你将Linux Box作为路由器,控制数据流同时也会使内核的排队机制运行地更加高效。你能找到很多在线的tc脚本,比如the one I use with some related performance results。
概要
在每个使用分组交换的网络中,数据包缓存的排队机制都是非常必要的。控制这些缓存的包大小是至关重要的,能直接影响到网络延时等问题。虽然静态的缓存大小分配对于降低延时也很有效,但是更好的方法还是用更加智能的方法动态控制包的大小。实现动态控制的最好方法是使用动态机制,比如BQL和active queue management(AQM)技术,比如Codel。本文描述了数据包时如何在Linux的网络栈中进行排队的,并且介绍了相关特性的配置方法,给出了降低延时的建议。