零拷贝并非万能解决方案:重新定义数据传输的效率极限

/ PageCache 有什么作用? /在我们前面讲解零拷贝的内容时,我们了解到一个重要的概念,即内核缓冲区 。那么,你可能会好奇内核缓冲区到底是什么?这个专有名词就是 PageCache,也被称为磁盘高速缓存 。也可以看下 windows 下的缓存区:如图所示:

零拷贝并非万能解决方案:重新定义数据传输的效率极限

文章插图
图片
零拷贝进一步提升性能的原因在于 PageCache 技术的使用 。接下来,我们将详细探讨 PageCache 技术是如何实现这一目标的 。
读写磁盘相比读写内存的速度慢太多了,但我们可以采取一种方法来改善这个问题,即将磁盘数据部分缓存到内核中,也就是将其存储在 PageCache 缓存区中 。这个过程实际上是通过 DMA(直接内存访问)控制器将磁盘数据拷贝到内核缓冲区中 。
然而,需要注意的是,由于内存空间较磁盘空间有限,因此存在一系列算法来确保 pageCache 占用的内存空间不过大 。我们在程序运行时都知道存在一种「局部性」,即刚刚被访问的数据在短时间内很可能再次被访问到,概率很高 。因此,pageCache 被用作缓存最近访问的数据 。可以将 pageCache 看作是 redis,而磁盘则类似于 MySQL 。此外,pageCache 还使用了内存淘汰机制,在内存空间不足时,会淘汰最近最久未被访问的缓存 。
当在项目中使用 Redis 时,你一定知道如何使用它 。和 Redis 类似, PageCache 的工作原理也是一样的 。在进程需要访问数据时,它会首先检查 PageCache 是否已经存储了所需的数据 。如果数据已经存在于 PageCache 中,内核会直接返回数据;如果数据未被缓存,则会从磁盘读取并将数据缓存到 PageCache 中,以备下次查询时使用 。这种方式可以有效提高访问效率 。
然而,pageCache 还具有另一个优点,即预读功能 。当访问并读取磁盘数据时,实际上需要定位磁盘中的位置 。对于机械硬盘而言,这意味着磁头必须旋转到数据所在的扇区位置,然后开始顺序读取数据 。然而,旋转磁头这种物理操作对计算机而言非常耗时 。为了降低其影响,就出现了预读功能 。通过预读功能,可以提前预读下一扇区的数据,减少等待磁头旋转的时间 。
比如 read 方法需要读取 32KB 的字节的数据,使其在读取 32KB 字节数据后,继续读取后面的 32-64KB,并将这一块数据一起缓存到 pageCache 缓冲区 。这样做的好处在于,如果后续读取需要的数据在这块缓存中命中,那么读取成本会大幅降低 。可以类比于 redis 中提前缓存一部分分布式唯一 id 用于插入数据库时的分配操作,这样就无需每次插入前都去获取一遍 id 。然而,一般情况下,为了避免可能出现的"毛刺"现象,我们通常会使用双缓存机制来处理 。这个双缓存机制可以进一步优化读取操作的效果 。
【零拷贝并非万能解决方案:重新定义数据传输的效率极限】因此,PageCache 的优点主要包括两个方面:首先,它能够将数据缓存到 PageCache 中;其次,它还利用了数据的预读功能 。这两个操作极大地增强了读写磁盘时的性能 。
但是,你可以想象一下如果你在传输大文件时比如好几个 G 的文件,如果还是使用零拷贝技术,内核还是会把他们放入 pageCache 缓存区,那这样不就产生问题了吗?你也可以想一下如果你往 redis 缓存中放了一个还几个 G 大小的 value,而且还知道缓存了也没用,那不就相当于 redis 形同虚设了吗?把其他热点数据也弄没了,所以 pageCache 也有这样的一个问题,一是大文件抢占了 pageCache 的内存大小,这样做会导致其他热点数据无法存储在 pageCache 缓冲区中,从而降低磁盘的读写性能 。此外,由于 pageCache 无法享受到缓存的好处,还会产生一个 DMA 数据拷贝的过程 。
因此,最佳的优化方法是针对大文件传输时不使用 pageCache,也就是不使用零拷贝技术 。这是因为零拷贝技术会占用大量的内存空间,影响其他热点数据的访问优化 。在高并发环境下,这几乎肯定会导致严重的性能问题 。
/ 大文件传输用什么方式实现? /那针对大文件的传输,我们应该使用什么方式呢?
让我们首先来观察最初的示例 。当调用 read 方法读取文件时,进程实际上会被阻塞在 read 方法的调用处,因为它需要等待磁盘数据的返回 。如下图所示:
零拷贝并非万能解决方案:重新定义数据传输的效率极限

文章插图
图片
在没有使用零拷贝技术的情况下,我们的用户进程使用同步 IO 的方式,它会一直阻塞等待系统调用返回数据 。让我们回顾一下之前的具体流程:


推荐阅读