下载本文的 PDF 版本 PDF

重新审视网络 I/O API:netmap 框架

在现代操作系统上,数据包处理的方式有可能实现巨大的性能提升。


Luigi Rizzo,比萨大学


如今,万兆接口在数据中心和服务器中得到越来越广泛的应用。在这些链路上,数据包的流速高达每 67.2 纳秒一个,然而,现代操作系统仅将一个数据包在线缆和应用程序之间移动就需要 10-20 倍的时间。我们可以在这方面做得更好,不是通过更强大的硬件,而是通过修改很久以前在设备驱动程序和网络堆栈设计方面做出的架构决策。

netmap 框架是朝着这个方向迈出的有希望的一步。由于其精心设计和新型数据包 I/O API 的工程化,netmap 消除了许多不必要的开销,并将流量的移动速度提高了高达现有操作系统的 40 倍。最重要的是,netmap 在很大程度上与现有应用程序兼容,因此可以逐步部署。


一点历史

在当前主流操作系统(Windows、Linux、BSD 及其衍生版本)中,网络代码和设备驱动程序的架构深受大约 30 年前做出的设计决策的影响。当时,内存是一种稀缺资源;链路以低速(以今天的标准衡量)运行;并行处理是一个高级研究课题;即使在软件介入之前,NIC(网络接口控制器)中的硬件限制也损害了在所有可能条件下以线速工作的能力。

在这样的环境中,设计者在易用性、性能和精简内存使用之间做出了权衡。数据包由描述符表示(命名为mbuf,5 skbuf,9NDISbuffer,具体取决于操作系统),这些描述符链接到固定大小缓冲区的链。Mbuf 和缓冲区是从公共池中动态分配的,因为它们的生命周期超出了单个函数的范围。缓冲区也进行引用计数,因此可以由多个消费者共享。最终,数据包的这种表示形式在网络堆栈的所有层之间实现了一个消息传递接口。

Mbuf 包含元数据(数据包大小、标志、对接口、套接字、凭据和数据包缓冲区的引用),而缓冲区包含数据包的有效负载。使用固定大小的缓冲区简化了内存分配器,即使当数据超过单个缓冲区的大小时需要链接。允许缓冲区共享可以在网络堆栈上的许多常见操作中节省一些数据复制(因此节省时间和空间)。例如,当传输 TCP 数据包时,协议栈必须保留数据包的副本,以防传输丢失,而共享缓冲区可以节省复制成本。

现代 NIC 的设计基于这种数据表示。它们可以发送或接收拆分到多个内存缓冲区中的数据包,正如操作系统所实现的那样。NIC 使用它们自己的描述符,这些描述符比操作系统使用的描述符简单得多,并且通常排列成一个环形数组,称为 NIC 环(见图 1)。NIC 环是静态分配的,其槽指向作为 mbuf 链一部分的缓冲区。


数据包处理成本

网络 I/O 有两个主要的成本组成部分。每字节成本来自数据操作(复制、校验和计算、加密),并且与处理的流量成正比。每数据包成本来自描述符的操作(分配和销毁、元数据管理)以及系统调用、中断和设备驱动程序函数的执行。每数据包成本取决于数据流如何拆分为数据包:数据包越大,组件越小。

为了了解速度限制的概念,请考虑 10-Gbit/s 以太网接口,这将是本文通篇的参考点。最小数据包大小为 64 字节或 512 位,周围环绕着额外的 160 位数据包间隙和前导码。在 10 Gbit/s 的速率下,这转化为每 67.2 纳秒一个数据包,最坏情况下的速率为 14.88 Mpps(每秒百万个数据包)。在最大以太网帧大小(1,518 字节加上成帧)下,传输时间变为 1.23 微秒,帧速率约为 812 Kpps。这比峰值速率低约 20 倍,但仍然非常具有挑战性,并且如果 TCP 要饱和 10-Gbit/s 链路,则需要维持这种状态。

