1 零拷贝:如何高效地传输文件
磁盘是主机中最慢的硬件之一,常常是性能瓶颈,所以优化它能获得立竿见影的效果。
围绕着内核中的磁盘高速缓存(PageCache),去减少CPU和磁盘设备的工作量。
1.1 你会如何实现文件传输
服务器提供文件传输功能,需要将磁盘文件读取出来,通过网络协议发送到客户端。
通过,最直接的办法;从网络请求中找出文件在磁盘的路径后,如果这个文件比较大,如320MB,可以在内存中分配32KB的缓冲区,再把文件分成1万份,每份只有32KB,这样,从文件的起始位置读入32KB到缓冲区。在通过网络API把这32KB发送到客户端,重复一万次。
这个方案性能并不好,主要原因是:
-
首先,它至少经历4万次用户态和内核态的上下文切换。上下文切换的成本并不小,虽然一切缓存只有几十纳秒,但是架不住频率高。
-
其次,这个方案作了四万次的内存拷贝,对320mb文件拷贝的字节数也翻了4倍,到了1280MB。很显然,过多的内存拷贝消耗了cpu资源,降低了系统的吞吐率。
所以想提升传输文件的性能,需要从降低上下文切换频率和内存拷贝次数两个方向入手
1.2 零拷贝如何提升文件的传输性能
首先,我们来看如何降低上下文切换的频率
为什么读取磁盘文件,一定要做上下文切换呢?这是因为,读取磁盘或者操作网卡都是由操作系统内核完成的。内核负责管理系统的所有进程,它的权限最高。只要我们的代码执行read或者write这样的系统调用,一定会发生两次上下文切换:首先从用户态切换到内核态,当内核执行完任务后,再切换回用户态交由进程代码执行。
因此,如果想减少上下文切换次数,就一定要减少系统调用的次数。解决方案就是把read、write两次系统调用合并成一次,在内核中完成磁盘和网卡的数据交换。
如果内核在读取文件后,直接PageCache中的内容拷贝到Socket缓冲区,待到网卡发送完毕后,再通知进程,这样就只有2次上下文切换,和三次内存拷贝。
如果网卡支持SG-DMA,还可以直接去除Socket缓冲区的拷贝,这样直接只有2ci内存拷贝。
实际上,这就是零拷贝技术
它是os提供的新函数,同时接受文件描述符合tcp socket作为输入参数,这样执行时就可以完全在内核态完成内存拷贝,即减少了内存拷贝次数,也降低了上下文切换的次数。
没有zero-copy时,在用户缓冲区分配了32kb的内存,把文件分成1万份发送,然后这32kb是怎么来的?为什么不是32mb护着32byte呢?
为什么用户缓冲区不与socket缓冲区大小一致呢?那是因为,socket缓冲区的可用空间是动态变化的,它既用于tcp滑动窗口,也用于应用缓冲区,还受到整个系统内存的影响。
零拷贝使我们不必关系socket缓冲取的大小。比如,调用zero-copy发送方法时,尽可以把发送字节数设为文件未发送字节数。
综合上述优点,zero-copy可以把性能提升至少一倍以上。此外,它还是用了PageCache技术。
1.3 PageCache,磁盘高速缓存
读取文件时,是先把磁盘文件拷贝到PageCache上,再拷贝到进程中。为什么这么做?
第一,磁盘速率比内存慢很多,所以我们应该想办法把读写磁盘替换成读写内存,比如把磁盘的数据复制到内存汇总。但是内存的空间远比磁盘要小,内存中注定只能复制一小部分磁盘的数据。
选择哪些数据复制到内存?通过,根据时间局部性原理缓存最近访问的数据,当空间不足时淘汰最近的未被访问的缓存(即LRU算法)
第二,读取磁盘数据时,需要先找到数据所在的位置,PageCache使用了预读功能。 也就是说,虽然 read 方法只读取了 0-32KB 的字节,但内核会把其后的 32-64KB 也读取到 PageCache,这后 32KB 读取的成本很低。如果在 32-64KB 淘汰出 PageCache 前,进程读取到它了,收益就非常大。这一讲的传输文件场景中这是必然发生的。
从这两点看到PageCache的优点,它在90%的场景下都会提升磁盘性能,但在某些情况下,PageCache会不起作用,甚至由于多做了一次内存拷贝,造成了性能的降低。在这些场景中,使用了PageCache会不起作用,甚至由于多做了一次内存拷贝,造成了性能的降低。
所以,高并发场景下,为了防止PageCache被大文件占满后不再对小文件产生作用。大文件不应使用PageCache,进而也不应使用零拷贝技术处理。
1.4 异步IO+直接IO
高并发场景处理大文件时,应该使用异步IO和直接IO来替换零拷贝技术
当调用read方法读取文件时,实际上read方法会在磁盘寻址过程中阻塞等待,导致进程无法并发地处理其他任务.
异步IO可以解决阻塞的问题。它把对操作分为两部分,前半部分发起读请求,但不等待数据就位就立刻返回。此时进程可以并发地处理其他任务,当内核将磁盘数据拷贝到进程缓冲区后,进程将接收到内核的通知,再去处理数据。
从图中可以看到,异步 IO 并没有拷贝到 PageCache 中,这其实是异步 IO 实现上的缺陷。经过 PageCache 的 IO 我们称为缓存 IO,它与虚拟内存系统耦合太紧,导致异步 IO 从诞生起到现在都不支持缓存 IO。
有了直接 IO 后,异步 IO 就可以无阻塞地读取文件了。现在,大文件由异步 IO 和直接 IO 处理,小文件则交由零拷贝处理,至于判断文件大小的阈值可以灵活配置(参见 Nginx 的 directio 指令)
1.5 小结
基于用户缓冲区传输文件时,过多的内存拷贝与上下文切换次数会降低性能。零拷贝技术在内核中完成内存拷贝,天然降低了内存拷贝次数。
zero-copy技术基于PageCache,而PageCache缓存了最近访问过的数据,提升了访问缓存数据的性能,同时解决了机械磁盘寻址慢的问题。几乎所有操作系统都支持零拷贝,如果应用场景就是把文件发送到网络中,那么我们应当选择使用零拷贝的解决方案。
零拷贝的缺点是,不允许进程对文件内容进行加工再发送。当PageCache引发负作用时,也不能使用零拷贝,可以用异步IO+直接IO替换。我们通常会设定一个文件大小阈值,针对大文件使用异步 IO 和直接 IO,而对小文件使用零拷贝。