TCP ≈ RDMA: CPU-efficient Remote Storage Access with i10 阅读

摘要:文章介绍的是一种完全在内核中实现的新型远程存储栈i10的设计、实现和评估。并且该系统栈还工作在普通的硬件上,应用查程序也不需要进行修改,利用的是类似于最新的用户空间和基于RDMA的解决方案的CPU利用率,为远程访问提供了100Gbps的链路。

1 介绍

云基础设施过去存在的两个重要趋势,(1)网络和存储硬件有了很好的改善,网络的链路从1G过渡到了100G;存储介质则从硬盘等跨到了NVMe等,远远的提高了读写速度。(2)由于资源的利用率等需求,大规模的分布式存储也正在部署,应用程序也开始通过网络访问存储设备。

由于这些改变,让性能瓶颈转移到了我们的软件栈,而网络和存储设备已经能够维持我们的高吞吐量下,传统的远程存储访问栈却无法维持CPU的开销,比如iscsi协议由于高协议处理和同步开销。每个CPU核只能实现70K IOPS,但是要使得一个NVMe存储设备饱和需要14个核,40个核才能让100G链路达到饱和。

为此,各界都重新审视了一下高效CPU远程存储栈的问题,由此产生了标准nvme-over-fabric(NVMe-oF),具体说就是NVMe-over-RDMA保留了内核存储栈,但是却将网络栈移到了硬件上去实现。而其他一些方案就是将存储和协议栈都移到用户空间去实现,从而让每个核在IOPS方面达到高性能,但是产生的问题则是需要改变我们的应用程序或网络基础设施。本文则是探讨一个基本问题:为了实现高效的远程存储访问,是否需要更改我们的基础设施呢?通过重新架构内核的方式,解决这个问题,就可以解决不是每一个组织都有用户空间协议栈和现代网络硬件的问题。

i10协议栈正是因为这样的一个问题产生的,它是内核中一个新的远程存储栈,对于那些吞吐量受限的应用来说,只需要在内核进行最小的修改,就可以实现与最新的用户控件和基于RDMA的方案相近的性能,它的好处在于不需要进行内核外的修改,其次它直接在现有的TCP/IP内核协议栈上运行,所以不需要修改基础设施和网络协议栈,再者它符合新规范NVMe-oF规范的,可以与最新的NVMe设备兼容,最后,则是在基准工作负载之上,i10使用CPU利用率类似于最先进的用户空间栈和NVMe-over-RDMA产品的普通服务器。i10为了达到以上优点,用了一个简单的设计:(1)端到端专用资源和批处理:从而优化软件协议栈和减少网络处理开销;(2)延迟时钟:当访问本地NVMe设备时,存储栈会收到请求时马上“敲响门铃”,从而会产生较高的上下文切换开销,所以i10采用延迟时钟方式,去处理多个请求。

                                                              

                                                  图1 远程存储访问的现有瓶颈位于存储和网络堆栈的边界

图中显示了内核网络、本地存储和远程存储栈的每核吞吐量。我们使用4 kb随机读请求两个服务器之间NVMe固态硬盘,连接通过一个100 Gbps链接。对于长连接的流,网络栈能维持每个核30 Gbps(大约915 k IOPS),类似地,本地I/O的存储堆栈可以维持每个核350个kIOPS。然而,当与现有的远程存储栈集成在一起时,吞吐量却减少到96个kIOPS。

2 i10的设计

在这里,会先介绍i10的一个大概情况,然后对i10是如何使用专用资源创建高度优化的i10-lanes,i10的配料和延迟门铃等。

2.1 概述

i10是一个处于内核存储栈和内核网络栈之间的一个shim层,下图2为i10的一个设计结构,包括了i10层和主机应用程序与目标NVMe存储设备之间的端到端路径。i10不对内核如何调度应用程序进行控制,每个应用可以运行在一个或者多个核心上,同时多个内核也可以共享一个核心,应用程序通过标准的读或写api向内核提交远程读或写请求;i10不需要修改这些api。

                    

                                                           图2 一个主机和两个目标服务器之间的端到端数据路径