为了了解如何改进数据包处理时间,了解典型操作系统中数据包处理时间的分配情况是很有用的。考虑以下两种情况。

使用 UDP(用户数据报协议)或原始套接字的应用程序通常每个数据包发出一个系统调用。这会导致分配一个 mbuf 链,该链使用元数据和用户提供的有效负载的副本进行初始化。然后,对 NIC 进行编程以传输数据包,最终回收 mbuf 链。接收时也会发生类似的操作过程。在总处理时间中,大约 50% 用于系统调用:30% 用于 mbuf 分配和回收,剩余部分在内存复制和对硬件进行编程之间平均分配。此处显示了分割的图形表示



TCP 发送者和接收者的情况略有不同:系统调用可以将大型段从/移动到内核,而分段发生在内核内部。在这种情况下,系统调用对单个数据包的影响减少了;另一方面,内核处理的成本更高,也是因为需要处理反向流动的确认。这导致了不同的工作分配,如图形所示为



然而,两种情况下的总体数据包处理能力相似:最先进的系统每个核心可以处理大约 1 Mpps。这远低于 10-Gbit/s 链路的峰值速度,并且勉强足以用 1,500 字节的数据包饱和链路。

为什么 mbuf 处理如此耗时?在 30 年前出现的单处理器系统中,内存分配器和引用计数相对便宜,尤其是在存在固定大小对象的情况下。如今情况有所不同:在存在多个核心的情况下,分配会争用全局锁,而引用计数则在共享内存变量上运行;这两种操作都可能很容易导致未缓存的 DRAM 访问,这需要高达 50-100 纳秒的时间。4

处理延迟

在尝试饱和高速链路时,处理成本只是等式的一部分。延迟也对性能产生严重影响。对延迟的敏感性并非 10-Gbit/s 网络所独有,但当链路速度不再是系统中的瓶颈时,这种现象变得尤为重要。

高内存读取延迟(前面提到的数十纳秒)通常发生在从 NIC 的寄存器或 NIC 环读取时。这些内存区域由 NIC 更新,因此使 CPU 缓存无效。为了避免停顿(这将耗尽所有可用的数据包处理时间)并实现所需的吞吐量,数据包处理代码必须在实际需要数据之前很好地发出预取指令。反过来,这可能需要进行重大的代码重组:代码不应一次处理一个数据包,而应批量处理,第一轮预取(使 CPU 在等待读取完成时保持忙碌),然后是实际的数据包处理。

内存写入受延迟的影响较小,因为缓存和写入缓冲区可以吸收许多未完成的写入请求而不会阻塞。另一方面,应用程序等待写入完成的停顿可能发生在传输协议级别。众所周知,在任何通信协议中,当传输中的数据量(“窗口”)受到某个最大值 W 的限制时,最大吞吐量为 min(B, W/RTT),其中 B 是瓶颈带宽,RTT 是系统的往返时间。即使对于非常乐观的 100 微秒 RTT,也需要 1 Mbit (125 KB) 的未完成数据才能利用链路的容量。如果通信实体在等待响应之前没有足够的数据要发送,则可能无法达到全速,或者需要对应用程序本身进行重大重组。


高速传输协议

对于具有实际 RTT 值(在 10 到 100 毫秒范围内)的路径,窗口大小变得巨大,并给整个系统带来压力。大型窗口与传统的 TCP 拥塞控制算法1 交互不良,后者对数据包丢失的反应非常保守。实验性 RFC(请求评论)提出了一种在数据包丢失后更积极地增加窗口大小的策略,从而缩短达到全速的时间。2 使用大型窗口的第二个副作用是内存占用和缓存使用量的增加,这可能会溢出可用的缓存空间并减慢数据访问速度。最后,通常用于存储数据包的数据结构(线性列表)在出现数据包丢失或重新排序时会引入线性成本。

