Linux 网络堆栈中的扩展¶
简介¶
本文档描述了 Linux 网络堆栈中的一组互补技术,以提高多处理器系统的并行性和性能。
描述了以下技术
RSS:接收端缩放
RPS:接收数据包引导
RFS:接收流引导
加速接收流引导
XPS:发送数据包引导
RSS:接收端缩放¶
现代网卡支持多个接收和发送描述符队列(多队列)。在接收时,网卡可以将不同的数据包发送到不同的队列,以在 CPU 之间分配处理。网卡通过将过滤器应用于每个数据包来分配数据包,该过滤器将数据包分配给少量逻辑流之一。每个流的数据包被引导到单独的接收队列,而接收队列又可以由单独的 CPU 处理。这种机制通常被称为“接收端缩放”(RSS)。RSS 和其他缩放技术的目标是统一提高性能。多队列分配也可用于流量优先级排序,但这并非这些技术的重点。
RSS 中使用的过滤器通常是网络和/或传输层标头的哈希函数——例如,数据包的 IP 地址和 TCP 端口上的 4 元组哈希。RSS 最常见的硬件实现使用一个 128 项的间接表,其中每个条目存储一个队列号。数据包的接收队列由屏蔽掉数据包计算出的哈希的低七位(通常是 Toeplitz 哈希)来确定,将此数字作为间接表的键并读取相应的值。
某些网卡支持对称 RSS 哈希,如果 IP(源地址、目标地址)和 TCP/UDP(源端口、目标端口)元组被交换,则计算出的哈希是相同的。这在某些监视 TCP/IP 流(IDS、防火墙等)并且需要流的两个方向都落在同一个 Rx 队列(和 CPU)上的应用程序中是有益的。“Symmetric-XOR”和“Symmetric-OR-XOR”是 RSS 算法的类型,它们通过 XOR/OR IP 和/或 L4 协议的输入源和目标字段来实现这种哈希对称性。然而,这会导致输入熵减少,并可能被利用。
具体来说,“Symmetric-XOR”算法按如下方式对输入进行 XOR
# (SRC_IP ^ DST_IP, SRC_IP ^ DST_IP, SRC_PORT ^ DST_PORT, SRC_PORT ^ DST_PORT)
另一方面,“Symmetric-OR-XOR”算法按如下方式转换输入
# (SRC_IP | DST_IP, SRC_IP ^ DST_IP, SRC_PORT | DST_PORT, SRC_PORT ^ DST_PORT)
然后将结果馈送到底层 RSS 算法。
一些高级网卡允许根据可编程过滤器将数据包引导到队列。例如,绑定到 webserver 的 TCP 端口 80 数据包可以定向到它们自己的接收队列。可以使用 ethtool(--config-ntuple)配置此类“n 元组”过滤器。
RSS 配置¶
支持多队列的网卡的驱动程序通常提供一个内核模块参数,用于指定要配置的硬件队列的数量。例如,在 bnx2x 驱动程序中,此参数称为 num_queues。典型的 RSS 配置是为每个 CPU 提供一个接收队列(如果设备支持足够的队列),否则至少为每个内存域提供一个接收队列,其中内存域是一组共享特定内存级别(L1、L2、NUMA 节点等)的 CPU。
RSS 设备的间接表通过屏蔽哈希来解析队列,通常由驱动程序在初始化时进行编程。默认映射是将队列均匀地分布在表中,但可以使用 ethtool 命令(--show-rxfh-indir 和 --set-rxfh-indir)在运行时检索和修改间接表。可以修改间接表以赋予不同的队列不同的相对权重。
RSS IRQ 配置¶
每个接收队列都有一个与其关联的单独的 IRQ。当新数据包到达给定的队列时,网卡会触发它以通知 CPU。 PCIe 设备的信令路径使用消息信号中断 (MSI-X),可以将每个中断路由到特定的 CPU。队列到 IRQ 的活动映射可以从 /proc/interrupts 中确定。默认情况下,IRQ 可以在任何 CPU 上处理。由于数据包处理的很大一部分发生在接收中断处理中,因此在 CPU 之间分散接收中断是有利的。要手动调整每个中断的 IRQ 亲和性,请参阅SMP IRQ 亲和性。某些系统将运行 irqbalance,这是一个动态优化 IRQ 分配的守护程序,因此可能会覆盖任何手动设置。
建议的配置¶
当关注延迟或接收中断处理形成瓶颈时,应启用 RSS。在 CPU 之间分散负载会减少队列长度。对于低延迟网络,最佳设置是分配与系统中 CPU 数量一样多的队列(如果更低,则为网卡最大值)。最高效的高速率配置可能是具有最小数量接收队列的配置,其中没有接收队列由于饱和的 CPU 而溢出,因为在启用中断合并的默认模式下,中断(以及工作)的总数会随着每个额外的队列而增长。
可以使用 mpstat 实用程序观察每个 CPU 的负载,但请注意,在具有超线程 (HT) 的处理器上,每个超线程都表示为一个单独的 CPU。对于中断处理,HT 在初始测试中没有显示出任何好处,因此将队列数量限制为系统中 CPU 核心的数量。
专用 RSS 上下文¶
现代网卡支持创建多个共存的 RSS 配置,这些配置基于显式匹配规则进行选择。当应用程序想要约束接收流量的队列集(例如,对于特定目标端口或 IP 地址)时,这可能非常有用。下面的示例显示了如何将所有流量定向到 TCP 端口 22 到队列 0 和 1。
要创建额外的 RSS 上下文,请使用
# ethtool -X eth0 hfunc toeplitz context new
New RSS context is 1
内核报告回分配的上下文的 ID(默认的、始终存在的 RSS 上下文的 ID 为 0)。可以使用与默认上下文相同的 API 查询和修改新上下文
# ethtool -x eth0 context 1
RX flow hash indirection table for eth0 with 13 RX ring(s):
0: 0 1 2 3 4 5 6 7
8: 8 9 10 11 12 0 1 2
[...]
# ethtool -X eth0 equal 2 context 1
# ethtool -x eth0 context 1
RX flow hash indirection table for eth0 with 13 RX ring(s):
0: 0 1 0 1 0 1 0 1
8: 0 1 0 1 0 1 0 1
[...]
要使用新的上下文,请使用 n 元组过滤器将流量定向到它
# ethtool -N eth0 flow-type tcp6 dst-port 22 context 1
Added rule with ID 1023
完成后,删除上下文和规则
# ethtool -N eth0 delete 1023
# ethtool -X eth0 context 1 delete
RPS:接收数据包引导¶
接收数据包引导 (RPS) 在逻辑上是 RSS 的软件实现。在软件中,它必然在数据路径中稍后被调用。RSS 选择队列,从而选择将运行硬件中断处理程序的 CPU,而 RPS 选择 CPU 来执行中断处理程序之上的协议处理。这是通过将数据包放置在所需 CPU 的积压队列中并唤醒 CPU 进行处理来完成的。RPS 比 RSS 有一些优势
它可以与任何网卡一起使用
可以轻松添加软件过滤器以对新协议进行哈希处理
它不会增加硬件设备中断率(尽管它会引入处理器间中断 (IPI))
当驱动程序使用 netif_rx()
或 netif_receive_skb()
将数据包发送到网络堆栈时,会在接收中断处理程序的下半部分调用 RPS。 这些调用 get_rps_cpu() 函数,该函数选择应该处理数据包的队列。
确定 RPS 目标 CPU 的第一步是计算数据包地址或端口上的流哈希(2 元组或 4 元组哈希,具体取决于协议)。这用作数据包关联流的一致哈希。哈希要么由硬件提供,要么将在堆栈中计算。能够胜任的硬件可以在数据包的接收描述符中传递哈希;这通常是用于 RSS 的相同哈希(例如,计算出的 Toeplitz 哈希)。哈希保存在 skb->hash 中,并可以在堆栈中的其他地方用作数据包流的哈希。
每个接收硬件队列都有一个与其关联的 CPU 列表,RPS 可以将数据包排队到这些 CPU 以进行处理。对于每个接收到的数据包,从流哈希模列表大小计算出列表中的索引。索引的 CPU 是处理数据包的目标,并且数据包被排队到该 CPU 的积压队列的尾部。在下半部分例程结束时,将 IPI 发送到任何已将数据包排队到其积压队列的 CPU。 IPI 唤醒远程 CPU 上的积压处理,然后处理任何排队的数据包到网络堆栈。
RPS 配置¶
RPS 需要一个使用 CONFIG_RPS kconfig 符号编译的内核(默认情况下对于 SMP)。即使编译后,RPS 仍然处于禁用状态,直到明确配置为止。可以使用 sysfs 文件条目为每个接收队列配置 RPS 可以转发流量到的 CPU 列表
/sys/class/net/<dev>/queues/rx-<n>/rps_cpus
此文件实现了一个 CPU 的位图。当它为零(默认值)时,RPS 被禁用,在这种情况下,数据包在中断 CPU 上处理。SMP IRQ 亲和性解释了如何将 CPU 分配给位图。
建议的配置¶
对于单队列设备,典型的 RPS 配置是将 rps_cpus 设置为与中断 CPU 相同的内存域中的 CPU。如果 NUMA 局部性不是问题,那么这也可能是系统中的所有 CPU。在高中断率下,最好将中断 CPU 从映射中排除,因为它已经执行了大量工作。
对于多队列系统,如果配置了 RSS,以便将硬件接收队列映射到每个 CPU,那么 RPS 可能是冗余和不必要的。如果硬件队列的数量少于 CPU 的数量,那么如果每个队列的 rps_cpus 与该队列的中断 CPU 共享相同的内存域,则 RPS 可能会受益。
RPS 流限制¶
RPS 在不引入重新排序的情况下跨 CPU 扩展内核接收处理。将来自同一流的所有数据包发送到同一 CPU 的权衡是,如果流的数据包速率不同,则 CPU 负载会失衡。在极端情况下,单个流会控制流量。尤其是在具有许多并发连接的常见服务器工作负载上,这种行为表明存在问题,例如配置错误或欺骗源拒绝服务攻击。
流限制是一项可选的 RPS 功能,它通过在来自小流的数据包之前稍微丢弃来自大流的数据包来优先处理 CPU 争用期间的小流。它仅在 RPS 或 RFS 目标 CPU 接近饱和时才处于活动状态。一旦 CPU 的输入数据包队列超过最大队列长度的一半(如 sysctl net.core.netdev_max_backlog 所设置的),内核就会在最后 256 个数据包上启动每个流的数据包计数。如果一个流在新数据包到达时超过了这些数据包的设定比率(默认情况下,为一半),那么新数据包将被丢弃。来自其他流的数据包只有在输入数据包队列达到 netdev_max_backlog 时才会被丢弃。当输入数据包队列长度低于阈值时,不会丢弃任何数据包,因此流限制不会完全切断连接:即使是大型流也保持连接性。
接口¶
默认情况下,流限制是编译的(CONFIG_NET_FLOW_LIMIT),但未启用。它是为每个 CPU 独立实现的(以避免锁定和缓存争用),并通过在 sysctl net.core.flow_limit_cpu_bitmap 中设置相关位来按 CPU 切换。从 procfs 调用时,它会公开与 rps_cpus 相同的 CPU 位图接口(参见上文)
/proc/sys/net/core/flow_limit_cpu_bitmap
每个流的速率是通过将每个数据包哈希到哈希表存储桶中并递增每个存储桶的计数器来计算的。哈希函数与在 RPS 中选择 CPU 的哈希函数相同,但由于存储桶的数量可能远大于 CPU 的数量,因此流限制可以更细粒度地识别大型流并减少误报。默认表有 4096 个存储桶。可以通过 sysctl 修改此值
net.core.flow_limit_table_len
仅在分配新表时才咨询该值。修改它不会更新活动表。
建议的配置¶
流限制在具有许多并发连接的系统上非常有用,其中单个连接占用 CPU 的 50% 表明存在问题。在这种环境中,在处理网络 rx 中断的所有 CPU 上启用该功能(如 /proc/irq/N/smp_affinity 中设置的)。
该功能取决于输入数据包队列长度超过流限制阈值 (50%) + 流历史记录长度 (256)。在实验中,将 net.core.netdev_max_backlog 设置为 1000 或 10000 表现良好。
RFS:接收流引导¶
虽然 RPS 仅根据哈希引导数据包,因此通常提供良好的负载分配,但它没有考虑应用程序局部性。这是通过接收流引导 (RFS) 完成的。RFS 的目标是通过将数据包的内核处理引导到消耗数据包的应用程序线程正在运行的 CPU,来提高数据缓存命中率。RFS 依赖于相同的 RPS 机制将数据包排队到另一个 CPU 的积压中并唤醒该 CPU。
在 RFS 中,数据包不会直接通过其哈希值转发,而是使用哈希作为流查找表的索引。此表将流映射到正在处理这些流的 CPU。流哈希(参见上面的 RPS 部分)用于计算此表的索引。每个条目中记录的 CPU 是上次处理该流的 CPU。如果一个条目没有保存有效的 CPU,那么映射到该条目的数据包将使用普通的 RPS 进行引导。多个表条目可能指向同一个 CPU。实际上,对于许多流和少量 CPU,单个应用程序线程很可能处理具有许多不同流哈希的流。
rps_sock_flow_table 是一个全局流表,其中包含流的期望 CPU:当前正在用户空间中处理流的 CPU。每个表值都是一个 CPU 索引,该索引在调用 recvmsg 和 sendmsg 期间更新(具体来说,inet_recvmsg()、inet_sendmsg() 和 tcp_splice_read())。
当调度程序在线程在旧 CPU 上有未完成的接收数据包时将该线程移动到新的 CPU 时,数据包可能会乱序到达。为了避免这种情况,RFS 使用第二个流表来跟踪每个流的未完成数据包:rps_dev_flow_table 是一个特定于每个设备的每个硬件接收队列的表。每个表值存储一个 CPU 索引和一个计数器。CPU 索引表示当前 CPU,该流的数据包被排队到该 CPU 以进行进一步的内核处理。理想情况下,内核和用户空间处理发生在同一个 CPU 上,因此两个表中的 CPU 索引相同。如果调度程序最近迁移了用户空间线程,而内核仍然有数据包排队到旧 CPU 上的内核处理,则这可能是不正确的。
rps_dev_flow_table 值中的计数器记录了此流中的数据包上次排队时当前 CPU 的积压的长度。每个积压队列都有一个头计数器,该计数器在出队时递增。尾计数器计算为头计数器 + 队列长度。换句话说,rps_dev_flow[i] 中的计数器记录了已排队到当前为流 i 指定的 CPU 上的流 i 中的最后一个元素(当然,条目 i 实际上是由哈希选择的,并且多个流可能哈希到同一个条目 i)。
现在是避免乱序数据包的技巧:当从 get_rps_cpu() 选择用于数据包处理的 CPU 时,将比较 rps_sock_flow 表和数据包接收队列的 rps_dev_flow 表。如果流的期望 CPU(在 rps_sock_flow 表中找到)与当前 CPU(在 rps_dev_flow 表中找到)匹配,则数据包被排队到该 CPU 的积压中。如果它们不同,则如果满足以下条件之一,则将当前 CPU 更新为与期望 CPU 匹配
当前 CPU 的队列头计数器 >= rps_dev_flow[i] 中记录的尾计数器值
当前 CPU 未设置(>= nr_cpu_ids)
当前 CPU 处于脱机状态
在此检查之后,数据包被发送到(可能更新的)当前 CPU。这些规则旨在确保流仅在旧 CPU 上没有未完成的数据包时才移动到新的 CPU,因为未完成的数据包可能会比那些即将要在新 CPU 上处理的数据包晚到达。
RFS 配置¶
只有在启用了 kconfig 符号 CONFIG_RPS(默认情况下对于 SMP)时,RFS 才可用。该功能在显式配置之前保持禁用状态。全局流表中的条目数通过以下方式设置
/proc/sys/net/core/rps_sock_flow_entries
每个队列的流表中的条目数通过以下方式设置
/sys/class/net/<dev>/queues/rx-<n>/rps_flow_cnt
建议的配置¶
在为接收队列启用 RFS 之前,需要设置这两个值。两个值都向上舍入到最接近的 2 的幂。建议的流计数取决于任何给定时间预期的活动连接数,这可能远小于打开的连接数。我们发现 rps_sock_flow_entries 的值 32768 在中等负载的服务器上运行良好。
对于单队列设备,单队列的 rps_flow_cnt 值通常配置为与 rps_sock_flow_entries 相同的值。对于多队列设备,每个队列的 rps_flow_cnt 可以配置为 rps_sock_flow_entries / N,其中 N 是队列的数量。因此,例如,如果 rps_sock_flow_entries 设置为 32768 并且配置了 16 个接收队列,则每个队列的 rps_flow_cnt 可以配置为 2048。
加速 RFS¶
加速 RFS 之于 RFS 就像 RSS 之于 RPS:一种硬件加速负载平衡机制,它使用软状态根据消耗每个流的数据包的应用程序线程的运行位置来引导流。加速 RFS 的性能应优于 RFS,因为数据包直接发送到本地于消耗数据的线程的 CPU。目标 CPU 要么是应用程序运行的同一个 CPU,要么至少是在缓存层次结构中本地于应用程序线程 CPU 的 CPU。
为了启用加速 RFS,网络堆栈调用 ndo_rx_flow_steer 驱动程序函数来通信与特定流匹配的数据包的期望硬件队列。每次更新 rps_dev_flow_table 中的流条目时,网络堆栈都会自动调用此函数。然后,驱动程序使用设备特定方法来编程网卡以引导数据包。
流的硬件队列是从 rps_dev_flow_table 中记录的 CPU 派生的。堆栈查询 CPU 到硬件队列的映射,该映射由网卡驱动程序维护。这是 /proc/interrupts 显示的 IRQ 亲和性表的自动生成的反向映射。驱动程序可以使用 cpu_rmap(“CPU 亲和性反向映射”)内核库中的函数来填充映射。或者,驱动程序可以通过调用 netif_enable_cpu_rmap() 将 cpu_rmap 管理委托给内核。对于每个 CPU,映射中相应的队列设置为其处理 CPU 在缓存局部性中最近的队列。
加速 RFS 配置¶
仅当使用 CONFIG_RFS_ACCEL 编译内核并且 NIC 设备和驱动程序提供支持时,加速 RFS 才可用。它还要求通过 ethtool 启用 ntuple 过滤。CPU 到队列的映射是从为每个接收队列配置的 IRQ 亲和性自动推导出来的,因此不应需要额外的配置。
建议的配置¶
只要想要使用 RFS 并且 NIC 支持硬件加速,就应该启用此技术。
XPS:发送数据包引导¶
发送数据包引导是一种用于在多队列设备上发送数据包时智能选择要使用的发送队列的机制。这可以通过记录两种映射来实现,要么是 CPU 到硬件队列的映射,要么是接收队列到硬件发送队列的映射。
使用 CPU 映射的 XPS
此映射的目标通常是将队列专门分配给 CPU 子集,其中这些队列的发送完成在集合中的 CPU 上处理。这种选择提供了两个好处。首先,设备队列锁上的争用显着减少,因为更少的 CPU 争用同一队列(如果每个 CPU 都有自己的发送队列,则可以完全消除争用)。其次,发送完成时的缓存未命中率降低,特别是对于保存 sk_buff 结构的数据缓存行。
使用接收队列映射的 XPS
此映射用于根据管理员设置的接收队列映射配置来选择发送队列。一组接收队列可以映射到一组发送队列(多对多),尽管常见的用例是 1:1 映射。这将能够在发送和接收时在同一队列关联上发送数据包。这对于繁忙轮询多线程工作负载非常有用,在这种工作负载中,将给定的 CPU 与给定的应用程序线程相关联存在挑战。应用程序线程未固定到 CPU,并且每个线程处理在单个队列上接收的数据包。在此模型中,在与关联的接收队列相对应的同一发送队列上发送数据包在保持低 CPU 开销方面具有优势。发送完成工作被锁定到给定的应用程序正在轮询的同一队列关联中。这避免了在另一个 CPU 上触发中断的开销。当应用程序在繁忙轮询期间清理数据包时,发送完成可以与同一线程上下文中的数据包一起处理,因此可以降低延迟。
通过设置可以使用该队列进行发送的 CPU/接收队列的位图来为每个发送队列配置 XPS。反向映射,从 CPU 到发送队列或从接收队列到发送队列,为每个网络设备计算和维护。在发送流中的第一个数据包时,调用函数 get_xps_queue() 来选择队列。此函数使用套接字连接的接收队列 ID 来匹配接收队列到发送队列查找表。或者,此函数还可以使用正在运行的 CPU 的 ID 作为 CPU 到队列查找表的键。如果 ID 与单个队列匹配,则使用该队列进行发送。如果多个队列匹配,则通过使用流哈希来计算集合中的索引来选择一个队列。当基于接收队列映射选择发送队列时,不会针对接收设备验证发送设备,因为它需要在数据路径中进行昂贵的查找操作。
为发送特定流选择的队列保存在流的相应套接字结构中(例如,TCP 连接)。此发送队列用于在流上发送后续数据包,以防止乱序 (ooo) 数据包。此选择还在流中的所有数据包上分摊了调用 get_xps_queues() 的成本。为了避免 ooo 数据包,只有在为流中的数据包设置了 skb->ooo_okay 时,才能随后更改流的队列。此标志指示流中没有未完成的数据包,因此发送队列可以更改而不会产生乱序数据包的风险。传输层负责适当地设置 ooo_okay。例如,TCP 在连接的所有数据都已确认时设置该标志。
XPS 配置¶
只有在启用了 kconfig 符号 CONFIG_XPS(默认情况下对于 SMP)时,XPS 才可用。如果已编译,则在设备初始化时是否配置以及如何配置 XPS 取决于驱动程序。可以使用 sysfs 检查和配置 CPU/接收队列到发送队列的映射
对于基于 CPU 映射的选择
/sys/class/net/<dev>/queues/tx-<n>/xps_cpus
对于基于接收队列映射的选择
/sys/class/net/<dev>/queues/tx-<n>/xps_rxqs
建议的配置¶
对于具有单个传输队列的网络设备,XPS 配置无效,因为在这种情况下没有选择。在多队列系统中,最好将 XPS 配置为使每个 CPU 映射到一个队列。如果系统的队列数与 CPU 数相同,则每个队列也可以映射到一个 CPU,从而导致不存在争用的独占配对。如果队列数少于 CPU 数,则共享给定队列的最佳 CPU 可能是那些与处理该队列的发送完成(发送中断)的 CPU 共享缓存的 CPU。
对于基于接收队列的发送队列选择,必须显式配置 XPS,将接收队列映射到发送队列。如果接收队列映射的用户配置不适用,则发送队列基于 CPU 映射选择。
每个 TX 队列速率限制¶
这些是由 HW 实现的速率限制机制,目前支持 max-rate 属性,方法是将 Mbps 值设置为
/sys/class/net/<dev>/queues/tx-<n>/tx_maxrate
值为零表示禁用,这是默认值。
更多信息¶
RPS 和 RFS 在内核 2.6.35 中引入。 XPS 已合并到 2.6.38 中。原始补丁由 Tom Herbert 提交 (therbert@google.com)
加速 RFS 在 2.6.35 中引入。原始补丁由 Ben Hutchings 提交 (bwh@kernel.org)
作者
Tom Herbert (therbert@google.com)
Willem de Bruijn (willemb@google.com)