Receive Packet Steering (RPS)
回想一下我们之前讨论的网络设备驱动程序注册 NAPI poll
函数的过程。 每个 NAPI
轮询器实例在软中断的上下文中执行,每个 CPU 有一个软中断。 进一步回想一下,驱动程序的 IRQ 处理程序运行的 CPU 将唤醒其 softirq 处理循环来处理数据包。
换句话说:单个 CPU 处理硬件中断并轮询数据包以处理输入数据。
某些 NIC(如Intel I350)在硬件级别支持多个队列。 这意味着传入的数据包可以被 DMA 到每个队列的单独的内存区域,并且还具有单独的 NAPI 结构来管理轮询该区域。 因此,多个 CPU 将处理来自设备的中断,并且还处理数据包。
该特征通常被称为 Receive Side Scaling (RSS)。
Receive Packet Steering (RPS) 是 RSS 的软件实现。 由于它是在软件中实现的,这意味着它可以为任何 NIC 启用,即使是只有单个接收队列的 NIC。 然而,由于它是在软件中,这意味着 RPS 只能在已经从 DMA 存储器区域收取数据包之后进入流。
这意味着您不会注意到处理 IRQ 或 NAPI poll
循环所花费的 CPU 时间减少,但您可以在收集数据包后分布处理数据包的负载,并减少网络栈上的 CPU 时间。
RPS 的工作原理是为传入数据生成一个散列,以确定哪个 CPU 应该处理数据。 然后排队数据到每 CPU 接收网络积压中以进行处理。 处理器间中断(IPI)被传送到拥有积压的 CPU。 如果当前没有处理积压工作中的数据,这有助于启动积压工作处理。 /proc/net/softnet_stat
包含每个 softnet_data
结构体接收 IPI(received_rps
字段)的次数计数。
因此,netif_receive_skb
将继续向网络栈发送网络数据,或者将其移交给 RPS 以在不同的 CPU 上进行处理。
调优:启用 RPS
要使 RPS 工作,必须在内核配置中启用它(Ubuntu 内核 3.13.0 是启用的),并使用位掩码描述哪些 CPU 应该处理给定接口和接收队列的数据包。
您可以在内核文档中找到有关这些位掩码的一些文档。
简而言之,要修改的位掩码位于:
/sys/class/net/DEVICE_NAME/queues/QUEUE/rps_cpus
因此,对于 eth0
和 接收队列 0,你将修改 /sys/class/net/eth0/queues/rx-0/rps_cpus
文件,其中十六进制数指示哪些 CPU 应处理来自 eth0
的接收队列 0 的数据包。 正如文档指出的,在某些配置中可能不需要 RPS。
注: 启用 RPS 将数据包处理分配给以前未处理数据包的 CPU,将导致该 CPU 的 NET_RX
软中断数增加,以及 CPU 使用率图中的 si
或 sitime
增加。 您可以比较软中断和 CPU 使用率图表的前后对比,以确认 RPS 配置是否符合您的喜好。
Receive Flow Steering (RFS)
Receive flow steering (RFS) 与 RPS 配合使用。 RPS 尝试在多个 CPU 之间分配传入数据包负载,但不考虑任何数据局部性问题以最大化 CPU 缓存命中率。 您可以使用 RFS 定向同一个流的数据包到同一个 CPU 进行处理,从而帮助提高缓存命中率。
调优:启用 RFS
要使 RFS 工作,您必须启用并配置 RPS。
RFS 跟踪所有流的全局哈希表,并且可以设置 net.core.rps_sock_flow_entries
sysctl 来调整该哈希表的大小。
设置 sysctl
增加 RFS 套接字流哈希的大小。
$ sudo sysctl -w net.core.rps_sock_flow_entries=32768
接下来,您还可以设置每个接收队列的流数,方法是写入此值每个接收队列的名为rps_flow_cnt
的 sysfs 文件。
示例:增加 eth0 上接收队列 0 的流数到 2048。
$ sudo bash -c 'echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt'
硬件加速 Receive Flow Steering (aRFS)
RFS 可以使用硬件加速来加速;NIC 和内核可以一起工作以确定哪些流应该在哪些 CPU 上被处理。 要使用此功能,NIC 和驱动程序必须支持此功能。
请参阅您的网卡数据手册以确定是否支持此功能。 如果您的 NIC 驱动程序公开了一个名为 ndo_rx_flow_steer
的函数,则该驱动程序支持加速 RFS。
调优:启用加速 RFS(aRFS)
假设您的 NIC 和驱动程序支持它,您可以启用和配置一组内容来启用加速 RFS:
- 启用并配置 RPS。
- 启用并配置 RFS。
- x在编译内核时启用
CONFIG_RFS_ACCEL
。 Ubuntu kernel 3.13.0 启用 - 如前所述,为设备启用 ntuple 支持。 您可以使用
ethtool
来验证是否为设备启用了ntuple 支持。 - 配置 IRQ 设置以确保每个接收队列由所需的网络处理 CPU 之一处理。
一旦配置了上述内容,加速 RFS 自动移动数据到与处理该流数据的CPU核心绑定的接收队列,并且您不需要为每个流手动指定 ntuple 过滤规则。
使用 netif_receive_skb
向上移动网络栈。
接着我们上次讲到的 netif_receive_skb
,它从几个地方调用。最常见的两个(也是我们已经看过的两个):
- 如果数据包不会合并到现有的 GRO 流中,则为
napi_skb_finish
,或者 - 如果协议层指示现在是刷新流的时候,则为
napi_gro_complete
,或者
提醒:netif_receive_skb
及其后代在 softirq 处理循环的上下文中运行,使用像 top
这样的工具,您将看到这里花费的时间计入 sitime
或 si
。
netif_receive_skb
首先检查一个 sysctl
值,以确定用户是否在数据包进入积压队列之前或之后请求接收时间戳。如果启用了此设置,则在数据进入 RPS(和 CPU 的关联积压队列)之前对数据进行时间戳。如果禁用了此设置,则在进入队列后进行时间戳。如果启用了 RPS,则可以使用此功能在多个 CPU 之间分配时间戳的负载,但会因此引入一些延迟。
调优:接收数据包时间戳
您可以调整一个名为 net.core.netdev_tstamp_prequeue
的 sysctl 来调优接收到数据包后的时间戳:
调整 sysctl
禁用接收数据包的时间戳
$ sudo sysctl -w net.core.netdev_tstamp_prequeue=0
默认值为 1。 请参阅上一节的解释,以了解此设置的确切含义。
netif_receive_skb
处理完时间戳后,netif_receive_skb
的操作方式会因 RPS 是否启用而不同。 让我们先从更简单的路径开始:RPS 已禁用。
RPS 禁用(默认设置)
如果未启用 RPS,则调用 __netif_receive_skb
,它执行一些簿记工作,然后调用 __netif_receive_skb_core
移动数据到协议栈附近。
我们将看到 __netif_receive_skb_core
的工作原理,但首先让我们看看启用 RPS 的代码路径如何工作,因为该代码也将调用 __netif_receive_skb_core
。
RPS 启用
如果启用了 RPS,在处理上述提到的时间戳选项之后,netif_receive_skb
将执行一些计算,以确定应使用哪个 CPU 的积压队列。这是使用 get_rps_cpu
函数完成的。来自 net/core/dev.c:
cpu = get_rps_cpu(skb->dev, skb, &rflow); if (cpu >= 0) { ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail); rcu_read_unlock(); return ret; }
get_rps_cpu
将考虑上述 RFS 和 aRFS 设置,以确保调用 enqueue_to_backlog
排队数据到所需的 CPU 的 backlog。
enqueue_to_backlog
此函数首先获取指向远程 CPU 的 softnet_data
结构指针,该结构包含指向input_pkt_queue
的指针。 接下来,检查远程 CPU 的 input_pkt_queue
。 来自 net/core/dev.c:
qlen = skb_queue_len(&sd->input_pkt_queue); if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) {
首先比较 input_pkt_queue
的长度与 netdev_max_backlog
。如果队列长度大于此值,则丢弃数据。同样,检查流量限制,如果超过了流量限制,则丢弃数据。在这两种情况下,都会增加 softnet_data
结构的丢弃计数。请注意,这是数据将要排队到的 CPU 的 softnet_data
结构。阅读上面关于 /proc/net/softnet_stat
的部分,基于监控的目的了解如何获取丢弃计数。
enqueue_to_backlog
在很少地方调用。它用于已启用 RPS 的数据包处理,也从 netif_rx
调用。大多数驱动程序都 不应 使用 netif_rx
,而应使用 netif_receive_skb
。如果您没有使用 RPS 并且您的驱动程序没有使用 netif_rx
,则增加积压不会对您的系统产生任何明显影响,因为它不会被使用。
注意:您需要检查正在使用的驱动程序。如果它调用了 netif_receive_skb
并且您 没有 使用 RPS,则增加 netdev_max_backlog
将不会产生任何性能改进,因为没有任何数据会进入 input_pkt_queue
。
假设 input_pkt_queue
足够小且未达到流量限制(接下来会详细介绍),则可以排队数据。这里的逻辑有点奇怪,但可以总结为:
- 如果队列为空:检查远程 CPU 上是否已启动 NAPI。如果没有,则检查是否已排队发送 IPI。如果没有,则排队一个并调用
____napi_schedule
启动 NAPI 处理循环。继续排队数据。 - 如果队列不为空,或者前面描述的操作已完成,则将数据入队。
这段代码使用了 goto
,所以要仔细阅读。 来自 net/core/dev.c:
if (skb_queue_len(&sd->input_pkt_queue)) { enqueue: __skb_queue_tail(&sd->input_pkt_queue, skb); input_queue_tail_incr_save(sd, qtail); rps_unlock(sd); local_irq_restore(flags); return NET_RX_SUCCESS; } /* Schedule NAPI for backlog device * We can use non atomic operation since we own the queue lock */ if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state)) { if (!rps_ipi_queued(sd)) ____napi_schedule(sd, &sd->backlog); } goto enqueue;
流量限制
RPS 可以在多个 CPU 之间分配数据包处理负载,但是单个大流量可能会垄断 CPU 处理时间并使较小的流量饥饿。流量限制是一种功能,限制每个流量排队到积压的数据包数量为一定数量。这有助于确保即使大流量推送数据包,也能处理较小的流量。
上面来自 net/core/dev.c 的 if 语句调用 skb_flow_limit
检查流量限制:
if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) {
这段代码检查队列中是否还有空间,以及是否尚未达到流量限制。 默认情况下,禁用流量限制。 要启用流量限制,必须指定位图(类似于 RPS 的位图)。
监控:监控 input_pkt_queue
已满或流量限制导致的丢弃
请参阅上面有关监视/proc/net/softnet_stat
。 dropped
字段是一个计数器,每次数据被丢弃而不是排队到CPU的 input_pkt_queue
时,它都会递增。
调优
调优:调优 netdev_max_backlog
防止丢弃
在调整此调优值之前,请参阅上一节中的注释。
如果使用 RPS 或驱动程序调用 netif_rx
,则可以增加 netdev_max_backlog
来帮助防止 enqueue_to_backlog
的丢弃。
示例:使用 sysctl
增加 backlog 到 3000。
$ sudo sysctl -w net.core.netdev_max_backlog=3000
默认值为 1000。
调优:调优 backlog 的 NAPI poll
权重
您可以设置 net.core.dev_weight
sysctl 来调整积压的 NAPI 轮询器的权重。调整此值可以确定积压 poll
循环可以消耗的总预算的多少(请参阅上面调整 net.core.netdev_budget
的部分):
示例:使用 sysctl
增加 NAPI poll
积压处理循环。
$ sudo sysctl -w net.core.dev_weight=600
默认值为 64。
记住,backlog 处理运行在 softirq 上下文,类似于设备驱动程序注册的 poll
函数,并且将受到总 budget
和时间的限制,如前几节所述。
调优:启用流量限制并调优流量限制哈希表大小
使用 sysctl
设置流量限制表的大小。
$ sudo sysctl -w net.core.flow_limit_table_len=8192
默认值为 4096。
此更改仅影响新分配的流哈希表。 因此,如果您想增加表的大小,应该在启用流量限制之前进行。
要启用 流量限制,您应该在 /proc/sys/net/core/flow_limit_cpu_bitmap
中指定一个位掩码,该位掩码类似于 RPS 位掩码,指示哪些 CPU 启用了流量限制。
backlog 队列 NAPI 轮询器
每个 CPU 的 backlog 队列插入 NAPI 的方式与设备驱动程序相同。提供了一个 poll
函数,从 softirq 上下文处理数据包。就像设备驱动程序一样,还提供了一个 weight
。
这个 NAPI 结构在初始化网络系统时提供。来自 net/core/dev.c
中的 net_dev_init
:
sd->backlog.poll = process_backlog; sd->backlog.weight = weight_p; sd->backlog.gro_list = NULL; sd->backlog.gro_count = 0;
backlog NAPI 结构与设备驱动程序 NAPI 结构的不同之处在于 weight
参数是可调整的,其中驱动程序编码其 NAPI 权重硬为 64。 我们将在下面的调优部分看到如何使用 sysctl
调整权重。
process_backlog
process_backlog
函数是一个循环,直到其权重(如前一节所述)被消耗完或 backlog 中没有更多数据为止。
backlog 队列中的每个数据都从 backlog 队列中移除,并传递给 __netif_receive_skb
。一旦数据进入 __netif_receive_skb
,代码路径与上面解释的 RPS 禁用情况相同。也就是说,在调用 __netif_receive_skb_core
传递网络数据到协议层之前,__netif_receive_skb
会进行一些簿记工作。
process_backlog
遵循与设备驱动程序相同的 NAPI 契约,即:如果不使用总权重,则禁用 NAPI。通过上面描述的 enqueue_to_backlog
中对 ____napi_schedule
的调用,轮询器重新启动。
该函数返回完成的工作量,net_rx_action
(上面描述)将从预算中扣减该工作量(使用上面描述的 net.core.netdev_budget
进行调整)。