请记住,协议有时不仅处理协议头,还处理对数据包有效负载的访问。例如,TCP 校验和计算需要读取数据包数据(在发送和接收端),这会消耗内存带宽并污染缓存。在某些情况下(例如,对于本地生成的数据),可以通过将校验和计算与将数据带入内核的复制操作合并来优化此操作。接收路径不可能进行相同的操作,因为确认(取决于校验和验证)不能延迟到用户空间进程读取数据之后。这解释了为什么校验和计算是硬件卸载的自然候选者。

性能增强技术

您可以提高数据包 I/O 机制的效率。使系统调用能够处理每个调用多个数据包可以分摊其成本,这占总成本的很大一部分。一些高性能数据包捕获 API 采用了此技术。另一个常见的选择是使用大于大多数以太网接口上使用的默认 1,500 字节的数据包。较大的 MTU(最大传输单元)降低了每数据包成本;在 TCP 流量的情况下,它还加快了数据包丢失后窗口增加的过程。然而,只有在源和目的地之间的整个路径上都允许使用大型 MTU 时,大型 MTU 才是有效的;否则,MTU 会通过路径 MTU 发现来修剪,或者更糟糕的是,MTU 不匹配可能会导致 IP 级别的分片。

将某些任务外包给硬件是另一种提高吞吐量的常用方法。典型的例子是 IP 和 TCP 校验和计算以及 VLAN(虚拟 LAN)标记添加/删除。这些任务只需少量额外的电路即可在 NIC 中完成,可以节省一些数据访问或复制。两种流行的机制,特别是在 TCP 的上下文中,是 TSO(TCP 分段卸载)和 LRO(大型接收卸载)。TSO 意味着 NIC 可以将单个大型数据包拆分为多个 MTU 大小的 TCP 段(而不是 IP 片段)。节省之处在于,对于整个数据包,协议栈仅遍历一次,而不是对于每 MTU 字节的数据遍历一次。LRO 在接收端起作用,将多个传入段(对于同一流)合并为一个,然后将其传递到网络堆栈。

现代 NIC 还支持某些形式的数据包过滤和加密加速。这些是更专业的功能,在特定情况下可以找到应用。

多年来,硬件加速的有用性一直备受争议。在链路速度的每次飞跃(通常是 10 倍)中,系统都会发现自己无法应对线速,供应商会添加硬件加速功能来填补空白。然后随着时间的推移,CPU 和内存变得更快,通用处理器可能会达到甚至超过硬件加速器的速度。

虽然硬件校验和可能有一些意义(它们在硬件中几乎不花费任何成本,并且可以节省大量时间),但即使在软件中实现,TSO 和 LRO 等功能也相对便宜。


多核支持

如今,几乎普遍可用的多个 CPU 核心可以用于提高数据包处理系统的吞吐量。现代 NIC 支持多个发送和接收队列,不同的核心可以独立使用这些队列,而无需协调,至少在访问 NIC 寄存器和环方面是这样。在内部,NIC 将来自发送队列的数据包调度到输出链路中,并提供某种形式的解复用,以便根据一些有用的键(例如 MAC 地址或 5 元组)将传入流量传递到接收队列。

NETMAP

回到提高数据包处理性能的最初目标,我们正在寻找一个很大的因素:从 1 Mpps 到 14.88 Mpps 及以上,以便达到线速。根据阿姆达尔定律,只有通过随后消除任务中最大的成本因素,才能实现如此大的加速。在这方面,到目前为止显示的所有技术都没有解决问题的潜力。使用大型数据包并不总是可行的选择;硬件卸载对系统调用和 mbuf 管理没有帮助,而求助于并行性是对问题的一种蛮力方法,如果其他瓶颈没有消除,则无法扩展。

Netmap7 是一个新颖的框架,它采用了一些已知技术来降低数据包处理成本。除了性能之外,它的关键特性是它可以与现有操作系统内部组件和应用程序平滑集成。这使得仅需少量新代码即可实现巨大的加速,同时构建一个健壮且可维护的系统。

