加速文件I/O:mmap() vs. read()

46

我有一个Linux应用程序,可以并行读取150-200个文件(4-10GB)。每个文件都是按顺序读取的,以小而不等的块为单位,通常每个块不到2K。

目前,我需要维持超过200 MB/s的读取速率。磁盘可以很好地处理这个请求。预计将需要超过1 GB/s(此时磁盘的性能无法达到)。

我们实现了两种不同的读取系统,都大量使用了posix_advise:第一种是mmap读取,在其中我们映射整个数据集并且按需读取。第二种是基于read()/seek()的系统。

两者都运作良好,但只适用于中等情况,read()方法更好地管理我们的文件缓存,并且可以处理数百GB的文件,但速度受限,mmap能够预先缓存数据,使得超过200MB/s的持续数据传输速率易于维护,但不能处理大型总数据集大小。

所以我的问题如下:

A: 在Linux上,read()类型的文件I/O是否可以通过posix_advise之外的进一步优化来实现?或者说,调整磁盘调度程序、虚拟内存管理器和posix_advise调用是我们所期望的吗?

B: 是否有系统化的方法可以使mmap更好地处理非常大的映射数据?

Mmap-vs-reading-blocks是我正在处理的类似问题,并提供了一个良好的起点,解决这个问题,以及mmap-vs-read中的讨论。


21
这是一个优秀的例子,说明了优化/性能相关问题应该如何处理。它展示了研究过程,包含了测量数据,并且有明确定义的目标。久而久之,“我不知道速度有多快,但希望更快”会让人感到厌烦。可惜我只能点赞+1 :) - R. Martinho Fernandes
你有没有考虑用固态硬盘替换旋转硬盘?这样可以避免在从一个文件转换到另一个文件时产生的寻道惩罚。 - Jeremy Friesner
分布式计算(Map-Reduce)的重点不就是解决这类问题(使用Hadoop)吗? - Ramadheer Singh
目前还不清楚您在使用mmap()映射整个数据集时遇到了什么问题。只要您编译的是64位可执行文件,mmap()应该可以轻松地映射数百GB的数据(无论多大)。 - caf
3个回答

14

读取回到什么地方?这些数据的最终目的地是什么?

由于听起来你完全受限于IO,mmapread不应该有什么区别。有趣的部分在于如何将数据传输给接收方。

假设你要将这些数据放入一个管道中,我建议你将每个文件的全部内容直接倒入管道中。为了使用零拷贝完成此操作,请尝试使用splice系统调用。您还可以尝试手动复制文件,或fork一个实例的cat或一些其他可以使用当前文件作为stdin并使用管道作为stdout进行大量缓冲的工具。

if (pid = fork()) {
    waitpid(pid, ...);
} else {
    dup2(dest, 1);
    dup2(source, 0);
    execlp("cat", "cat");
}

更新0

如果你的处理程序不依赖于文件类型,并且不需要随机访问,则应使用上面列出的选项创建流水线。您的处理步骤应该可以从标准输入或管道接收数据。

回答您更具体的问题:

A:在Linux上,除了使用 posix_advise 调用和调整磁盘调度程序、虚拟内存管理器和 posix_advise 调用之外,还可以进一步优化 read() 类型的文件 I/O 吗?

就用户空间而言,这已经是最好的方法了。其余的取决于您:缓冲、线程等,但这是危险的,可能是无意义的猜测。我建议只使用将文件分割成管道。

B:有没有系统性的方法来更好地处理非常大的映射数据?

有的。下面的选项可能会带来很好的性能提升(并且可能会在测试过程中使 mmap 使用价值大于 read):

  • MAP_HUGETLB 使用“大页面”分配映射。

    这将减少内核中的分页开销,如果您将映射到千兆字节级别的文件,则效果非常好。

  • MAP_NORESERVE 不要为此映射保留交换空间。当保留交换空间时,可以保证可以修改该映射。当没有保留交换空间时,如果没有可用的物理内存,写入操作可能会导致 SIGSEGV。

    如果您实际上没有足够的物理内存+交换空间来完整地映射,则这可以防止内存不足,同时保持您的实现简单。

  • MAP_POPULATE 填充(预取)映射的页表。对于文件映射,这会在文件上执行 read-ahead 操作。稍后对映射的访问不会被页面故障所阻塞。

    如果有足够的硬件资源,并且预取是有序和延迟的,则可能会加速操作。我怀疑这个标志是多余的,VFS 可能已经默认更好地执行这个操作了。


最终目的地是网络设备,但在那之前必须进行处理和重新排序。 - Bill N.
我认为MAP_HUGETLB不适用于普通文件支持的映射。我认为它只适用于匿名映射。(或者对于在/dev/hugepages中创建的文件,我猜是为了允许进程之间的巨页共享内存。)我认为MAP_NORESERVE对于共享文件支持的映射没有意义。它对于私有映射是有意义的,因为对这些映射的写入不会影响文件内容。但是共享映射已经由磁盘上的文件支持,而不是交换空间。 - Peter Cordes

4

如果您的程序可以预测它想要读取的文件片段,也许使用readahead系统调用可能会有所帮助(但这只是一个猜测,我可能错了)。

我认为您应该调整您的应用程序,甚至是算法,以读取比几千字节更大的数据块。难道不可以是半兆字节吗?


好主意。然而,我可能错了,read() 函数调用 do_generic_read() 函数,据我所知,它会自动执行预读取操作。也许微调预读取窗口或其他一些设置可以帮助提高性能,但很可能这已经在后台完成了。 - gnometorule
并且同意请求更多。这会触发自动预读算法。 - gnometorule

1

这里的问题似乎不在于使用哪个API。无论您使用mmap()还是read(),磁盘仍然必须定位到指定点并读取数据(尽管操作系统有助于优化访问)。

如果您读取非常小的块(几个字节),则mmap()比read()具有优势,因为您不必为每个块调用操作系统,这会变得非常慢。

我也建议像Basile一样连续读取超过2kb,这样磁盘就不必那么频繁地寻道。


2
使用mmap读取未映射的页面将导致陷入操作系统,然后从磁盘读取数据,因此它并不比read()调用更便宜。当然,使用mmap重复读取同一页是很快的。 - janneb
1
True,而且从应用程序的角度来看,填充mmap的读取也是异步的,因此与readahead()调用不同,除非我已经耗尽了缓存页面并且内核处于IO等待状态,否则我的文件线程将不会阻塞。 - Bill N.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接