在图中,APP1使用core1和core2向target1发送读或写请求,App2使用core2向target2发送读或写请求,因此i10为(core1,target1)、(core2,target1)、(core2,target2)对各创建一个i10-lanes,此外从App2提交的请求被复制到core2的块层请求队列(绿色),从App1提交的请求被复制到core1和core2的块层请求队列,这取决于请求来自哪个核心,虽然块层请求队列可能包含发送到不同目标服务器的请求,比如,位于host1的正确块层请求队列,但是每个请求都会被复制到i10 I/O队列中,对应于(核心,目标)对i10-lanes,最后,如果有一个运行在向target2发送请求的core2上的App3,它将与App2完全共享i10-lanes.

i10使用i10-lanes的核心抽象实现其目标,i10-lanes是一种专用管道,用于沿着一组专用资源交换控制和数据平面消息。i10在(核心,目标)对的粒度上创建i10-lane并将资源分配给每个i10-lane,其中目标指的是远程服务器上的block设备(不一定是单独的存储设备)。例如,考虑(可能有多个应用程序运行)核心c向两个目标服务器t1和t2提交读/写请求,每个服务器都有多个存储设备d11、d12...和d21、d22...。然后,i10创建两个i10-lanes,一个c->t1用于发送到前一组存储设备的所有请求,另一个c->t2用于发送到后一组存储设备的所有请求。注意,这与在核心c上运行的应用程序的数量无关。此外,如果在两个核心c1和c2上运行的单个应用程序向两个目标服务器t1和t2提交读/写请求,那么i10将创建四个i10- lanes,每个(核心、目标)对一个。i10不管在主机端还是目标端都会为每个i10-lane使用三组专用资源,第一个资源是i10层中的I/O队列,如图2中的蓝色横杠所示;第二个是主机和目标端i10队列之间的专用TCP连接及其缓冲区;最后则是主机端与i10交互的每个核心以及目标端需要的每个核心会提供一个专用的i10工作线程,其中i10队列和TCP连接的粒度是每通道的,而i10工作线程的粒度是每核的。

主机核心和目标之间的端到端路径,当接收到一个核心的请求时,Block层通常操作生成一个bio 实例(代表一个动态块I/O操作在内核中),使用Linux内核初始化相应的请求实例支持多种单核block队列(blk-mq),然后复制请求到block层对应核心的请求队列中,这些请求队列不同于i10队列,最后,block层请求实例会被转换为i10请求,为了符合NVMe-oF标准,i10请求时类似于PDU(Protocol Data Unit)的,最后,使用block层请求数据结构中的上下文信息,i10请求会被复制到相应的i10-lane的i10队列中。为每一个i10-lane设置一个专用队列意味着队列中的所有请求和数据包都将发送到相同的目标服务器,并将相同的TCP连接进行传输,因此,i10能将多个请求和数据包批处理到i10 “caravans”中,而所有的请求和数据都将用相同的TCP连接进行处理和传输,从而通过批处理的方式去显著的减少网络处理开销。

NVMe在存储栈和本地存储时候,提供了低延迟、低开销的通信,因此在本地访问下,在创建请求时会立即按响门铃(向存储设备发送一个信号,表明新的I/O请求已经就绪),但是在远程访问情况下,请求通过一个相对高延迟的高开销网络时,会涉及工作线程的高上下文切换开销,i10为了减少这种开销,因此引入了延迟门铃的概念,其中block层工作线程在按响门铃唤醒i10工作线程之前处理多个请求或超时,从而大大的减少了上下文切换的开销,而且为i10-lane队列提供了足够的请求/数据去生成合适大小的caravans

最后,i10 caravan是通过内核内的套接字接口进行传输的,如图2,当caravan到达目标端i10队列时,i10会解析caravan以重新生成bio实例、对应的请求并将它们提交给block层,接收到请求后,block层会执行类似于pci连接时访问本地存储设备相同的步骤:将请求插入NVMe提交队列,并且完成后将结果返回到NVMe完成队列,在本地访问之后,结果返回到block设备层,最后被i10抽象为响应caravan,并通过TCP连接发送回主机服务器。

2.2 i10-lane