Netmap 定义了一个 API,该 API 支持在每个系统调用中发送和接收大量数据包,因此使系统调用(UDP 的最大成本组成部分)几乎可以忽略不计。Mbuf 处理成本(下一个最重要的组成部分)被完全消除,因为缓冲区和描述符仅在初始化网络设备时分配一次。内核空间和用户空间之间共享缓冲区在许多情况下可以节省内存复制并减少缓存污染。

数据结构

当查看用于表示数据包并支持应用程序和内核之间通信的数据结构时,描述 netmap 架构会更容易。该框架围绕共享内存区域构建,该区域可供内核和用户空间应用程序访问,其中包含接口管理的所有数据包的缓冲区和描述符。数据包缓冲区具有固定大小,足以存储最大大小的数据包。这意味着没有分片和固定且简单的数据包格式。描述符(每个缓冲区一个)非常紧凑(每个八个字节),并存储在循环数组中,该数组与 NIC 环一一映射。它们是称为netmap_ring的数据结构的一部分,该数据结构还包含一些附加字段,包括要发送或接收的第一个缓冲区的索引(cur)和可用于发送或接收的缓冲区数量(avail)。

Netmap 缓冲区和描述符仅分配一次(当接口启动时),并且始终绑定到该接口。Netmap 有一个简单的规则来仲裁对共享数据结构的访问:netmap 环始终由应用程序拥有,除非在执行系统调用期间,此时应用程序被阻塞,并且操作系统可以自由访问该结构而不会发生冲突。之间的缓冲区curcur+avail遵循相同的规则:它们属于用户空间,除非在系统调用期间。剩余的缓冲区(如果有)则由内核拥有。


用户 API

对于程序员来说,使用 netmap 非常简单直观。首先,通过调用open("/dev/netmap")创建一个未绑定的文件描述符(类似于套接字)。然后使用ioctl()将描述符绑定到给定的接口,并将接口名称作为参数的一部分传递。返回时,参数指示共享内存区域的大小以及 netmap 环在该区域中的位置。随后的mmap()将使共享内存区域可供进程访问。

要发送数据包,应用程序最多填充avail缓冲区,并在len中的相应槽中设置netmap_ring字段,并通过要发送的数据包数来推进cur索引。在此之后,非阻塞的ioctl(fd, NIOCTXSYNC)告诉内核传输新数据包并回收已完成传输的缓冲区。接收端使用类似的操作:非阻塞的ioctl(fd, NIOCRXSYNC)将 netmap 环的状态更新为内核已知的状态。返回时,cur指示具有数据的第一个缓冲区,avail指示有多少缓冲区可用,并且数据和元数据在缓冲区和槽中可用。用户进程通过推进cur来指示已消耗的数据包,并且在下一个ioctl()中,内核使这些缓冲区可用于新的接收。

让我们重新审视使用 netmap 的系统中的处理成本。系统调用仍然存在,但现在可以在大量数据包(可能是整个环)上分摊,因此其成本可以忽略不计。下一个最高的组件 mbuf 管理完全消失了,因为缓冲区和描述符现在是静态的。数据复制也被删除,唯一剩下的操作(由ioctl()s 实现)是在验证netmap_ring中的信息后更新 NIC 环,并通过写入 NIC 的寄存器之一来启动传输。通过这些简化,netmap 可以实现比标准 API 高得多的发送和接收速率也就不足为奇了。

许多应用程序需要阻塞 I/O 调用。Netmap 描述符可以传递给poll()/select()系统调用,当环有可用的槽时,该调用将被解除阻塞。在 netmap 文件描述符上使用poll()与之前显示的ioctl()s 具有相同的复杂度,并且poll()有一些额外的优化来减少典型应用程序中所需的系统调用数量。例如,poll()可以推出任何挂起的传输,即使 POLLOUT 不是参数的一部分;并且它可以更新netmap_ring中的时间戳,从而避免许多应用程序在gettimeofday()调用之后发出的额外的poll().


支持多个环

