异步 I/O 栈¶
标题:[저자직강] Asynchronous I/O Stack (성균관대 정진규교수)
日期:2022/09/05
作者:정진규
链接:https://www.youtube.com/watch?v=_I8SDqdcCns
注意:此为 AI 翻译生成 的中文转录稿,详细说明请参阅仓库中的 README 文件。
备注:推荐配合幻灯片看,里面有很精细的延迟测量图示。
那么,我们就开始吧。大家好,我是被介绍过的成均馆大学的郑镇圭。得到大家这么多的称赞,我现在有点不知所措。总之,今天要讲的内容,题目是 Asynchronous I/O Stack。这篇论文是在SSD性能呈几何级数发展、变得非常快的状况下,对内核的I/O Stack如何能很好地应对这种高速SSD进行了思考并提出了解决方案。所以这篇论文去年在USENIX ATC上发表了。那么,在说明这篇论文之前,我简要整理了一下在准备发表过程中我迄今为止所做的研究。主要的研究主题大致分为三类。第一个是过去研究过面向云计算的机器虚拟化技术;然后过去也研究过移动系统、像安卓这样的智能设备上的内存管理;最近主要是研究为了更好应对新兴存储技术的I/O软件栈。那么,简要说说最近研究的内容有哪些:首先要说的是,当存在这种高速SSD时,在I/O栈中,像数据库或NoSQL这样的数据密集型应用程序如何能良好运行。为此,跨越整个I/O栈良好地进行I/O处理会非常有帮助。我们曾做过这样的研究。接下来是,随着像三星的Z-SSD或英特尔的Optane SSD这种所谓的超低延迟SSD的出现,I/O栈本身也出现了问题。当这个超低延迟,比如说I/O延迟在10微秒甚至更短,也就是非常快的存储设备时,内核栈如何能成为适当提供这种I/O存储设备性能的栈。最近,又因为构建了这种低延迟I/O栈后发现缺少I/O调度功能,所以还研究了对该I/O调度功能进行了补充的I/O栈。不过这个目前还没有发表。此外,虽然主要做的是软件研究,但也思考过另一方面:如果存在这种高效低延迟存储设备时,在CPU方面思考些什么会对计算机系统更有帮助?于是思考了用硬件处理一次按需分页的论文。这是和首尔大学的李在赫教授共同研究的。还有,当存在非易失性内存时,如何良好管理写缓冲区才能使应用程序性能更好?总之,全面思考了在这种存储技术发展时,软件方面如何做好才能使系统变得更好。今天,我将主要详细讲解其中的低延迟I/O栈。
所以,动机是这样的。这是计算机体系结构中经常展示的图。这是将构成计算机系统的主要组件的访问时间用对数尺度绘制的图。请看,最下面的两条线是DRAM和SRAM,CPU也可以看作在类似SRAM的位置。内存和硬盘、二级存储之间的性能差距曾经是相当大的。但随着SSD的出现,这个差距开始显著缩小。但我产生动机是因为更快的SSD。就是所谓的超低延迟SSD,像三星的Z-SSD或英特尔的Optane SSD,看这些SSD的I/O性能,仅考虑I/O设备类型时,它们展现出个位数微秒级的非常快的I/O延迟。当这种I/O设备出现时,最终会产生问题。我所指出的问题是内核的I/O栈开销成为了问题。最终,这个内核I/O栈是在我们从应用程序中执行像sys_read
、sys_write
这样的系统调用时,执行该I/O操作的一系列工作发生的部分。随着设备变快,这个时间相对地就成为了问题。举个简单的例子,比如在普通的SATA SSD上,这个图表显示了执行4KB读取所需的时间,大约80多微秒。其中绿色部分是实际执行设备I/O的时间,前面的红色部分是经过内核栈的时间,再前面是用户的时间。因为用了FIO基准测试,所以用户时间很短,然后是内核的I/O栈时间,再是设备时间。当然,在I/O如此慢的存储设备中,内核栈的开销只占大约6%,并不算多。但是,当SSD变得像刚才提到的Z-SSD或英特尔Optane SSD这样的超低延迟存储设备那样快时,I/O栈所占的比重就会像这样变大。请看,内核对应的时间实际上并没有太大变化。因为是相同的系统,执行几乎相似数量的命令,所以时间相似,但相对地,因为设备时间变得太快了,所以其比重就变得这么大。想想写操作,缓冲写因为是异步的,如果考虑执行缓冲写加上F-SYNC的同步写时,当SSD变快成这样,内核I/O栈所占的比重就会变得相当大。最终,为了优化I/O,当前内核栈的开销是相当大的,有必要想办法对其进行优化。
那么,优化I/O、优化内核I/O栈的直观方法,思考一下就是仔细分析内核I/O栈中发生的操作,然后对其进行优化就可以了。所以,这里用红色显示的部分可以看作是内核I/O栈的操作。详细的操作我就不解释了,但我想说的是,仔细一看,其实各个操作本身都很小很小。例如0.3微秒、0.33微秒、0.72微秒等等。如何进一步优化减少这些时间,是我们面临的第一个问题。再想想写操作,假设执行写操作、同步写操作,假设执行写操作直到F-sync,通常文件系统会提供崩溃一致性机制,所以通常会发生三次I/O,在这三次I/O之前,各自都有这样的I/O栈操作存在。同样地,如何逐个优化这些操作,也成为了一个很大的困扰。把0.3微秒减少到0.2微秒,把0.3减少到0.2,这样逐个减少,可能会变得非常困难。
所以我们思考的方法是换个思路。是什么方法呢?我们论文的标题是Asynchronous I/O Stack(异步I/O栈),最终是从异步I/O中得到了一些启发。是什么呢?我们在执行I/O时,直观使用的方法是同步I/O。意思是说,当有计算任务和I/O时,执行这两者最直观的方法是同步地直接发出I/O。但是,如果这两个操作之间存在可以相互重叠的部分,让它们这样相互重叠,这最终就成了一种异步I/O。发出I/O后,线程与I/O异步地继续做自己的工作。这样,让计算操作和I/O操作重叠执行,最终首先能提高吞吐量。因为CPU和I/O设备可以同时使用。但附加的好处是,执行一项任务(我同时完成A和B)的延迟本身也能有所减少。这就是:如果这种异步I/O,即让I/O和计算部分重叠的方法,应用到I/O栈操作本身,我们是不是能构建出更快的I/O栈?这就是我们的想法。
所以,应用了我们想法的Read、Write和路径简要说明如下。看原生内核中的Read I/O路径,I/O栈操作同步地发生,操作产生后,同步地执行I/O操作。然后那个I/O结束后,再次执行I/O栈操作,返回用户空间。我们的想法是,在I/O栈操作中,将与I/O可以重叠的操作,这样异步地执行,最终目的是减少Read延迟,即系统内核提供的Read操作的延迟。
对于Write的情况,首先,如果是简单的缓冲写,实际上做不了什么。因为即使应用程序请求Write,也只是将数据缓冲到OS的缓冲区缓存之类的地方,然后就直接返回用户空间了。所以我们思考的部分是,当像fsync
这样在用户空间同步地要求将数据送到磁盘底层的请求时,对那个操作进行了聚焦。在典型的日志文件系统中,为了崩溃一致性机制,会像这样发生I/O栈操作 -> I/O -> I/O栈操作 -> I/O -> I/O栈操作 -> I/O,总共发生三次I/O。所以,我们同样地,让这些I/O栈操作与I/O相互重叠,从而获得减少延迟的效果。到这里,是整体展示我们提出的方案中重叠的部分。那么,针对这种方法,具体在读取路径上做了哪些事,以及在这个过程中,因为块I/O层的开销比较大,为了改善其开销,还引入了轻量级块I/O层,稍后会介绍。之后,再介绍我们在写路径上做了哪些事,展示评估结果,最后做结论。
那么,更具体地说明我们如何优化读取路径:首先分析了读取路径中的行为,并对其进行了优化。读取路径中发生的事情如下。因为是I/O栈,所以是在多个层中各自执行任务,最终产生读取I/O。首先做的事情是到达页面缓存VFS层,执行页面缓存查找。基本上假设是缓冲读,所以在页面缓存中查找我想要的数据是否存在,如果不存在,那么为了执行实际的I/O,会分配一个页面来容纳那个I/O。之后,会向文件系统发送请求,要求从磁盘填充该页面的实际内容。所以页面缓存文件系统接下来要做的事情是,先将这个分配好的页面放入页面缓存。因为预先放入页面缓存,这样以后如果有访问相同页面的线程,在它们之间的同步上会更容易些。然后,判断要读取的文件页面的逻辑块地址。接下来,将I/O发送到底层的块层。所以通过提交BIO发送到块层。顺便说明一下,每个操作花费的时间是我们基于Optane SSD测量的,以4KB I/O为基准测量的。下面显示的方框也是按各自时间比例对应大小绘制的。这样做了之后,通过块层,最终到达设备驱动层,这里是以NVMe设备为基准编写的。在NVMe设备驱动层,首先做的是DMA映射操作。意思是,要对分配好的页面进行DMA,需要将该页面在I/O地址空间中的地址进行映射,这是通过IOMMU执行地址映射。执行完DMA映射地址操作后,最后创建NVMe I/O命令,并将其传递到设备的命令队列。做到这一步,实际的I/O就发出去了。主要地,请求这个I/O的线程在上下文切换过程中,I/O则在设备中被处理。所以当I/O结束时,中断会飞来,在中断处理程序中,首先对之前做的DMA映射进行解映射。那个缓冲区现在不需要DMA了,所以在I/O地址空间中进行解映射。之后,执行对该块请求的完成操作,最终唤醒请求该I/O并在等待的线程,进行上下文切换,最后请求读取的用户线程再次进入页面缓存,经历将数据复制到用户内存的复制到用户过程。到这里,读取操作就全部完成了。
如前所述,每个操作本身都很小,要再逐个优化有点困难。但我们寻找操作中能与I/O重叠的操作,让它们重叠执行。但是仔细想想,其实这些操作一个个看起来都是必需的。所以我们也用了一些技巧来实现重叠。我们首先针对的操作是页面操作和DMA映射。并且,如果同步地执行这些I/O过程,会是0.2μs, 0.3μs, 0.3μs……。当I/O请求到来时,从页面池中分配吧。那么这个页面池分配是简单的链表,因为预先执行了DMA映射,所以从链表中取出一个的操作可以非常快地执行。但问题是,如果从这个页面池消耗页面,消耗完后如果什么也不做,最终就没了。所以,分配页面和执行DMA映射的这个操作,如果在发出I/O命令、将I/O命令发送到设备之后执行,那么就可以将这个计算部分与I/O重叠,从而在减少整体I/O延迟的同时,执行的动作是相同的。这样,首先让页面分配和DMA映射重叠了。
其次我们关注的部分是放入页面缓存的部分。总之,执行页面缓存查找后发现页面不存在,那么进行的操作是在分配该页面之后,将其挂载到页面缓存结构上。Linux是将其挂载为基数树的形式。将这个页面挂载到页面缓存上的操作,如果能重叠执行,应该也会有帮助。但我们执行这个时考虑的问题是:为了插入页面缓存,也需要先查找一次页面缓存。查找时,如果我想要的页面不存在,那么由于未命中,就会执行将我分配的页面插入页面缓存工具的操作。这样做的原因是,如果在相近的时间点,有线程执行访问相同文件偏移量的页面缓存读取,它们同样会执行这个页面缓存的查找。那时,如果无论如何,这个页面缓存的查找和插入页面的操作本身是同步化的,是临界区,所以如果两个线程试图访问相同的索引,一个线程已经成功插入了,那么另一个线程会发现该文件偏移量处已有页面,因此不需要再请求额外的、重复的I/O,而是一起等待那个页面。这样,它同时也起到了一种同步的作用。最终结果是,即使同时尝试访问相同的文件偏移量,I/O也只会发生在一个线程上。
最终,从我们的立场思考,如果把这个页面缓存插入操作放在前面,虽然不会发生重复的I/O,但页面缓存操作的这个时间成为了代价。最终我们权衡的方法是:把页面缓存插入放到后面做吧。反正它是耗时的操作,不要在前面做,在提交I/O之后再去做。这样做的话,如果同时有想访问相同文件偏移量的线程到达,这时I/O可以在两个线程上同时执行,所以I/O本身在两边可以同时发生,但其中,只有成功将页面插入页面树的线程才能使用该页面。对于插入失败的线程,那个页面就直接丢弃掉,最终I/O还是会发出,但那只是多了一个不必要的I/O而已,我们仍然让它使用页面缓存中已有的页面。这样做的结果是,虽然产生了重复I/O的成本,但通过这种方式让页面缓存操作的时间本身与I/O执行重叠,从而减少了整体的I/O执行时间。
那么,可以考虑的是,这个重复的I/O到底有多大问题?我们在实际测试的RocksDB基准测试工作负载中测试过,发现访问相同文件偏移量的情况发生次数非常少,可以忽略不计,因此判断它实际上对性能没有那么大影响。
接下来我们考虑的另一个操作是执行DMA地址解映射的操作。这个操作发生在I/O结束后。它最终也存在于读取的关键路径内,所以如果能将其重叠,也能进一步减少延迟。我们采用的方法是惰性DMA解映射。意思是,即使DMA I/O结束了,也不立即执行对该I/O缓冲区的DMA地址解映射,而是等到系统空闲或者正在处理其他I/O请求时,才执行DMA解映射。最终,仿佛是I/O的解映射操作被重叠了的效果。准确地说,并不是在我这次请求的I/O操作过程中执行了DMA地址解映射,而是在下一次I/O发生时,才执行对之前已结束I/O的缓冲区的DMA解映射。
这样做实际上有个问题,就是DMA缓冲区的可写性窗口(Writability Window)会变大一点。可以说是一种安全性问题。但仔细想想,其实也不是什么严重问题。因为这个DMA解映射操作本身会访问IOMMU,其开销相当大,所以Linux默认已经使用了延迟保护方案。意思是,即使DMA解映射请求来了,也不会立即执行解映射,而是收集一些后稍后执行解映射。最终结果类似,和我们提出的方法一样,不是立即执行DMA解映射,而是稍后执行,所以存在DMA攻击的可利用性窗口变大的问题。但反正无论如何都存在类似问题,我们也只是类似地遵循了那个方案。不过,如果可能存在某些安全问题,为了安全起见,我们可选地提供了可以不执行这个解映射的选项。
到这里为止,虽然基本将相当多的I/O栈操作重叠了,但仍有很大的开销存在。BIO提交以及设备驱动部分的完成方面发生的块层操作。所以为了进一步优化这部分,接下来我们提出了轻量级块I/O层(Lightweight Block I/O Layer)。
那么,说明这个操作:要讲块层操作的优化,需要先说明当前块层是如何运作的以及存在哪些开销。在Linux中,使用多队列块层(Multi-Queue Block Layer)来运作。基本使用BIO结构。其内部使用请求对象,利用这些提供块I/O服务。主要通过submit_bio()
这个函数,块I/O请求进入块层。从那时起发生的事情是:首先,在这两个层中,上层使用BIO对象,但它进入块层后,就会被制作成请求对象。在这个过程中,会执行合并操作以及标签分配等多种操作。但这个过程出乎意料地耗时。特别是合并操作,因为最终要判断与块层中挂起的其他请求的邻接性,所以耗费相当多的时间。接下来,请求化的对象通过下面的软件队列和硬件队列进行调度操作。当它通过调度到达可以分派到设备的状态时,在设备驱动内部又会分配其他对象。基本上有BIO,也有请求和BIO对象,但比如IOD、PRP列表、分散/聚集列表这类东西,又需要分配额外的对象,最终发出IOD命令。在通过空间转换和多队列的调度部分的过程中,这些执行各种动态内存分配的东西就表现为时间成本。
所以我们提出的方法是:事实上,块层开销大已经是众所周知的事实了。这种空间转换过程本身,在空间转换过程中执行I/O合并的操作效率非常低。尤其是在像硬盘这样慢的存储设备上,合并相邻的I/O以减少请求数量是有用的,但在这种低延迟SSD的情况下,为了减少I/O请求数量而花费更多时间本身反而成了问题,所以这些东西效率低下是已知的。像I/O调度特性,特别是在低延迟SSD中,最好不使用。所以Linux为SSD的调度器默认都设为None调度器。而且,绕过这些调度层本身,从延迟角度讲实际上更有帮助也是众所周知的事实。特别是,调度本身确实是非常有用的功能,但我们认为绕过它也没问题的原因是:NVMe协议本身如此,研究社区也表明存储设备内部已有I/O调度特性,所以可以利用那个,不需要软件层再做任何调度。我们认同这种复杂的调度层是不必要的。而且如前所述,执行各种动态内存分配的操作,在Linux中默认使用SLAB分配器,内存分配速度相当不错,但像往常一样,如果SLAB分配器的SLAB不足,最终需要通过伙伴分配器进行内存分配,这些东西可能成为额外的延迟尖峰。
所以我们提出的层是:尽可能简化块层。特别是,我们假设是针对快速的NVMe SSD设备,基于NVMe协议的快速SSD,所以针对NVMe协议进行对象配置优化,并据此最小化不必要的结构转换或对象分配,只构建用于投掷NVMe I/O请求所需的最小结构。所以我们称之为轻量级块层。我们使用的对象是轻量级BIO,即LBIO对象。这个对象假设设备是NVMe的,包含了为发出I/O命令所需的NVMe协议特定特性,如PRP列表等。这有点像是逆行了某种分层方法,设备特定的部分在上层也包含了一些。但这样做的代价是,只要延迟不损失,就尽量让延迟变短。所以不构建调度层,任何对象最终都做成每核数组的形式,实现了无锁分配和无锁标记等。在提交设备的过程中,在我们的场景下,只做一次额外的动态内存分配,就是PRP列表,这是为了指定I/O缓冲区的可变长度I/O缓冲区的NVMe协议特性,为了这个对象是唯一需要动态内存分配的形式来配置块层。
简要地,测量了我们制作的块层在减少I/O延迟方面的效果:测量提交块I/O所需时间的CDF(累积分布函数)。图有两个,一个是4KB随机读取,一个是32KB随机读取。原因是大于32KB的情况下,会多发生一次动态内存分配。在LBIO中也是,所以区分了两种情况。请看,我们的(方案)是黑线,原有的Linux BIO是红色虚线。可以看到,块I/O提交延迟大约减少了4倍以上。所以BIO大约需要1微秒、3微秒的时间,而我们的方案大约是0.4微秒、0.5微秒,实现了非常快的块I/O提交延迟的块层。
所以最终,在读取路径中的行为,之前展示的剩余的与块层相关的部分,像下面这张图一样减少了,从而进一步减少了延迟。最终,最初展示的读取路径中的行为如果是上面这样,那么我们就如下面这样,让大部分操作与I/O操作重叠,以减少延迟。
好的,以上首先是对读取路径的说明。
我稍微看一下是否有问题,检查一下聊天。没有问题。好,那么接下来要讲的是写路径。这里也同样,分析原有的写路径并提议我们的重叠技术。写路径的行为有点不同。基本上,在写路径中也同样提供前面提到的轻量级块层,但写路径中应用前面读取路径中应用的那种重叠,比想象中要困难。因为写操作可能发生在哪里是未知的,要预先准备其实很困难。想想看,读取那边虽然不知道会读取哪个文件偏移量,但反正读取新东西时需要页面,也需要DMA映射,存在可以预先准备的东西。但写操作那边不是这样。
所以我们采取了更宏观的视角,从远处观察,特别是在通过F-Sync使写操作下发的文件系统崩溃一致性机制运作的情况下,找到了重叠的机会。所以以EXT4的有序模式为基准,当F-Sync下来时,做的事情是:首先执行将文件的脏页发送到磁盘的操作。准确地说,是先执行I/O栈操作准备I/O命令,最终将其发送到设备。这样,文件的脏块全部发送到磁盘后,就唤醒文件系统的日志机制。因为EXT4使用jbd2,所以唤醒jbd2。唤醒后,这个jbd2就开始执行日志记录。首先执行在日志区域写入日志块的日志写。所以准备对元数据更改进行日志写。这时仍然是CPU密集型的I/O栈操作。所以分配缓冲页,确定在日志区域的哪个位置写入,然后执行校验和计算等使用CPU部分的工作。日志块准备好后,就发送到设备执行日志写。日志写结束后,最后准备提交块,先下发提交块进行刷写,刷写结束后,最后发送提交块I/O执行提交块I/O。那结束后,就返回用户空间,fsync
就结束了。
如我们前面概述所示,让对应的计算操作和下面的I/O操作重叠。能这样做的最大原因其实是I/O栈操作是发生在内存中的操作。更准确地说,为了崩溃一致性机制,这三个I/O必须按顺序发生。特别是最后做提交块时,在提交块写入磁盘之前,有内存屏障在这里。必须确认前面这些I/O都完整地写入磁盘后,才能写入提交块。这样才能保证崩溃一致性上没有问题。我们的想法是:那是在磁盘状况下让它那样发生就可以了。内存中的操作真的也需要这样仿佛三个必须按顺序发生吗?需要这样相互关联、仿佛有先后顺序地动作吗?这就是我们的想法。
所以我们执行的方法是像这样进行重叠:首先,数据块写同样执行。但绝对时间本身会减少。因为用了前面提到的轻量级块层,发送I/O命令的时间变短了。做完这个后,我们发送数据块后,接下来要做的事情是:不要等这个I/O结束,而是直接像这样唤醒jbd2
。唤醒jbd2
后,jbd2
立即醒来,马上开始准备日志块的工作。这是在内存中。这个日志块准备好后,就直接发送该日志块的I/O。因为准确地说,只需要保证数据块、日志块和后面将发生的提交块之间的顺序正确即可。所以发送日志块。但因为数据块已经先发出去了,所以数据块I/O结束后,日志块I/O会被处理。在这个过程中,同样也执行提交块的准备工作。但提交块与日志块不同,不会发送I/O。因为不知道磁盘内部会发生什么重排序,所以提交块只是准备好,然后等待所有东西。等待什么呢?等待下面已发送的数据块I/O和日志块I/O全部完成。所以当确认日志块I/O完成后,才发送刷写,之后发送提交块。这样最终在磁盘上,无论如何,在崩溃一致性之上,日志记录所要求的提交块和其前面的I/O之间的屏障得到了确切的遵守。但是,计算对应的部分无论如何是内存中的部分。即使我在上面提前准备好了,最终改变的也只是内存中的某个值,并没有改变磁盘上的任何值。如果计算机关机再开机,内存的值反正会丢失,我们为了崩溃恢复而查看的完全是磁盘上写入的数据。所以日志相关的、在内存中动作的这些操作,稍微提前进行也没关系,这是我的想法,并这样实现了。所以最终,相比前面展示的原生内核的F-Sync路径,我们的动作修改如下面这样,可以预期延迟会像这样减少。
所以,我们将其在Linux内核中实现并进行了评估。使用的内核是Linux内核5.0.5。在普通的服务器环境中实验,使用的存储设备是三星Z-SSD和Optane SSD。论文中有两种结果,但在之后的报告中只带来了Optane SSD的结果。工作负载方面,使用FIO作为合成基准测试,使用RocksDB的DBBench作为真实世界工作负载。
首先展示的是FIO性能。首先是随机读取的性能。左边是单线程下改变块大小时,I/O延迟是多少。右边是改变线程数量时,IOPS是多少的吞吐量测量图。左边图是单线程,所以延迟是关键。因为是单线程以单队列深度发出I/O的情况。请看,原生是白色,我们的方案是红色。因为是延迟图,短的好。所以整体上大约减少了23%的延迟。我前面其实没提到,剩余时间中占相当大部分的是上下文切换。在I/O中减少上下文切换开销的一个众所周知的方法是使用轮询。所以我们在我们的方案上同时应用了轮询,用黑色线展示。此时,基于Optane SSD,4KB I/O延迟减少了7.6微秒。这个我多少有些自豪地认为:因为Optane SSD的设备时间大约6微秒左右,而使用内核I/O栈时这个时间超过10微秒,是两位数的延迟。我们的方案加上轮询后,即使通过了完整的I/O栈,也能达到7.6微秒的个位数延迟,我为此感到高兴。
接下来右边图是增加线程数量时,IOPS能跟到多少的性能评估图。请看,因为是IOPS吞吐图,数值越高越好。所以当设备达到饱和后,IOPS性能反正是一样的。但在此之前,总是我们的方案先显示出更好的性能。在相同线程数时,相比原生内核显示出更好的性能。
接下来展示的是写性能。测量了执行写操作和F-Sync的FIO基准测试的性能。这里只展示了EXT4有序模式,论文中还包含了EXT4日志模式。单线程下改变块大小时测量延迟,同样可以看到延迟减少了。最终原因是,通过让原本同步发生的I/O栈操作和实际I/O相互重叠,从而将发出I/O的时间提前了,最终延迟减少了。这样I/O延迟变好也反映在IOPS上。最终可以看到在设备达到饱和点之前性能变好了。但是请看,随着线程增多,性能增益其实并没有那么好。可以看到性能变好的幅度在减少。这其实是理所当然的。因为增加线程数本身就是让I/O和计算重叠的一种典型方法。创建很多线程,自然就能同时执行I/O和执行计算,它们自然地重叠了。所以线程增多时,相对地,我们方案带来的收益就减少了。
所以这次使用真实世界工作负载RocksDB工作负载,用DBBench测量了性能。左边是随机读取工作负载,右边是填充同步工作负载。所以在随机读取工作负载中改变线程数等测量吞吐量时,最多获得了约27%的性能提升。在填充同步工作负载中增加线程数测量吞吐量时,最多获得了约44%的性能提升。
所以下一张幻灯片是为了再讲一点而准备的。就是说,我们其实是在使用快速SSD的同时,最终执行I/O栈的优化,试图最大限度地发挥其性能。通过最大限度地压缩I/O栈操作,我们真正想实现的是:如果SSD变得真的非常快,那么原本使用快速I/O设备的好方法之一就是使用用户级I/O,像英特尔的SPDK那样。所以当使用用户级驱动时,内核的I/O栈操作完全被排除,性能几乎能达到设备时间本身的水平。所以这个图比较了原生内核、我们的方案和SPDK下的I/O延迟。但请看,我们原本其实雄心勃勃地想:即使使用内核栈,也要让延迟和SPDK一样。这或许有点不可能,但我们是带着那样的目标进行工作的。首先,相比原生内核,可以看到明显降下来了。相比原生内核,像这样陡降了。但仍有斜线阴影部分和红色部分存在。看这部分,首先红色部分其实是复制到用户。意思是,无论如何,通过内核I/O栈,通过页面缓存执行I/O时,会发生两次内存复制。发生一次DMA(到内核内存),然后从内核内存到用户内存再复制一次。从内核到用户的内存复制时间就是红色对应的部分。在SPDK中当然没有这个时间,所以SPDK获得了那部分时间的好处。但我们认为这不是大问题。因为SPDK如果也要做自己的一些缓冲管理,比如类似页面缓存的管理,最终也会增加相同的时间。但是上面出现的斜线阴影部分的操作,我们内部分析后发现,是页面缓存相关的操作还多一些,文件系统操作也同样还多一些。另外,还有一个占比较大的是上下文切换对应的部分。所以上下文切换对应的部分,实际上SPDK使用轮询所以排除了。但SPDK使用轮询的代价是CPU利用率会升高一些。绝对延迟上SPDK更好,因此很难说我们的比SPDK更好。最后,SPDK严格来说,实际上只做纯粹地投掷I/O的事情,只起到某种设备驱动程序的作用。最终磁盘是块的连续数组,要在其上形成文件结构、目录结构等文件抽象的东西,在SPDK实验中并不存在。需要在其上附加块文件系统(Block FS)之类的东西。但执行那些操作的话,最终文件系统存在的理由是因为块层本身太简单,为了使用文件抽象这种非常有用的抽象才需要文件系统。如果加上那些东西,那些操作也会增加SPDK的时间,我主张(但可能没有根据)会变得差不多吧。尽管如此,为了进一步减少这种复制到用户的时间,我们内部也在进行更多研究,但到目前为止还没有像样的好成果,有点遗憾。如果以后准备好了,有机会再分享给大家。
总之最后说了很多。无论如何,我们提出的这个Asynchronous I/O Stack,是为了让内核I/O栈在低延迟SSD上良好应对,需要减少内核I/O栈延迟的某种想法。我们利用Asynchronous I/O这个概念,让内核I/O栈的操作与I/O重叠,从而减少整体I/O延迟。我们提议了这种操作。并且块层本身,因为当前块层在低延迟环境下不合适,所以我们进行了精简。通过这些提议的技术,在Optane SSD基准下,也显示了个位数微秒级的延迟。并且在FIO和DBBench工作负载中也确认了性能提升。我们制作了这个栈,在GitHub上开源公开了,如果有兴趣的人可以访问URL自由使用。
好的,我今天准备的内容到此为止。如果有问题或评论,非常感谢。