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)上的应用程序中很有用。“对称-XOR”是一种 RSS 算法,通过对 IP 和/或 L4 协议的输入源字段和目标字段进行异或运算来实现这种哈希对称性。但是,这会导致输入熵减少,并且可能会被利用。具体来说,该算法按以下方式对输入进行异或运算
# (SRC_IP ^ DST_IP, SRC_IP ^ DST_IP, SRC_PORT ^ DST_PORT, SRC_PORT ^ DST_PORT)
然后将结果馈送到基础 RSS 算法。
一些高级网卡允许根据可编程过滤器将数据包导向队列。例如,绑定到 Web 服务器的 TCP 端口 80 数据包可以被定向到其自己的接收队列。此类“n 元组”过滤器可以使用 ethtool (–config-ntuple) 进行配置。
RSS 配置¶
支持多队列的网卡的驱动程序通常提供一个内核模块参数,用于指定要配置的硬件队列的数量。例如,在 bnx2x 驱动程序中,此参数名为 num_queues。典型的 RSS 配置是为每个 CPU 设置一个接收队列(如果设备支持足够的队列),或者至少为每个内存域设置一个接收队列,其中内存域是一组共享特定内存级别(L1、L2、NUMA 节点等)的 CPU。
RSS 设备的间接表(通过掩码哈希解析队列)通常由驱动程序在初始化时进行编程。默认映射是将队列均匀地分配到表中,但是可以使用 ethtool 命令(–show-rxfh-indir 和 –set-rxfh-indir)在运行时检索和修改间接表。修改间接表可以为不同的队列赋予不同的相对权重。
RSS IRQ 配置¶
每个接收队列都有一个与之关联的单独的 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。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 上排队等待内核处理的数据包,则这很可能是不正确的。
rps_dev_flow_table 值中的计数器记录了当此流中的数据包上次排队时当前 CPU 的积压长度。每个积压队列都有一个在出队时递增的头计数器。尾计数器计算为头计数器 + 队列长度。换句话说,rps_dev_flow[i] 中的计数器记录了已排队到流 i 当前指定的 CPU 的流 i 中的最后一个元素(当然,条目 i 实际上是通过哈希选择的,并且多个流可能会哈希到同一个条目 i)。
现在是避免乱序数据包的技巧:当选择用于数据包处理的 CPU(从 get_rps_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 中的流条目时,网络堆栈都会自动调用此函数。驱动程序反过来使用设备特定的方法来编程 NIC 以控制数据包。
流的硬件队列来自 rps_dev_flow_table 中记录的 CPU。堆栈会查询 NIC 驱动程序维护的 CPU 到硬件队列映射。这是 /proc/interrupts 显示的 IRQ 亲和性表的自动生成的反向映射。驱动程序可以使用 cpu_rmap(“CPU 亲和性反向映射”)内核库中的函数来填充映射。对于每个 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 上触发中断的开销。 当应用程序在繁忙轮询期间清理数据包时,传输完成可能会在同一线程上下文中与它一起处理,从而降低延迟。
XPS 通过设置可以使用该队列传输的 CPU/接收队列的位图,为每个传输队列配置。 反向映射,从 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 实现的速率限制机制,目前通过将 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)