Abstract
现有商用系统中的以太网网络接口的设计重点是在低CPU利用率下实现高带宽,但同时通常会牺牲延迟。只有在高接口延迟绝大多数由软件请求处理时间构成时,这种方法才是可行的。然而,最近在基于请求响应的系统(例如memcached和RAMCloud)中,降低软件延迟的各种努力已经促使网络接口成为整体延迟的重要因素。 所以作者提出了适用于基于请求-响应的应用程序的低延迟网络接口设计。他们的设计表现出两倍的延迟改进,而不会对带宽或CPU功率造成负面影响。同时,作者还研究了使用中断和轮询之间的延迟权衡,以及处理器低功耗状态的影响。
Introduction
从历史上看,网卡延迟一直被区域链接和速度较慢的软件所掩盖,这两者都很容易将总体延迟带入毫秒范围。但最近数据中心应用程序出现了更严格的延迟要求,从而激发了在商品系统上实现低延迟的努力,例如各种从主内存操作的数据库和缓存服务,比如memcached。此外还有正在进行的构建超低延迟软件的工作,比如RAMCloud项目,它是一个持久的存储系统,它的延迟目标是对于一个商用数据中心延迟为5-10个US RTT。
而由于经济原因,我们可以预见,在未来,低延迟的趋势会继续下去,这将会成为产生收入的驱动力,软件公司的服务必须在较小的延迟预算内进行。所以需要提供低延迟的技术。
在学术领域,人们对低延迟也有类似的兴趣,而且很可能是相关的。已经提到了一些软件方面的工作,但是其他学科也有贡献。例如,网络和互连的研究为无缓冲切换和新的网络拓扑提供了思路。而作者的的工作可以定位在网络和软件研究之间,就在连接主机和网络的接口上。
作者的方法是一种全新的方法,它可以帮助尽可能地构建最低延迟的请求-响应系统。遵循clean slate方法(电脑保护软件,没听说过),他们开发了一种小巧、快速的最小对象存储评估应用程序,它只有两个操作:GET和SET。这将延迟的重点转移到网络接口的设计,这是这篇论文的主要贡献。在网络接口中,他们主要关注两个延迟的原因:1)控制CPU与网卡之间的通信和数据传输;2)处理器空闲状态唤醒时间和电源管理。
他们的网络接口叫NIQ(Network Interface Quibbles),Quibbles意思是:为小事吹毛求疵或者无足轻重的小意见,有点意思!他们是在研究了网卡延迟低效的原因之后设计的,如第二节所述,他们的一个关键目标是最小化PCIe互连上的转换延迟。对于在请求-相应协议中普遍存在的小数据包尤其如此。在第三节中,作者会描述如何将现有技术(例如在描述符中嵌入小数据包)与新思想(例如自定义轮询、创造性地使用缓存策略)相结合,从而得到一个不会牺牲带宽的低延迟接口。
为了详细评估NIQ并将其与其他可能的解决方案进行比较,作者构建了一个可配置的基于fpga的网卡。此网卡由用户空间NIQ驱动程序配置和控制,该驱动程序提供零复制功能,并通过绕过内核堆栈提供直接的应用程序访问。第4节描述了评估系统,以及带宽和延迟性能分析。第5节介绍了CPU空闲状态功率测量和中断和轮询的功率延迟权衡
理想情况下,网络总延迟将由线传播延迟控制,该延迟受光速限制,为每米5ns左右假设数据中心内的总往返距离小于200米,则花费在线路上的总时间小于1us。最大的挑战是将网卡延迟带入相同的范围。与此同时,我们的NIQ实现了4.65us的最佳往返延迟(客户机-服务器-客户机,中间有网络电缆)。然而,大部分时间是我们现有的硬件组件所固有的。在第7节中,我们讨论了在2.3us以下的往返延迟的可能性,包括最先进的组件和ASIC实现。
Network Interface Card Design
在这里作者又强调了一下,当前的商用系统中的延迟主要是软件延迟导致的,并且告诉我们,一个空闲linux内核网络堆栈的往返时间度量为32-37 us(UDP是32us,TCP是37us)。
然后作者介绍了一下经典网卡的结构:
一个典型的网卡通过PCI Express (PCIe)连接到主机系统,并包含以下组件:DMA引擎、环形缓冲区、以太网MAC和PHY,以及其他特性(卸载引擎、QoS、虚拟化等)。DMA引擎直接连接到PCIe接口,并在主机内存和NIC上的环形缓冲区之间传输数据。MAC和PHY以及PCIe互连的性能对整体性能有贡献,但本文主要关注NIC和主机之间的接口。这个接口由环形缓冲区的功能定义,以及NIC和主机驱动程序如何管理它们。我们通过单步执行包传输和包接收过程来描述主机和NIC之间的交互,如图1所示。
传输的步骤
1、当客户机发出请求时,它会格式化请求包并将其发送到服务器。NIC传输接口会将包的元数据(即包描述符)添加到CPU和NIC共享的主存描述符环中。(图一左侧的步骤一)
2、通知网卡有一个新的传输描述符,CPU会有一个I/O操作写网卡上的门中断寄存器(第二步的第一部分)然后网卡使用DMA引擎会去取下一个传输描述符(第二步的最后一个部分)
3、DMA同时会根据获取的包描述符来获取内容来准备数据包(步骤三)
总的来说从软件开始到NIC组织好可用数据包需要两次半PCIe
描述符环缓冲区的使用提供了CPU和NIC之间的解耦,从而允许CPU在传输数据包的数量上领先于NIC。从CPU发出的数据包被有效地排在环形缓冲区中,网络允许网卡以最快的速度将环形缓冲区耗尽。这种解耦是实现高带宽的关键
4、此时,数据包已经发送到网络上了,并且去往服务器,但是仍然有一些必要的客户端记录。当数据包被交给
NIC进行传输时,它的内存在完成DMA之前不能被重用。DMA完成后,NIC在主机内存中设置一个标志,指示包就可以重用这个部分
5、这之后会产生一个中断
6、该中断会触发CPU的任务完成的处理,从而完成传输过程
4~6这些步骤不在请求-响应的延迟影响原因中,但是仍然必须马上执行,以防客户机资源耗尽(例如内存空间和流控)
接收的步骤
接收端在接收任何数据包之前,它必须加载关于主存中可用的接收数据包缓冲区的信息。描述符环缓冲区用于保存可用的接收描述符,类似于传输描述符环。在任何数据包到达之前,驱动程序分配多个接收数据包缓冲区,创建指向这些缓冲区的接收描述符,并将描述符传输到NIC(图1右边的的 步骤1 和步骤2)。
当请求包到达服务器的NIC时,NIC就开始读取下一个可用的描述符条目,以确定将包存储在何处。之后的步骤3是DMA引擎会将包存储到主机内存,并通知CPU数据包到达。步骤4即是重新利用包的描述符作为完成条目,其中包含包的长度和一个指示是否有新的有效包的标志位。
类似在传输的步骤中的情况,这个环形缓冲区结构允许NIC先于CPU运行,并且存储数据包的速度比CPU处理数据包的速度快,至少在突发情况下是这样。CPU必须读取环完成条目来发现到达包的位置和大小。为了防止完全一个CPU完全用于来检测环状结构,操作系统更倾向于配置中断作为一种完成通知(步骤5 )。如果是高负载的情况,那么很容易因为过多的锁而产生接收活锁;如果是低负载的情况,那么就会导致非常显著的延迟。所以作者建议这里用轮询方式运行,主机不断读取下一个预期的完成环条目,直至有一个生效的条目。现代操作系统根据负载在轮询模式和中断模式之间进行切换,从而避免了无论中断合并与否都存在接收端锁定问题。
最后,步骤6就是CPU读取接收描述符和接收数据包,然后将数据包转发到应用层进行处理。此外,此时CPU分配一个新的接收缓冲区,并用一个新的接收描述符更新NIC,以替换刚才使用的接收描述符。在处理请求之后,服务器应用程序格式化一个应答包,并按照刚才描述的发送-接收序列将其发送回客户机,从而完成一次请求-响应往返。
总的来说,在客户机发起请求和从服务器接收响应之间发生了16次PCIe链接上的单向转换。在16个单向转换中,有12个是同步的,这意味着它们必须在下一个转换开始之前完成,从而影响总体延迟(延迟产生的原因!!!!!!)
NIQ :Interface Design
这一段开头作者又吹了一波(我们提出的这个NIQ就可以对于这个request-response做到低延迟了)。他们实现这个目标是通过对于小包(small packet)和减少PCIe的传输数来做的。
作者首先阐述了为什么专注于小包:
1、request-response的网络数据包至少有一半是小包
2、小包的网络延迟比大包的网络延迟更重要。这个很好理解,根据作者前面的解释,当包比较大的时候,延迟主要是由于软件处理延迟等造成,所以这个网卡和主机交互的延迟就显得不是那么重要了。
然后作者就说,一共8个PCIe才能同时发送和接收,这对于它们的RTT来说是非常昂贵的(0.9us),所以他们专注于最小化PCIe转换数量。最好情况下只需要两次转换:一个用于传输,一个用于接收。他们提供的接口不仅为小数据包提供了最好的解决方案,而且为更大的数据包提供了良好的结果。
Small packet
NIQ利用了现代处理器是围绕cache-line大小传输进行优化的这一事实。然后说明最小的以太网数据包长度为60字节(标准的64字节减去4字节的FCS(帧检查序列))。接下来的工作都是假设小数据包的最小尺寸是60字节。扩展接口来支持是非常有可能的,例如支持两个缓存行大小的数据包。图2的上半部分说明了小数据包发射和接收序列的时序,步骤号与图1对应。
为了达到传输转换和接收转换的理想目标,NIQ接口将图1中的所有关键步骤折叠为一个步骤。通过在传输描述符中嵌入整个小数据包来完成。此外,**不使用DMA引擎来传输描述符,而是由CPU直接将描述符传输到网卡。**传输描述符是cache中的一行长度,其中含有标志位表示是有整个小数据包在其中,还是它只是一个传统的带有数据地址和长度的描述符。
在接收端,作者的操作也是类似的,折叠步骤是通过将整个小数据包嵌入完成条目来实现的。接收后,整个小数据包在高速缓存线范围内传输完成,而不是通过DMA引擎将其复制到主机缓冲区。并且作者强调采用轮询方式通知上层,这样就成功折叠了所有的接收步骤。
另外一个好处是,在传输或接收时不会消耗主机内存缓冲区。由于不消耗主机内存,因此不需要分配或释放任何缓冲区,从而减少了软件记录的总数量。在网卡和主机之间还是需要交换流量控制的信用信息,但这可以批量处理,而且不那么频繁。
如前面所说,传输描述符和完成接收都是64字节宽的。为了以高速缓存单元大小让网卡和主机通信,作者使用了缓存层级结构和写集合缓冲区。所有CPU写的内存都映射成写集合;所有CPU读取的内存映射成可缓存区,这种方式与传统的内存既可读又可写的标准相悖,但是类似于将写收集策略用于映射帧缓冲区的显卡实践。
CPU对写集合地址的任何写操作都会绕过缓存层次结构,进入CPU的一个写收集缓冲区。一旦写入了整个高速缓存线,或者执行了一条内存排序指令(例如sfence),整个高速缓存的一项就会通过PCIe刷新到网卡。在靠近CPU内核的缓冲区中组合写操作可以提高CPU带宽和PCIe带宽。实际上,CPU使用未缓存的写通过PCIe来写描述符不会超过 8字节上。而每个PCIe包在所有层上都会产生多达28字节的报头开销,导致77%的开销。不管数据负载大小如何,开销字节的数量都是相同的,这使得64字节的传输更加有效。(这里作者在说明,整合写操作会变得很有效)
然后作者举例说明了这种设计不能依赖读取来通知我们CPU何时处理了数据,而是需要显式的流控制通知。
Large Packets
对于大数据包,NIQ仍然使用高速缓存线路进行通信,但是整个数据包不再适合全部嵌入到描述符中。相反,会遵循使用DMA引擎传输数据包数据的传统方法,如第2节所讨论的。描述符和完成这些还是可缓存的,并且64字节宽,那么不会嵌入整个包而是只嵌入包的第一个48字节,其中包含包头。把包头放入其中可以在传输和接收时有效地实现包头分割。
包头分离(指包的header和data部分分离)是非常必要的,因为这样可以让我们可以实现低延迟系统中常用的零复制技术。如果包头不能与有效负载分离,则有必要在传输之前将包数据复制到中间内存缓冲区。为了避免将标头和数据复制到一个缓冲区中,可以对网卡的DMA引擎进行编程,以便在它们离开网卡之前分别传输和连接它们。为这个DMA引擎编程也可能需要两个单独的传输描述符,而作者提供的接口只需要一个。因为包头和包处在同一高速缓存线路,所以包头一读取完就可以用于CPU处理,相比之下会更快。
接下来作者说明了他们需要允许包的重新排序(对整体方案理解作用不大):
我们已经探索并评估了使用CPU通过writegathering缓冲区传输整个大数据包的选项,但是我们发现CPU带宽损失会随着数据包大小的增加而显著增加(对于最大的数据包,最高可达70%的损失)。即使小数据包接口提供较低的延迟,也完全有可能通过大数据包接口发送小数据包。当严格的数据包排序很重要时,建议这样做,因为通过小数据包接口发送的小数据包可以传递一个大数据包,从而使网卡处于无序状态。允许小数据包绕过大数据包是一个特性,因为它可以很容易地为小数据包节省超过1 us的传输时间。在接口上强制执行严格的排序是完全可能的,但是由于底层网络不保证包排序,所以我们选择允许重新排序。
关于通知,在传输路径上,一般是用中断处理,并且有中断合并的方案,但是对于关键通知作者选择使用自定义的轮询方案。
NIQ polling
作者设计的NIQ轮询不是轮询主机内存,而是反复通过PCIe读取网卡,以此避免通过主存进行通信。此外,来自网卡的响应被直接放到第一级缓存中,从而避免了缓存丢失。
设计这个NIQ轮询方案的目标是最小化通知延迟。通知延迟的定义是从NIQ中准备好一个新的有效完成条目到该完成在CPU中已准备好被处理之间的时间。如图三所示,当重复轮询网卡时,这个通知延迟就处于半个PCIe往返到一个半往返之间。预期的时间是一个完成的PCIe往返。
为了降低预期的通知延迟,作者引入了一个PCIe读延迟,当没有新的有效完成的时候,NIC不会马上答复,而是等待一个读延迟时间,这个延迟时间到达的时候,如果还没有效的完成,那么就用一个无效条目进行应答;如果在期间有一个有效的完成,那么就马上应答,那么就有效地降低了预期的通知延迟(最多减少一半)。图三的右半部分显示了这个过程。而且这还有一个额外好处,减少了通过PCIe的 无效条目,也减少了CPU必须处理的无效条目的数量。
既然有了这个PCIe读延迟,那么需要为它选择一个正确、合适的时间(在最小化延迟的同时避免处罚任何死锁预防和监视机制),所以需要有所权衡。作者发现预期的通知延迟与1+(PCIe读延迟)成反比,经实验发现,20次PCIe延迟的时长作为读延迟时间比较好。
作者还遇到了Inter’s同步多线程实现(称为超线程)的一些有趣影响。当一个超级线程等待轮询读取返回时,它的同胞线程将充分利用执行单元和其他共享资源,从而使NIQ轮询成为一个更有吸引力的解决方案。然而,他们发现,当使用NIQ轮询并将读取响应延迟超过4个us(或10000个周期)时,同级的超级线程不会向前推进。他们将此现象归因于死锁/饥饿预防机制,该机制检测到轮询线程长时间没有取得进展(在等待轮询读取时),并在检测到时预防性地停止同级线程。因此,作者不使用超线程。
Interrupts vs. host polling vs. mwait
这里作者首先阐述了自己的观点:为什么用轮询好?
从历史上看,I/O设备和CPU之间存在巨大的速度不匹配,这使得中断方案是必要的和有效的。对于单核CPU来说,等待I/O设备完成一个操作需要几毫秒是不可能的。然而,现代网卡要快得多,而且现代cpu有多个核心,因此改变了平衡。人们仍然希望CPU内核能够在等待网络设备时执行其他处理,但通常没有其他事情可做。这种推理加上中断的延迟性能差,使得轮询成为一个有吸引力的选项。
作者同时介绍了中断延迟性能差的三个原因:
1、从中断生成到Linux执行中断处理程序之间有1.4us的延迟。
2、中断处理程序和用户应用线程之间的线程间通信造成延迟
3、电源管理,CPU处于空闲状态时会有一段退出时间,深度空闲时为几十微秒
然后就简单介绍了一下相比之下,为什么轮询会比较好。
再之后就是有关实验和测试的部分了,这部分我没有过多的关注。对于此论文我做一个自己的总结:随着软件方面的努力、类似memcache这些系统的部署,软件的延迟在总延迟中的比例越来越低,人们开始关注这个网卡与主机交互的延迟。作者所做的延迟优化核心是减少PCIe的交换次数和CPU采用轮询来获取结果,减少PCIe的交换次数的方法是将后面流程的数据包嵌入到最初的PCIe描述符(元数据)传输,所以PCIe的交换轮数就可以减小。对于在实际场景中占大比例的小包,可以直接嵌入,优化程度巨大;对于大包,将包头嵌入,做到数据与包头分离,也有一个不错的优化。