创建i10-lane时有两个明显的选项:(1)为每个目标服务器创建一个i10-lane,它与服务器的核心数量无关,如图3a所示;(2)每个核心创建一个i10-lane,独立于目标服务器的数量如图3b。所以在高负载时,第一个选项会导致block层工作线程之间的高写争用,因为它们需要将请求写入相同的i10队列,第二个选项在这时也没有更好的选择,发送到不同目标的请求被迫处于相同的i10队列中,从而导致i10 caravans无法处理足够的请求,或者导致较高的CPU开销(用于将请求排序以批处理到相同的caravans中),甚至最糟糕的情况下,两种开销都会变得更糟。i10为了避免这种情况,是通过为每个(核心,目标)对创建一个i10-lane来避免这些开销如图3c,所以对于使用P个主机核心来访问T个目标服务器上的数据,i10会创建P×T个i10-lanes,与每个目标上的存储设备数量无关。

                                                    

                                                                              图3不同创建i10-lane的情况

对于每一个i10-lane的专用资源,其中blk-mq level request queues,i10使用blk-mq来利用block层中定义的每核请求队列,对于支持使用多核的高吞吐量随机读写的现代存储设备,由于两个原因,其公式有很大的不同:(1)在单个队列上运行的多核成为性能瓶颈;(2)由于寻道时间不是问题,最小化前端阻塞就变得更加重要。因此,最新版本的Linux内核允许block层创建每个内核的请求队列,并为blk-mq和底层远程访问层维护多队列上下文信息。这使i10能够有效地将blk-mq中的请求解复用到单独的i10-lane队列中。

i10 I/O queue,i10为每个单独的i10-lane创建一个专用队列,类似于NVMe标准中的I/O队列,区别在于它们是与远程目标服务器进行通信,而不是本地存储设备,一旦来自blk-mq的请求被转换为与NVMe兼容的命令PDUs,它们就被插入到i10队列中。NVMe标准允许创建多达64K的NVMe队列来支持并行I/O处理;因此i10的设计不应该受到可用队列数量的限制。

TCP socket instance,每个i10-lane维护自己的TCP套接字实例,并与目标建立长时间的TCP连接,以及相应的TCP缓冲区。需要在内核中维护每个TCP连接的状态已经非常小了。i10需要的惟一附加状态是TCP连接和相应的i10-lane之间的映射,该映射同样很小。

i10 worker thread,i10会创建一个专门的单核工作线程,它的工作是将i10 caravan在i10-lanes上双向移动。当同一核心上的任何i10-lane的门铃响时,这个工作线程开始执行,并将相应i10- ane队列中的命令pdu聚集到caravan中。工作线程然后移动caravan到相应的i10-lane的TCP缓冲区。最后,工作线程进入睡眠模式,直到运行一个新的门铃。

2.3 i10 Caravans

