监控网络数据到达
硬件中断请求
注意: 监视硬件中断并不能全面了解数据包处理的健康状况。 许多驱动程序在 NAPI 运行时关闭硬件中断,我们将在后面看到。 它是整个监控解决方案的重要组成部分。
读取 /proc/interrupts
检查硬件中断状态。
$ cat /proc/interrupts CPU0 CPU1 CPU2 CPU3 0: 46 0 0 0 IR-IO-APIC-edge timer 1: 3 0 0 0 IR-IO-APIC-edge i8042 30: 3361234770 0 0 0 IR-IO-APIC-fasteoi aacraid 64: 0 0 0 0 DMAR_MSI-edge dmar0 65: 1 0 0 0 IR-PCI-MSI-edge eth0 66: 863649703 0 0 0 IR-PCI-MSI-edge eth0-TxRx-0 67: 986285573 0 0 0 IR-PCI-MSI-edge eth0-TxRx-1 68: 45 0 0 0 IR-PCI-MSI-edge eth0-TxRx-2 69: 394 0 0 0 IR-PCI-MSI-edge eth0-TxRx-3 NMI: 9729927 4008190 3068645 3375402 Non-maskable interrupts LOC: 2913290785 1585321306 1495872829 1803524526 Local timer interrupts
您可以监控 /proc/interrupts
中的统计信息,以查看随着数据包到达而硬件中断的数量和速率如何变化,并确保您的 NIC 的每个接收队列都由适当的 CPU 处理。正如我们不久将看到的,这个数字只告诉我们发生了多少次硬件中断,但它并不一定是了解接收或处理了多少数据的好指标,因为许多驱动程序会作为与 NAPI 子系统协作的一部分禁用 NIC 硬中断。此外,使用中断合并也会影响从此文件收集的统计信息。监控此文件可以帮助您确定所选的中断合并设置是否真正起作用。
要获得更完整的网络处理健康状况图像,您需要监控 /proc/softirqs
(如上所述)以及我们将在下面介绍的 /proc
中的其他文件。
调优网络数据到达
中断合并
中断合并 是一种防止设备向 CPU 发出中断的方法,直到有特定数量的工作或事件处于挂起状态。
这可以帮助防止中断风暴,并可以根据使用的设置帮助提高吞吐量或延迟。产生的中断较少会导致吞吐量更高,延迟增加,CPU 使用率降低。产生的中断较多会导致相反的结果:延迟降低,吞吐量降低,但 CPU 使用率增加。
历史上,早期版本的 igb
、e1000
和其他驱动程序都包含对名为 InterruptThrottleRate
的参数的支持。在较新的驱动程序中,此参数已替换为通用的 ethtool
函数。
使用 ethtool -c
获取当前的 IRQ 合并设置。
$ sudo ethtool -c eth0 Coalesce parameters for eth0: Adaptive RX: off TX: off stats-block-usecs: 0 sample-interval: 0 pkt-rate-low: 0 pkt-rate-high: 0 ...
ethtool
提供了一个通用接口,设置各种合并设置。但是,请记住,并非每个设备或驱动程序都支持所有设置。您应该检查驱动程序文档或驱动程序源代码以确定支持或不支持的内容。根据 ethtool 文档:“驱动程序未实现的任何内容都会导致这些值被静默忽略。”
一些驱动程序支持的一个有趣选项是“自适应接收/传输硬中断合并”。此选项通常在硬件中实现。驱动程序通常需要做一些工作来通知 NIC 启用了此功能,并进行一些簿记(如上面的 igb
驱动程序代码所示)。
启用自适应接收/传输硬中断合并的结果是,在数据包速率低时调整中断传递以改善延迟,并在数据包速率高时提高吞吐量。
使用 ethtool -C
启用自适应接收硬中断合并。
$ sudo ethtool -C eth0 adaptive-rx on
你可以使用 ethtool -C
来设置多个选项。一些常见的选项包括:
rx-usecs
:在数据包到达后,延迟多少微秒才触发接收中断。rx-frames
:在触发接收中断之前,最多接收多少个数据帧。rx-usecs-irq
:当主机正在处理中断时,延迟多少微秒才触发接收中断。rx-frames-irq
:当系统正在处理中断时,在触发接收中断之前,最多接收多少个数据帧。
还有更多选项。
提醒,你的硬件和驱动程序可能只支持上述选项的一个子集。你应该查阅驱动程序源代码和硬件数据表,以获取有关支持的合并选项的更多信息。
不幸的是,除了头文件之外,你可以设置的选项并没有在其他地方得到很好的记录。查看 include/uapi/linux/ethtool.h 的源代码,以找到 ethtool
支持的每个选项的解释(但不一定是你的驱动程序和 NIC)。
注意:虽然中断合并看起来是一个非常有用的优化,但在尝试优化时,网络栈的其他内部也会受到影响。在某些情况下,中断合并可能很有用,但你应该确保网络栈的其他部分也调整得当。仅仅修改合并设置本身可能带来的好处微不足道。
调整 IRQ 亲和性
如果你的网卡支持 RSS/多队列,或者你想优化数据本地性,你可能希望使用特定的 CPU 来处理网卡产生的中断。
设置特定的 CPU 可以让你划分哪些 CPU 处理哪些 IRQ。这些更改可能会影响上层操作,正如在网络栈中看到的那样。
如果你决定调整 IRQ 亲和性,你应该首先检查是否运行了 irqbalance
守护程序。这个守护程序试图自动平衡 IRQ 到 CPU 上,它可能会覆盖你的设置。如果你正在运行 irqbalance
,你应该禁用 irqbalance
或使用 --banirq
与 IRQBALANCE_BANNED_CPUS
结合使用,让 irqbalance
知道它不应该触碰你想要自己分配的 IRQ 和 CPU 集合。
接下来,你应该检查文件 /proc/interrupts
,查看网卡每个网络 RX 队列的 IRQ 编号列表。
最后,你可以修改每个 IRQ 编号的 /proc/irq/IRQ_NUMBER/smp_affinity
来调整每个 IRQ 将由哪些 CPU 处理。
你只需写入十六进制位掩码到此文件,以指示内核应使用哪些 CPU 来处理 IRQ。
示例:设置 IRQ 8 的 IRQ 亲和性为 CPU 0
$ sudo bash -c 'echo 1 > /proc/irq/8/smp_affinity'
网络数据处理开始
当软中断代码确定软中断(译者注:软中断信号)正在等待时,它开始处理并执行 net_rx_action
,网络数据处理就开始了。
让我们来看看 net_rx_action
处理循环的部分内容,以了解它是如何工作的,哪些部分是可调的,以及可以监控什么。
net_rx_action
处理循环
net_rx_action
开始从设备通过 DMA 传输数据包到内存中的数据包进行处理。
该函数遍历当前 CPU 队列中的 NAPI 结构列表,对每个结构执行出队操作,并对其进行操作。
处理循环限制了注册的 NAPI poll
函数所能消耗的工作量和执行时间。它通过两种方式实现这一点:
- 跟踪工作
budget
(可以调整),以及 - 检查运行时间
来自 net/core/dev.c:
while (!list_empty(&sd->poll_list)) { struct napi_struct *n; int work, weight; /* If softirq window is exhausted then punt. * Allow this to run for 2 jiffies since which will allow * an average latency of 1.5/HZ. */ if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit))) goto softnet_break;
这就是内核如何防止数据包处理占用整个 CPU 的方法。上面提到的 budget
是在这个 CPU 上注册的所有可用 NAPI 结构花费的总预算。
这也是多队列网卡应该仔细调整 IRQ 亲和性的另一个原因。回想一下,处理设备的 IRQ 的 CPU 将是执行软中断处理程序的 CPU,因此也将是上述循环和预算计算运行的 CPU。
具有多个网卡,每个网卡都有多个队列的系统可能会出现多个 NAPI 结构注册到同一个 CPU 的情况。同一 CPU 上所有 NAPI 结构的数据处理都从同一个 budget
中扣减。
如果您没有足够的 CPU 来分布您的网卡的 IRQ,您可以考虑增加 net_rx_action
的 budget
,以允许每个 CPU 处理更多的数据包。增加预算将增加 CPU 使用率(具体来说是 sitime
或 top
或其他程序中的 si
),但减少延迟,因为数据处理更及时。
注意: 无论如何分配预算,CPU 仍然受到 2 个 jiffies 的时间限制。
NAPI poll
函数和 权重
回想一下,网络设备驱动程序使用 netif_napi_add
来注册 poll
函数。正如我们在本文前面看到的那样,igb
驱动程序有一段类似这样的代码:
/* initialize NAPI */ netif_napi_add(adapter->netdev, &q_vector->napi, igb_poll, 64);
这行代码注册了具有硬编码权重 64 的 NAPI 结构。 现在我们将看到如何在 net_rx_action
处理循环中使用它。
来自 net/core/dev.c:
weight = n->weight; work = 0; if (test_bit(NAPI_STATE_SCHED, &n->state)) { work = n->poll(n, weight); trace_napi_poll(n); } WARN_ON_ONCE(work > weight); budget -= work;
这段代码获取了注册到 NAPI 结构的权重(上面驱动程序代码中的 64
)并传递其给也注册到 NAPI 结构的 poll
函数(上面代码中的 igb_poll
)。
poll
函数返回处理的数据帧数。这个数量被保存为上面的 work
,然后从总 budget
中扣减。
因此,假设:
- 您使用来自驱动程序的权重
64
(在 Linux 3.13.0 中,所有驱动程序都使用这个值硬编码),并且 - 您设置
budget
为默认值300
当满足以下任一条件时,您的系统将停止处理数据:
- 最多调用了 5 次
igb_poll
函数(如果没有数据要处理,我们接下来会看到次数更少),或者 - 至少经过了 2 个 jiffies 的时间。
NAPI / 网络设备驱动程序契约
关于 NAPI 子系统和设备驱动程序之间的契约,有一个重要的信息尚未提及,那就是关闭 NAPI 的要求。
这部分契约如下:
- 如果驱动程序的
poll
函数消耗了其全部权重(硬编码为64
),则它不得修改 NAPI 状态。net_rx_action
循环将接管。 - 如果驱动程序的
poll
函数未消耗其全部权重,则必须禁用 NAPI。下次收到 IRQ 并且驱动程序的 IRQ 处理程序调用napi_schedule
时,NAPI 将重新启用。
我们现在将看到 net_rx_action
如何处理该契约的第一部分。接下来,我们检查 poll
函数,我们将看到如何处理该契约的第二部分。
完成 net_rx_action
循环
net_rx_action
处理循环以最后一段代码结束,该代码处理前一节中解释的 NAPI 合约的第一部分。 来自 net/core/dev.c:
/* Drivers must not modify the NAPI state if they * consume the entire weight. In such cases this code * still "owns" the NAPI instance and therefore can * move the instance around on the list at-will. */ if (unlikely(work == weight)) { if (unlikely(napi_disable_pending(n))) { local_irq_enable(); napi_complete(n); local_irq_disable(); } else { if (n->gro_list) { /* flush too old packets * If HZ < 1000, flush all packets. */ local_irq_enable(); napi_gro_flush(n, HZ >= 1000); local_irq_disable(); } list_move_tail(&n->poll_list, &sd->poll_list); } }
如果整个工作都被消耗了,net_rx_action
会处理两种情况:
- 网络设备应该关闭(例如,因为用户运行了
ifconfig eth0 down
), - 如果设备未关闭,请检查是否存在 generic receive offload(GRO)清单。 如果定时器滴答速率〉= 1000,则最近刷新更新的所有 GRO 网络流。 稍后我们将详细讨论GRO。 移动 NAPI 结构到该 CPU 的列表末尾,以便循环的下一次迭代获得注册的下一个 NAPI 结构。
这就是包处理循环调用驱动程序的注册 poll
函数处理包的方式。 我们很快就会看到,poll
函数将收集网络数据,并发送其到栈上进行处理。
达到限制时退出循环
当以下任一条件满足时,net_rx_action
循环将退出:
- 此 CPU 注册的轮询列表中没有更多的 NAPI 结构 (
!list_empty(&sd->poll_list)
),或 - 剩余预算 <= 0,或
- 已达到 2 个 jiffies 的时间限制
这是我们之前看到的代码:
/* If softirq window is exhausted then punt. * Allow this to run for 2 jiffies since which will allow * an average latency of 1.5/HZ. */ if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit))) goto softnet_break;
如果跟随 softnet_break
标签,你会偶然发现一些有趣的东西。 来自 net/core/dev.c:
softnet_break: sd->time_squeeze++; __raise_softirq_irqoff(NET_RX_SOFTIRQ); goto out;
struct softnet_data
结构会增加一些统计数据,然后关闭 softirq NET_RX_SOFTIRQ
。time_squeeze
字段是衡量 net_rx_action
有更多工作要做,但预算耗尽或时间限制到达之前无法完成的次数。这是一个极其有用的计数器,理解网络处理中的瓶颈。我们稍后将看到如何监控这个值。禁用 NET_RX_SOFTIRQ
以释放处理时间给其他任务。这是有道理的,因为这段小代码只在有更多工作可以完成时执行,但我们不希望垄断 CPU。
然后执行转移到 out
标签。如果没有更多的 NAPI 结构要处理,执行也可以到达 out
标签,换句话说,预算比网络活动多,所有驱动程序都关闭了 NAPI,而且 net_rx_action
没有剩下任何事情要做。
在从 net_rx_action
返回之前,out
部分做了一件重要的事情:它调用了 net_rps_action_and_irq_enable
。如果启用了 Receive Packet Steering,此函数具有重要作用;它唤醒远程 CPU 开始处理网络数据。
我们稍后将了解更多关于 RPS 的工作原理。现在,让我们看看如何监控 net_rx_action
处理循环的健康状况,并继续深入了解 NAPI poll
函数的内部工作原理,以便我们能够沿着网络栈向上。