图 2 显示了一个接口可以有多个 netmap 环。这支持了现代高速 NIC 的多环架构。当将文件描述符绑定到 NIC 时,应用程序可以选择将所有环或仅一个环附加到文件描述符。使用第一个选项,相同的代码可以用于单队列或多队列 NIC。使用第二个选项,可以使用每个环一个进程/核心来构建高性能系统,从而利用系统中可用的并行性。

图 3 显示了一个可以处理多环 NIC 的流量生成器的工作示例。设备附加序列遵循到目前为止讨论的结构。负责发送数据包的代码围绕poll()循环,当缓冲区可用时返回。代码只是填充所有环中的所有可用缓冲区,并且下一个poll()调用将推出数据包并在更多缓冲区可用时返回。

主机堆栈访问和零拷贝转发

在 netmap 模式下,NIC 与主机堆栈断开连接,并可以直接供应用程序访问。然而,操作系统仍然认为 NIC 存在且可用,因此它将尝试从中发送和接收流量。Netmap 将两个软件 netmap 环附加到主机堆栈,使其可以使用 netmap API 访问。主机堆栈生成的数据包从 mbuf 中提取并存储在输入环的槽中,类似于来自网络的流量所使用的槽。Netmap 客户端将发往主机堆栈的数据包排队到输出 netmap 环中,然后从那里封装到 mbuf 中,并传递到主机堆栈,就好像它们来自相应的启用 netmap 的 NIC 一样。这种方法为构建流量过滤器提供了理想的解决方案:netmap 客户端可以将一个描述符绑定到主机环,将一个描述符绑定到设备环,并决定应在两者之间转发哪些流量。

由于 netmap 缓冲区的实现方式(全部映射在同一共享区域中)。构建真正的零拷贝转发应用程序很容易。应用程序只需要在接收环和发送环之间交换缓冲区指针,在为发送环排队一个缓冲区的同时,用一个新的缓冲区补充接收环。这在 NIC 环和主机环之间,以及不同接口之间都有效。

性能

netmap API 主要为发送和接收原始数据包提供了一个快捷方式。一类应用程序(防火墙、流量分析器和生成器、网桥和路由器)可以通过使用本机 API 轻松利用性能优势。这需要在应用程序中进行更改,尽管很小,但这通常是不希望的。然而,在 netmap 之上构建一个libpcap兼容的 API 是微不足道的,这意味着您可以在 netmap 之上运行大量未修改的应用程序。

netmap 的改进在简单的应用程序(例如流量生成器或接收器)上最为明显。这些应用程序将大部分时间用于原始数据包 I/O。图 4 比较了使用 netmap 或标准 API 的各种流量生成器的性能,以及各种时钟速度和核心数量。底部曲线显示netsend,一个使用传统套接字 API 的数据包生成器,在一个核心全速运行时几乎无法达到 1 Mpps。性能图上的下一个是pktgen,一个专门的 Linux 应用程序,它完全在内核中实现数据包生成。即使在这种情况下,峰值速度也约为 4 Mpps,远低于 10-Gbit/s 接口上可实现的最大速率。

接下来的两条曲线显示了图 3 中 netmap 生成器的性能:它不仅可以达到线速(最小尺寸帧为 14.88 Mpps),而且即使在最大时钟速度的三分之一时也能做到这一点。这比使用本机 API 快约 40 倍,比内核 Linux 工具快 10 倍。顶部曲线(使用四个核心)显示 API 在多个 CPU 上具有合理的扩展性。

图 5 显示了各种数据包转发应用程序如何从 netmap 的使用中受益。两个极端是本机桥接(在 FreeBSD 上),达到 0.69 Mpps,以及一个自定义应用程序,该应用程序使用 netmap API 实现跨接口的最简单的数据包转发,达到超过 10 Mpps。如图所示,在本例中,在 netmap 之上使用libpcap模拟仅牺牲了 25% 的性能(在 CPU 密集型更高的应用程序中牺牲的性能更少)。同样,两个流行的转发应用程序也实现了巨大的加速。8 Open vSwitch6 即使在经过大量优化以消除原始实现中的一些性能问题之后,也获得了四倍的加速。Click3 使用 netmap 比原始版本快 10 倍。事实上,带有 netmap 的 Click 比内核版本快得多,内核版本多年来一直被认为是构建软件数据包处理系统的最有效解决方案之一。


