并行磁盘访问对性能的影响

2020-08-26 13:22:38

众所周知,加速数据处理任务的方法之一是将数据划分为小块,然后并行处理这些块。让我们假设我们可以很容易地对任务进行分区,或者输入数据已经被分区为单独的文件,这些文件都驻留在单个存储设备上。我们还假设我们在这些数据上运行的算法足够简单,这样计算时间就不会成为瓶颈。通过并行读取文件可以获得多少性能?我们能丢一些吗?

在开发fclone重复文件查找器时,我花了大量精力利用现代硬件的功能使其尽可能快,这就是为什么我设计我的程序时,所有数据处理阶段都可以很容易地并行化。撰写本文时的最新版本(0.7.0)允许分别为随机I/O和顺序I/O设置线程池,并且可以根据不同类型的存储设备调整设置。

在这篇博客文章中,我分别介绍了我在固态硬盘和硬盘上进行的几个实验的结果。所有实验都是在运行Ubuntu Linux 20.04的Dell Precision 5520笔记本电脑和运行Mint Linux 19.03的7200 RPM东芝硬盘上进行的。这些实验要么是在2016年推出的Dell Precision 5520笔记本电脑上运行Ubuntu Linux 20.04,要么是在运行Mint Linux 19.03的Dell Precision 5520笔记本电脑和512 NVMe固态硬盘上执行。

作业中最耗时的部分实际上是将数据从磁盘读取到内存中,以便计算哈希。文件的数量通常很大(数千甚至数百万),并且计算它们的散列的问题并行得令人尴尬。我的重复查找程序做的第一件事是扫描目录树并获取文件元数据,如文件长度和索引节点标识符。此过程发出大量随机I/O请求。正如预期的那样,多线程带来的性能收益是巨大的,如图1所示。

在下一阶段,按大小匹配的文件将通过其初始4 kB数据块的散列进行比较。这也涉及大量随机I/O-对于每个文件,fclones打开它,读取前4 KB的数据,计算哈希并关闭文件,然后移动到下一个文件。固态硬盘在随机I/O方面非常出色,高并行度也会在这方面带来巨大的成功(图2)。令我惊讶的是,即使是远远超过CPU核心数量(4个物理,8个虚拟)的64个线程仍然提高了性能。我猜,对于如此小的存储请求,您需要提交非常多的线程才能让SSD保持忙碌。

让我们看看IOSTAT。在只有1个线程的情况下,iostat报告CPU大部分处于空闲状态,但SSD利用率为100%。

平均CPU:%USER%NICE%SYSTEM%iowait%Step%IDLE 2,39 0,00 5,03 5,03 0,00 87,55设备r/s元/秒rqm%rqm r_await rareq-sz aque-sz%utilnvme0n1 5458,00 21,32 0,00 0,00 0,11 4,00 0,00 100,00。

这是否意味着固态硬盘已经达到100%的性能?不会,因为%util是根据设备服务请求的挂钟时间比率计算的。这还没有考虑到同时提交多个请求的影响。看起来我的SSD非常乐意接收更多的负载。对于64个线程,%util仍然是100%,但是服务读取请求率增加了40倍以上:

平均CPU:%USER%NICE%SYSTEM%iowait%Step%IDLE 28,46 0,00 66,92 4,62 0,00 0,00设备r/s人民币/s rrqm/s%rrqm r_await rareq-sz aque-sz%utilnvme0n1 223974,00 874,90 0,00 0,00 0,17 4,00 0,00 100,00。

顺便说一下:为什么即使低于64个线程,平均队列大小AQU-SZ仍然是0,00,这对我来说仍然是个谜。欢迎在评论中留下任何线索。

那我怎么知道CPU不是这里的主要瓶颈呢?Iostat给出的CPU负载数字很高,不是吗?我测量了当所有数据都被缓存时,通过重新运行它,而不事先删除缓存,完成任务所需的时间。当所有缓存时,元数据扫描耗时1.5秒,部分哈希耗时1.7秒。这仍然比涉及物理读取时快得多,所以不,I/O仍然是主要瓶颈,即使有64个线程也是如此。

那么顺序I/O读取又如何呢?将顺序I/O并行化是否也提高了速度?看起来是这样,尽管没有随机I/O提高那么多(图3)。fclones算法的最后一个阶段是对完整文件进行散列-在这个实验中,文件主要是JPG和原始图像,平均大小约为10MB。在经历了8个线程之后,涨幅似乎更早地达到了平台期。在这种情况下,操作系统有机会预取数据,因此即使我的应用程序在一段时间内没有请求数据,它也可以让SSD保持忙碌。

与固态硬盘相反,旋转驱动器具有较大的寻道延迟,并且可以低得多的速率处理I/O请求。因此,我们绝对可以预期硬盘上的随机I/O会比固态硬盘上的慢得多。但是,我们能期望通过并行读取获得任何性能提升吗?我最初的想法是不应该有任何明显的收益,因为单个HDD在给定的时间只能服务于单个读取请求,然后它必须重新定位磁头以“跳转”到另一个文件,这在原则上看起来非常“顺序”。在队列中堆积大量请求应该不会改变任何事情:无论如何,HDD都会按顺序处理它们。HDD也足够慢,即使是单个快速线程也应该让它完全忙碌,因为至少有一个请求随时准备好服务。

我错了。事实证明,对于小而随机的I/O请求,即使是在HDD上,并行也会带来显著的收益(图4)。但发生这种情况的原因与固态硬盘不同。寻道延迟在很大程度上取决于I/O请求的顺序。如果进程从多个线程提交更多I/O请求,操作系统可以根据物理数据位置对它们重新排序,从而最大限度地减少硬盘磁头必须移动的距离。

不幸的是,当顺序读取较大的数据块时,使用多线程实际上会损害吞吐量(图5),这是因为操作系统交错处理来自不同线程的I/O请求,并且HDD将不得不重新定位磁头频繁地从一个文件跳到另一个文件。吞吐量损失多少在很大程度上取决于操作系统及其配置,但通常我预计这是2x-10x的一个因素。

在应用程序中解决此问题的一种方法是不允许许多线程在操作系统级别争用同一个HDD设备,而是让应用程序自己控制I/O请求调度。您可以使用专用的单个线程来处理到单个旋转驱动器的所有I/O(这是fclone从0.7.0版开始所做的事情),或者通过与每个HDD相关联的临界区(互斥体)保护I/O操作,并且锁定在足够粗的粒度,以使查找时间无关紧要。我不建议将整个应用程序设置为单线程,因为这将不允许向多个设备发出并行请求,也不会带来上面概述的好处。

此外,许多操作系统允许告诉内核应用程序将按顺序读取文件数据。例如,在Linux中,打开文件后,只需使用POSIX_FADV_SEQUENCE调用POSIX_FADEST:

在内部,此选项会增加预读缓冲区的大小,因此系统可以在更大的区块中提取数据,从而潜在地减少了寻道次数。此标志的效果清晰可见,它提高了并行访问的性能,但还不足以将寻道开销降低到零。有趣的是,我在测试中没有观察到这个标志对单线程吞吐量的任何影响,但观察到YMMV。

随机I/O和读取元数据受益于固态硬盘和硬盘这两种类型的驱动器上的并行性。

在Linux上的多个线程之间共享设备时,调用POSIX_fAdise通知系统有关顺序访问模式的信息会略微提高读取吞吐量