假设i10队列中的所有请求都通过相同的TCP连接到达相同的目的地,那么i10将多个请求批处理到一个i10 caravan中。这使i10能够从TSO和GRO等标准优化中受益,从而大大减少网络处理的开销。文中这里的主要观点是,i10队列正是创建caravans的地方,原因有两个。首先,在block层,blk-mq是单核的,在任何给定的时间点,可能有属于不同目标的请求(如图3(c)所示);因此,在block层对请求进行批处理将需要大量的CPU处理来对发送到相同目标设备的请求进行排序。其次,在TCP层进行批处理将要求i10分别处理每个请求并将其发送到TCP缓冲区,从而为每个请求创建一个事件;但是之前的工作[已经表明,这样的每个请求事件会导致较高的CPU处理开销。但是通过对自己的队列进行批处理,i10减少了这两项开销(图4)。我们将caravan所携带的最大数据量设置为64KB,以便与TSO支持的最大数据包大小保持一致。然而,为了防止单个caravan处理太多的小型请求,每个caravan可以处理的请求数量不能超过预先定义的聚合大小。

                                        

                                                                图4 i10为了减少每个请求网络处理开销的设计

2.4 Delayed Doorbells

最初的NVMe规范是为通过PCI express (PCIe)访问存储设备而设计的。尽管标准本身并没有规定如何使用门铃,但只要将请求插入NVMe提交队列(图5(a)),当前的存储堆栈就会按响门铃(即更新提交队列门铃寄存器)。由于PCIe在存储堆栈和本地存储设备之间提供了低延迟、低开销的通信,因此按请求按门铃可以在最小延迟的情况下达到设备的最大吞吐量。然而,在远程访问的情况下,请求通过一个相对高延迟的高开销网络,按每个请求按门铃会导致高上下文切换开销。在i10的特定上下文中,按响门铃意味着一旦block层线程向i10队列插入一个i10请求,它就立即唤醒i10工作线程来处理该请求(图5(b))。这会引起上下文切换,这在高负载时可能会导致高CPU开销。

                                                      

                                                                                图5 按响门铃的方式图示

因此i10使用延迟门铃的概念来减少这些开销。当i10队列为空时,将在第一个请求到达时设置一个门铃计时器。然后,当i10队列有与预定义的聚合大小相同的请求时,或者当计时器达到超时值时(以较早的时间为准),门铃就会响起。每当门铃响时,i10 caravans就会与i10队列中的所有请求一起创建,此时门铃计时器还未进行设置。我们注意到,延迟的门铃可以独立于请求是否成批加入到caravans中来使用。此外,如果应用程序生成低负载(导致请求观察超时延迟量),这种设计将导致额外的延迟。

3 i10的实现

本文在Linux内核4.20.0中实现了i10主机端和目标端。i10实现在普通硬件上运行(我们确实使用了大多数普通硬件支持的TSO和GRO特性),并允许未修改的应用程序直接在内核的TCP/IP网络栈上运行。

kernel_sendpage() vs kernel_sendmsg ():通过内核socket接口传输i10 caravans有两种选择。第一个接口kernel_sendpage()允许在发送每个页面的数据时避免传输端数据的复制,但将批处理大小限制为不超过16。第二个是kernel_sendmsg(),它接受内核I/O向量作为函数参数,并在内部将I/O向量的每个分散数据复制到单个套接字缓冲区中。这允许i10批处理大小大于16,从而在某些情况下降低网络处理开销。测试显示kernel_sendmsg()实现了更好的总体CPU使用(包括数据复制和网络处理开销),从而提高了总体吞吐量。因此,我们对i10 caravans使用kernel_sendmsg()来实现更好的吞吐量。

i10 no-delay path:对于延迟关键型应用程序,更重要的可能是避免i10批处理和延迟门铃带来的延迟。例如,可能希望在提交时立即对文件系统元数据(如inode表)执行读请求,以避免阻塞进一步的读/写请求,如果没有对原始请求的响应,就无法执行这些请求。对于这种情况,i10支持一个无延迟路径,当这样一个对延迟敏感的请求到达i10队列时,i10将刷新队列中所有未完成的请求,并立即处理对延迟敏感的请求。这是在延迟门铃铃响过程中使用一个简单的检查实现的:当收到一个延迟敏感的请求时,可以立即按响门铃,迫使i10创建一个使用所有未完成的请求和延迟敏感的请求的caravan。

i10 parameters:通常,我们期望i10中的每核吞吐量随着批处理大小的增加而提高,这是由于减少了网络处理开销。然而,当我们将批处理大小增加到某个阈值以上将导致边际吞吐量改进,同时需要更大的门铃超时值才能聚合更多的请求(因此,在低负载时增加每个请求的延迟)。这个阈值,也就是达到边际改进点的值,当然,取决于内核堆栈实现。对于我们的内核实现,我们发现16是最佳聚合大小,具有50 s的门铃超时值。

4 i10的评估和测试

5 结论

本文介绍了一种用于高性能网络和存储硬件的新型内核内远程存储栈i10的设计、实现和评估。i10不需要在内核之外做任何修改,它直接在内核的TCP/IP网络栈上运行。并已经证明,i10仍然能够实现与最先进的用户空间和基于rdma的解决方案相当的每核吞吐量。因此,i10代表了远程存储栈的一个新的操作点,允许在不修改应用程序和/或网络基础设施的情况下实现最先进的性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值