实现

netmap 的关键设计目标之一是使其易于集成到现有操作系统中,并且同样易于维护和移植到新的(或闭源的第三方)硬件。如果没有这些特性,netmap 就只是另一个没有希望被广泛使用的研究原型。

Netmap 最近已导入到 FreeBSD 发行版中,Linux 版本正在开发中。核心由不到 2,000 行经过大量注释的 C 代码组成,并且它不会更改操作系统的内部数据结构或软件接口。Netmap 确实需要单独的设备驱动程序修改,但这些更改很小(每个更改约 500 行代码,而构成典型设备驱动程序的代码为 3,000 到 10,000 行),并且经过分区,因此仅修改原始源代码的一小部分。

结论

我们使用 netmap 的经验表明,在操作系统采用数据包处理的方式中,有可能实现巨大的性能提升。这种结果是可能的,无需特殊的硬件支持或对现有软件进行深入更改,而是通过关注传统数据包处理架构中的瓶颈,并通过修订设计决策,以最大限度地减少对系统的更改。

参考文献

1. Allman, M., Paxson, V., Blanton, E. 2009. RFC 5681: TCP 拥塞控制。

2. Floyd, S. 2003. RFC 3649: 用于大型拥塞窗口的高速 TCP。

3. Kohler, E., Morris, R., Chen, B., Jannotti, J., Kaashoek, F. 2000. Click 模块化路由器。 Transactions on Computer Systems 18(3); http://dl.acm.org/citation.cfm?id=354874.

4. Levinthal, D. 2008-09. Intel Core i7 处理器和 Intel Xeon 5500 处理器的性能分析指南:22; http://software.intel.com/sites/products/collateral/hpc/vtune/performance_analysis_guide.pdf.

5. McKusick, M. K., Neville-Neil, G. 2004. FreeBSD 操作系统设计与实现。波士顿,MA:Addison-Wesley。

6. Open vSwitch; http://openvswitch.org/.

7. Rizzo, L. netmap 主页; http://info.iet.unipi.it/~luigi/netmap.

8. Rizzo, L., Carbone, M., Catalli, G. 2012. 使用 netmap 透明加速软件数据包转发。Infocom; http://info.iet.unipi.it/~luigi/netmap/20110729-rizzo-infocom.pdf.

9. Rubini, A., Corbet, J. 2001. Linux 设备驱动程序,第 2 版。Sebastopol, CA: O'Reilly (第 14 章); http://lwn.net/Kernel/LDD2/ch14.lwn.

喜欢它,讨厌它?请告诉我们

[email protected]


Luigi Rizzo ([email protected]) 是意大利比萨大学信息工程系的副教授。他的研究重点是计算机网络,最近的研究重点是 快速数据包处理数据包调度网络仿真磁盘调度

© 2012 1542-7730/12/0100 $10.00

acmqueue

最初发表于 Queue vol. 10, no. 1
数字图书馆 中评论本文





更多相关文章

Michi Henning - API:设计至关重要
作为一名软件工程师超过 25 年,我仍然发现自己低估了完成特定编程任务所需的时间。有时,由此导致的计划延误是由于我自身的缺点造成的:当我深入研究一个问题时,我只是发现它比我最初想象的要困难得多,因此解决问题需要更长的时间——这就是程序员的生活。通常,我也清楚地知道自己想要实现什么以及如何实现它,但仍然比预期的时间长得多。当这种情况发生时,通常是因为我正在与一个 API 作斗争,这个 API 似乎尽最大努力在我的道路上扔石头,让我的生活变得艰难。





© 保留所有权利。

© . All rights reserved.