Linux 网络堆栈中的队列
数据包队列是任何网络堆栈或设备的核心组件。它们允许异步模块进行通信,提高性能,并具有影响延迟的副作用。本文旨在解释 IP 数据包在 Linux 网络堆栈的发送路径上排队的位置,诸如 BQL 之类有趣的新型减少延迟的功能如何运作,以及如何控制缓冲以减少延迟。

图 1. Linux 网络堆栈发送路径上队列的简化高级概述
驱动程序队列(又名环形缓冲区)在 IP 堆栈和网络接口控制器 (NIC) 之间是驱动程序队列。此队列通常实现为先进先出 (FIFO) 环形缓冲区 (http://en.wikipedia.org/wiki/Circular_buffer)——只需将其视为固定大小的缓冲区即可。驱动程序队列不包含数据包数据。相反,它由指向其他数据结构(称为套接字内核缓冲区 (SKB, http://vger.kernel.org/%7Edavem/skb.html))的描述符组成,这些缓冲区保存数据包数据并在整个内核中使用。

图 2. 部分满的驱动程序队列,其中描述符指向 SKB
驱动程序队列的输入源是 IP 堆栈,它将 IP 数据包排队。数据包可以本地生成,也可以在一个 NIC 上接收,然后在设备充当 IP 路由器时路由到另一个 NIC。IP 堆栈添加到驱动程序队列的数据包由硬件驱动程序出队,并通过数据总线发送到 NIC 硬件进行传输。
驱动程序队列存在的原因是确保系统有数据要传输时,NIC 可以立即使用它进行传输。也就是说,驱动程序队列为 IP 堆栈提供了一个位置,用于异步于硬件操作来排队数据。另一种设计方案是让 NIC 在物理介质准备好传输时向 IP 堆栈请求数据。由于响应此请求不可能瞬时完成,因此这种设计会浪费宝贵的传输机会,从而导致吞吐量降低。这种设计方法的反面是让 IP 堆栈在创建数据包后等待,直到硬件准备好传输。这也不是理想的,因为 IP 堆栈无法继续进行其他工作。
来自堆栈的巨型数据包大多数 NIC 都有固定的最大传输单元 (MTU),这是物理介质可以传输的最大帧。对于以太网,默认 MTU 为 1,500 字节,但某些以太网网络支持高达 9,000 字节的巨型帧 (http://en.wikipedia.org/wiki/Jumbo_frame)。在 IP 网络堆栈内部,MTU 可以表现为发送到设备进行传输的数据包大小的限制。例如,如果应用程序向 TCP 套接字写入 2,000 字节,则 IP 堆栈需要创建两个 IP 数据包,以使数据包大小小于或等于 1,500 MTU。对于大型数据传输,相对较小的 MTU 会导致创建大量小数据包并通过驱动程序队列传输。
为了避免与发送路径上大量数据包相关的开销,Linux 内核实现了多项优化:TCP 分段卸载 (TSO)、UDP 分片卸载 (UFO) 和通用分段卸载 (GSO)。所有这些优化都允许 IP 堆栈创建大于传出 NIC 的 MTU 的数据包。对于 IPv4,可以创建最大为 IPv4 最大值 65,536 字节的数据包并将其排队到驱动程序队列。在 TSO 和 UFO 的情况下,NIC 硬件负责将单个大数据包分解为足够小的数据包,以便在物理接口上传输。对于没有硬件支持的 NIC,GSO 在排队到驱动程序队列之前立即在软件中执行相同的操作。
回想一下,驱动程序队列包含固定数量的描述符,每个描述符都指向大小不同的数据包。由于 TSO、UFO 和 GSO 允许更大的数据包,因此这些优化具有大大增加驱动程序队列中可以排队的字节数的副作用。图 3 说明了与图 2 形成对比的这个概念。

图 3. 启用 TSO、UFO 或 GSO 时,可以将大数据包发送到 NIC。这可以大大增加驱动程序队列中的字节数。
尽管本文的重点是发送路径,但值得注意的是,Linux 具有接收端优化,其操作方式与 TSO、UFO 和 GSO 类似,并且具有减少每个数据包开销的共同目标。具体而言,通用接收卸载 (GRO, http://vger.kernel.org/%7Edavem/cgi-bin/blog.cgi/2010/08/30) 允许 NIC 驱动程序将接收到的数据包组合成单个大数据包,然后将其传递给 IP 堆栈。当设备转发这些大数据包时,GRO 允许重建原始数据包,这对于维护 IP 数据包流的端到端性质是必要的。但是,有一个副作用:当大数据包被分解时,会导致流的多个数据包一次排队。这种数据包的“微爆发”可能会对流间延迟产生负面影响。
饥饿和延迟尽管 IP 堆栈和硬件之间的队列是必需的并且有益,但它引入了两个问题:饥饿和延迟。
如果 NIC 驱动程序唤醒以从队列中拉取数据包进行传输,而队列为空,则硬件将错过传输机会,从而降低系统的吞吐量。这被称为饥饿。请注意,当系统没有任何要传输的内容时,队列为空不是饥饿——这是正常的。与避免饥饿相关的复杂性在于,填充队列的 IP 堆栈和清空队列的硬件驱动程序异步运行。更糟糕的是,填充或清空事件之间的时间间隔随系统负载和外部条件(例如网络接口的物理介质)而变化。例如,在繁忙的系统上,IP 堆栈获得向队列添加数据包的机会较少,这增加了硬件在更多数据包排队之前清空队列的可能性。因此,拥有一个非常大的队列以降低饥饿的可能性并确保高吞吐量是有利的。
虽然大型队列对于繁忙的系统保持高吞吐量是必要的,但它的缺点是允许引入大量延迟。
图 4 显示了一个驱动程序队列,它几乎已满,其中包含来自单个高带宽、大容量流量流(蓝色)的 TCP 段。最后排队的是来自 VoIP 或游戏流(黄色)的数据包。VoIP 或游戏等交互式应用程序通常以固定间隔发出小的延迟敏感数据包,而高带宽数据传输会生成更高的数据包速率和更大的数据包。这种更高的数据包速率会填充交互式数据包之间的队列,从而导致交互式数据包的传输延迟。

图 4. 交互式数据包(黄色)在大容量流量数据包(蓝色)之后
为了进一步说明这种行为,请考虑基于以下假设的场景
-
网络接口能够以 5 Mbit/秒或 5,000,000 位/秒的速度传输。
-
来自大容量流量的每个数据包为 1,500 字节或 12,000 位。
-
来自交互式流量的每个数据包为 500 字节。
-
队列的深度为 128 个描述符。
-
有 127 个大容量数据包和一个交互式数据包最后排队。
鉴于上述假设,清空 127 个大容量数据包并为交互式数据包创建传输机会所需的时间为 (127 * 12,000) / 5,000,000 = 0.304 秒(对于那些以 ping 结果考虑延迟的人来说,为 304 毫秒)。此延迟量远远超出交互式应用程序可接受的范围,这甚至不代表完整的往返时间——这只是传输排在交互式数据包之前的数据包所需的时间。如前所述,如果启用了 TSO、UFO 或 GSO,则驱动程序队列中的数据包大小可能大于 1,500 字节。这使得延迟问题相应地更加严重。
由超大、未管理的队列引入的大延迟称为缓冲膨胀 (http://en.wikipedia.org/wiki/Bufferbloat)。有关此现象的更详细解释,请参阅本文的资源。
正如以上讨论所示,为驱动程序队列选择正确的大小是一个金发姑娘问题——它不能太小,否则吞吐量会受到影响;它不能太大,否则延迟会受到影响。
字节队列限制 (BQL)字节队列限制 (BQL) 是最新 Linux 内核(> 3.3.0)中的一项新功能,旨在自动解决驱动程序队列大小调整问题。这是通过添加一个层来实现的,该层基于计算在当前系统条件下避免饥饿所需的最小队列大小来启用和禁用驱动程序队列的排队。回想一下,排队数据量越小,排队数据包体验的最大延迟就越低。
关键是要理解 BQL 不会更改驱动程序队列的实际大小。相反,BQL 计算在当前时间可以排队多少数据(以字节为单位)的限制。超过此限制的任何字节都必须由驱动程序队列上方的层持有或丢弃。
一个真实世界的例子可能有助于了解 BQL 对可以排队的数据量的影响。在作者的服务器之一上,驱动程序队列大小默认为 256 个描述符。由于以太网 MTU 为 1,500 字节,这意味着最多可以将 256 * 1,500 = 384,000 字节排队到驱动程序队列(TSO、GSO 等被禁用,否则会更高)。但是,BQL 计算的限制值为 3,012 字节。如您所见,BQL 大大限制了可以排队的数据量。
BQL 通过将驱动程序队列中的数据量限制为避免饥饿所需的最小值来减少网络延迟。它还具有重要的副作用,即将大多数数据包排队的点从简单的 FIFO 驱动程序队列移动到能够实现更复杂排队策略的排队规则 (QDisc) 层。
排队规则 (QDisc)驱动程序队列是一个简单的先进先出 (FIFO) 队列。它平等地对待所有数据包,并且没有区分不同流的数据包的能力。这种设计使 NIC 驱动程序软件保持简单和快速。请注意,更高级的以太网和大多数无线 NIC 支持多个独立的传输队列,但类似地,这些队列中的每一个通常都是 FIFO。更高层负责选择要使用的传输队列。
夹在 IP 堆栈和驱动程序队列之间的是排队规则 (QDisc) 层(图 1)。此层实现了 Linux 内核的流量管理功能,其中包括流量分类、优先级排序和速率整形。QDisc 层通过有点不透明的 tc 命令配置。在 QDisc 层中需要理解三个关键概念:QDisc、类和过滤器。
QDisc 是 Linux 对流量队列的抽象,它比标准 FIFO 队列更复杂。此接口允许 QDisc 执行复杂的队列管理行为,而无需修改 IP 堆栈或 NIC 驱动程序。默认情况下,每个网络接口都分配了一个 pfifo_fast QDisc (http://lartc.org/howto/lartc.qdisc.classless.html),它基于 TOS 位实现了一个简单的三频段优先级排序方案。尽管是默认设置,但 pfifo_fast QDisc 远非最佳选择,因为它默认具有非常深的队列(请参阅下面的 txqueuelen),并且不感知流。
第二个概念与 QDisc 密切相关,是类。单个 QDisc 可以实现类,以便以不同的方式处理流量子集——例如,分层令牌桶 (HTB, http://lartc.org/manpages/tc-htb.html)。QDisc 允许用户配置多个类,每个类具有不同的比特率,并根据需要将流量定向到每个类。并非所有 QDisc 都支持多个类。支持多个类的 QDisc 称为有类 QDisc,不支持的称为无类 QDisc。
过滤器(也称为分类器)是用于将流量定向到特定 QDisc 或类的机制。有许多不同复杂程度的过滤器。u32 过滤器 (http://www.lartc.org/lartc.html#LARTC.ADV-FILTER.U32) 是最通用的,而流过滤器是最容易使用的。
传输层和排队规则之间的缓冲在查看本文的图时,您可能已经注意到在 QDisc 层上方没有数据包队列。网络堆栈将数据包直接放入 QDisc,否则如果队列已满,则会向上层(例如,套接字缓冲区)推送。随之而来的明显问题是,当堆栈有大量数据要发送时会发生什么?当 TCP 连接具有较大的拥塞窗口时,或者更糟糕的是,当应用程序以最快的速度发送 UDP 数据包时,可能会发生这种情况。答案是,对于具有单个队列的 QDisc,驱动程序队列的图 4 中概述的相同问题会发生。也就是说,高带宽或高数据包速率的流可能会消耗队列中的所有空间,从而导致数据包丢失并为其他流增加显着的延迟。由于 Linux 默认为 pfifo_fast QDisc,它有效地具有单个队列(大多数流量都标记为 TOS=0),因此这种现象并不少见。
从 Linux 3.6.0 开始,Linux 内核具有一项名为 TCP 小队列的功能,旨在解决 TCP 的此问题。TCP 小队列在任何给定时间添加到 QDisc 和驱动程序队列中可以排队的字节数,添加了每个 TCP 流的限制。这具有有趣的副作用,即导致内核更早地向上层应用程序推送,这允许应用程序更有效地优先处理对套接字的写入。在撰写本文时,其他传输协议的单个流仍然有可能淹没 QDisc 层。
解决传输层泛洪问题的另一个部分解决方案(与传输层无关)是使用具有许多队列的 QDisc,理想情况下每个网络流一个队列。随机公平队列 (SFQ, http://crpppc19.epfl.ch/cgi-bin/man/man2html?8+tc-sfq) 和具有受控延迟的公平队列 (fq_codel, http://linuxmanpages.net/manpages/fedora18/man8/tc-fq_codel.8.html) QDisc 非常适合这个问题,因为它们有效地具有每个网络流的队列。
如何在 Linux 中操作队列大小驱动程序队列
ethtool 命令 (http://linuxmanpages.net/manpages/fedora12/man8/ethtool.8.html) 用于控制以太网设备的驱动程序队列大小。ethtool 还提供低级接口统计信息以及启用和禁用 IP 堆栈和驱动程序功能的能力。
-g
标志到 ethtool
显示驱动程序队列(环)参数
# 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
您可以从上面的输出中看到,此 NIC 的驱动程序默认为传输队列中的 256 个描述符。在缓冲膨胀调查的早期,通常建议减小驱动程序队列的大小以减少延迟。随着 BQL 的引入(假设您的 NIC 驱动程序支持它),不再有任何理由修改驱动程序队列大小(请参阅下文了解如何配置 BQL)。
ethtool 还允许您通过 -k
和 -K
标志查看和管理优化功能,例如 TSO、GSO、UFO 和 GRO。-k
标志显示当前的卸载设置,-K
标志修改它们。
如上所述,某些优化功能大大增加了驱动程序队列中可以排队的字节数。如果您想优化延迟而不是吞吐量,则应禁用这些优化。除非系统处理非常高的数据速率,否则禁用这些功能后,您可能不会注意到任何 CPU 影响或吞吐量下降。
字节队列限制 (BQL)
BQL 算法是自调优的,因此您可能不需要修改其配置。BQL 状态和配置可以在基于 NIC 的位置和名称的 /sys 目录中找到。例如:/sys/devices/pci0000:00/0000:00:14.0/net/eth0/queues/tx-0/byte_queue_limits。
要对可以排队的字节数设置硬上限,请将新值写入 limit_max 文件
echo "3000" > limit_max
什么是 txqueuelen?
在早期的缓冲膨胀讨论中,经常提到静态减小 NIC 传输队列的想法。ifconfig 命令输出中的 txqueuelen 字段或 ip 命令输出中的 qlen 字段显示了当前传输队列的大小
$ 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
$ 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
Linux 中传输队列的长度默认为 1,000 个数据包,这是一个很大的缓冲量,尤其是在低带宽下。
有趣的问题是此值控制哪个队列?有人可能会猜测它控制驱动程序队列大小,但实际上,它充当某些 QDisc 的默认队列长度。最重要的是,它是 pfifo_fast QDisc 的默认队列长度,它是默认值。tc 命令行上的“limit”参数可用于忽略 txqueuelen 默认值。
传输队列的长度使用 ip 或 ifconfig 命令配置
ip link set txqueuelen 500 dev eth0
排队规则
如前所述,Linux 内核有大量的排队规则 (QDisc),每个规则都实现了自己的数据包队列和行为。描述如何配置每个 QDisc 的详细信息超出了本文的范围。有关完整详细信息,请参阅 tc 手册页 (man tc
)。您可以在 man tc qdisc-name
中找到每个 QDisc 的详细信息(例如,man tc htb
或 man tc fq_codel
)。
TCP 小队列
可以使用以下 /proc 文件查看和控制每个套接字 TCP 队列限制:/proc/sys/net/ipv4/tcp_limit_output_bytes。
在任何正常情况下,您都不需要修改此值。
不受您控制的超大队列不幸的是,并非所有会影响您的 Internet 性能的超大队列都在您的控制之下。最常见的情况是问题出在连接到您的服务提供商的设备(例如 DSL 或有线调制解调器)或服务提供商的设备本身中。在后一种情况下,您无能为力,因为很难控制发送给您的流量。但是,在上游方向,您可以将流量整形到略低于链路速率。这将阻止设备中的队列拥有超过几个数据包。许多住宅家庭路由器都有速率限制设置,可用于将速率整形到低于链路速率。当然,如果您在家庭网关上使用 Linux,则可以利用 QDisc 功能进一步优化。网上有很多 tc 脚本示例可以帮助您入门。
总结数据包缓冲区中的队列是任何数据包网络(设备内部和跨网络元素)的必要组成部分。正确管理这些缓冲区的大小对于实现良好的网络延迟至关重要,尤其是在负载下。尽管静态队列大小调整可以在降低延迟方面发挥作用,但真正的解决方案是智能管理排队数据量。这最好通过动态方案来实现,例如 BQL 和主动队列管理 (AQM, http://en.wikipedia.org/wiki/Active_queue_management) 技术(如 Codel)。本文概述了数据包在 Linux 网络堆栈中排队的位置,与排队相关的功能如何配置,并提供了一些关于如何实现低延迟的指导。
致谢感谢 Kevin Mason、Simon Barber、Lucas Fontes 和 Rami Rosen 审阅本文并提供有益的反馈。
资源控制队列延迟:https://queue.org.cn/detail.cfm?id=2209336
缓冲膨胀:互联网中的黑暗缓冲区:http://cacm.acm.org/magazines/2012/1/144810-bufferbloat/fulltext
缓冲膨胀项目:http://www.bufferbloat.net
Linux 高级路由和流量控制指南 (LARTC):http://www.lartc.org/howto