接上文解析从Linux零拷贝深入了解Linux-I/O(上)
大文件传输场景
零拷贝还是最优选吗
在大文件传输的场景下,零拷贝技术并不是最优选择;因为在零拷贝的任何一种实现中,都会有「DMA 将数据从磁盘拷贝到内核缓存区——Page Cache」这一步,但是,在传输大文件(GB 级别的文件)的时候,PageCache 会不起作用,那就白白浪费 DMA 多做的一次数据拷贝,造成性能的降低,即使使用了 PageCache 的零拷贝也会损失性能。
这是因为在大文件传输场景下,每当用户访问这些大文件的时候,内核就会把它们载入 PageCache 中,PageCache 空间很快被这些大文件占满;且由于文件太大,可能某些部分的文件数据被再次访问的概率比较低,这样就会带来 2 个问题:
-
PageCache 由于长时间被大文件占据,其他「热点」的小文件可能就无法充分使用到 PageCache,于是这样磁盘读写的性能就会下降了;
-
PageCache 中的大文件数据,由于没有享受到缓存带来的好处,但却耗费 DMA 多拷贝到 PageCache 一次。
异步 I/O +direct I/O
那么大文件传输场景下我们该选择什么方案呢?让我们先来回顾一下我们在文章开头介绍 DMA 时最早提到过的同步 I/O:
这里的同步 体现在当进程调用 read 方法读取文件时,进程实际上会阻塞在 read 方法调用,因为要等待磁盘数据的返回,并且我们当然不希望进程在读取大文件时被阻塞,对于阻塞的问题,可以用异步 I/O 来解决,即:
它把读操作分为两部分:
-
前半部分,内核向磁盘发起读请求,但是可以不等待数据就位就返回 ,于是进程此时可以处理其他任务;
-
后半部分,当内核将磁盘中的数据拷贝到进程缓冲区后,进程将接收到内核的通知 ,再去处理数据;
而且,我们可以发现,异步 I/O 并没有涉及到 PageCache;使用异步 I/O 就意味着要绕开 PageCache,因为填充 PageCache 的过程在内核中必须阻塞。
所以异步 I/O 中使用的是direct I/O(对比使用 PageCache 的buffer I/O),这样才能不阻塞进程,立即返回。
direct I/O 应用场景常见的两种:
-
应用程序已经实现了磁盘数据的缓存,那么可以不需要 PageCache 再次缓存,减少额外的性能损耗。在 MySQL 数据库中,可以通过参数设置开启direct I/O,默认是不开启;
-
传输大文件的时候,由于大文件难以命中 PageCache 缓存,而且会占满 PageCache 导致「热点」文件无法充分利用缓存,从而增大了性能开销,因此,这时应该使用`direct I/O;;
当然,由于direct I/O 绕过了 PageCache,就无法享受内核的这两点的优化:
-
内核的 I/O 调度算法会缓存尽可能多的 I/O 请求在 PageCache 中,最后「合并 」成一个更大的 I/O 请求再发给磁盘,这样做是为了减少磁盘的寻址操作;
-
内核也会「预读 」后续的 I/O 请求放在 PageCache 中,一样是为了减少对磁盘的操作;
实际应用中也有类似的配置,在 nginx 中,我们可以用如下配置,来根据文件的大小来使用不同的方式传输:
location /video/ { sendfile on; aio on; directio 1024m; }
当文件大小大于 direc