磁盘是主机中最慢的硬件,往往是性能的瓶颈,优化它能获得立竿见影的效果。
针对磁盘的优化技术有零拷贝、直接IO、 异步IO等
主要目的是为了降低时延、提升操作系统的吞吐量,围绕着内核的磁盘高速缓存(PageCache)去减少CPU和磁盘设备的工作量。
场景
现在假设服务端有数据大小为320MB的文件,要将这个数据通过网络发送给客户端,在内存中分配32KB的缓冲区进行发送。
如何实现文件传输
服务器提供文件传输功能,需要将磁盘上的文件读取出来,通过网络协议这边发送到客户端。320MB的文件传输过程如下:
由于内核缓冲区Page Cache只有32KB,所以需要把文件分成1万份发送,一次32KB发送过程如下:
- 32KB数据的发送需要2次系统调用、4次上下文切换、4次拷贝
- 320MB的数据需要4万次的上下文切换、4万次的拷贝操作、1280MB(320MB*4)的文件拷贝
所以提升性能的关键在于
- 减少内存拷贝次数
- 降低上下文切换频率
零拷贝如何做
降低上下文切换频率
读取磁盘和操作网卡都是由操作系统内核完成,内核和用户进程工作环境完全不同,只要我们执行read、write系统调用,一定会发生2次上下文切换:
- 用户态切换成内核态,内核处理任务
- 内核态处理完任务,切换回用户态,由进程代码执行
减少上下文切换次数方案:把read、write两次系统调用合并成一次,在内核中完成磁盘与网卡的数据交互。
减少内存拷贝次数
每个周期中有4次拷贝,其中两次和物理设备相关的拷贝是必不可少的,所以我们优化的主要方向为用户缓冲区的两次拷贝,本质上着两个都没有必要,如图中拷贝2和拷贝3
如果内核在读取文件后,直接把 PageCache 中的内容拷贝到 Socket 缓冲区,待到网卡发送完毕后,再通知进程,这样就只有 2 次上下文切换,和 3 次内存拷贝。
如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术,还可以再去除 Socket 缓冲区的拷贝,这样一共只有 2 次内存拷贝。
这个就是零拷贝技术。sendfile是操作系统提供的新函数,同时接收文件描述符和TCP Socket作为输入参数,执行时可以完全可以在内核态完成内存拷贝,减少了内存拷贝次数,降低了上下文切换次数。
零拷贝如何利用socket 缓冲区内存
零拷贝取消了用户缓冲区后,在内核态完成数据拷贝
- 降低了用户内存的消耗
- 最大化利用 socket 缓冲区中的内存,再一次减少了系统调用的次数,从而带来了大幅减少上下文切换次数的机会
用户内存为什么分配32KB
- 因为没有零拷贝的情况下,为了使内存利用率更高
- 用户缓冲区过大,无法一次性把数据拷贝到Socket缓冲区
- 用户缓冲区过小,会增加上下文切换和拷贝次数
- Socket缓冲区的可用空间大小是动态变化的,所以用户缓存区无法Socket缓冲区设置成一致,受到TCP滑动窗口、应用缓冲区、整个系统的影响。
所以综合权衡下最终选了32KB。
零拷贝使我们不用关心Socket缓冲区的大小,调用零拷贝发送方法时,尽可能把文件字节数设置为未发送的自己数,比如320MB,会根据Socket缓冲区大小发送数据,假如Socket缓冲区大小为1.4MB,那么就会发送1.4MB,会比32KB,提升很多效率
综上分析,零拷贝通过降低上下文切换、内存拷贝把性能提高至少一倍以上。
PageCache 磁盘高速缓存
零拷贝还使用了PageCache技术,可以通过它进一步提升性能
上面几张图中,读取文件都是先把文件拷贝到PageCache上,再拷贝到进程中,这么做的原因如下:
- 磁盘读写比内存慢很多,所以尽量把对磁盘的读写改成对内存的读写,但内存大小有限,只能选择性的复制一部分
- 时间局部性原理:刚被访问的数据端时间内再次访问的概率很高,缓存最近访问的数据,空间不足用LRU算法淘汰。
- 机械磁盘的旋转磁头寻址过程很慢,所以通过 PageCache进行预读
例如 read 方法只读取了 0-32KB 的字节,但内核会把其后的 32-64KB 也读取到 PageCache,后32KB 读取的成本很低。如果在 32-64KB 淘汰出 PageCache 前,进程读取到它了,收益就非常大
大文件零拷贝下的问题
- 大文件会很快把PageCache占满,热点小文件就无法充分利用PageCache,所以读起来就慢了
- PageCache中的大文件因为太大,某部分内容再次被访问到的概率很低,没有享受到缓存的好处,但却耗费CPU多拷贝到PageCache一次
所以大文件不应该使用PageCache,也不应该使用零拷贝技术处理。
异步IO+直接IO
高并发场景处理大文件用异步IO和直接IO来替换零拷贝技术。
调用Read方法读取文件时,阻塞等待过程如下
异步IO可以解决阻塞问题,操作分为两部分:
- 向内核发起读请求,不等待数据就位就立即返回,进程可以并发处理其他任务
- 内核将磁盘的数据拷贝到进程缓冲区之后,进程收到内核的通知,去处理数据
异步IO没有拷贝到PageCache中,是异步IO实现上的缺陷
经过PageCache的IO叫做缓存IO,与虚拟内存系统耦合太紧,导致异步IO不支持缓存IO
绕过PageCache的IO是直接IO,异步IO只支持直接IO。
直接IO的应用场景:
- 应用程序已经实现了磁盘文件的缓存,不需要PageCache再次缓存,引发额外性能消耗。比如MySQL数据使用直接IO
- 高并发下传输大文件,难以命中PageCache缓存,又带来额外的内存拷贝,挤占了小文件使用的PageCache的内存,所以使用直接IO
直接IO的缺点:不能享受以下性能提升
-
内核会试图缓存尽量多的连续IO在PageCache中,最后合并成一个更大的IO发送给磁盘,减少磁盘的寻址操作
-
内核也会预读后续的IO放在PageCache中,减少磁盘操作
-
有了直接IO之后,异步IO就可以无阻塞的读取文件了,大文件由异步IO和直接IO处理
-
小文件交给零拷贝处理
大小文件的判断,可以灵活配置:Nginx 的 directio 指令
总结
基于用户缓冲区传输文件时,过多的内存拷贝与上下文切换次数会降低性能。
零拷贝技术在内核中完成内存拷贝,天然降低了内存拷贝次数。
通过一次系统调用合并了磁盘读取与网 络发送两个操作,降低了上下文切换次数。
由于拷贝在内核中完成,它可以最大化使用 socket 缓冲区的可用空间,从而提高了一次系统调用中处理的数据量,进一步降低了 上下文切换次数。
零拷贝技术基于 PageCache,而 PageCache 缓存了最近访问过的数据,提升了访问缓存 数据的性能,同时,为了解决机械磁盘寻址慢的问题,它还协助 IO 调度算法实现了 IO 合 并与预读(这也是顺序读比随机读性能好的原因),这进一步提升了零拷贝的性能。
几乎所有操作系统都支持零拷贝,如果应用场景就是把文件发送到网络中,那么我们应当选择使用 了零拷贝的解决方案。
不过,零拷贝有一个缺点,就是不允许进程对文件内容作一些加工再发送,比如数据压缩后再发送。另外,当 PageCache 引发负作用时,也不能使用零拷贝,此时可以用异步 IO+ 直接 IO 替换。我们通常会设定一个文件大小阈值,针对大文件使用异步 IO 和直接 IO,而 对小文件使用零拷贝。
事实上 PageCache 对写操作也有很大的性能提升,因为 write 方法在写入内存中的 PageCache 后就会返回,速度非常快,由内核负责异步地把 PageCache 刷新到磁盘中。
磁盘 IO 优化技术三个方向
- 要么减少了磁盘的工作量(比如 PageCache 缓存)
- 要么减少了 CPU 的工作量 (比如直接 IO)
- 要么提高了内存的利用率(比如零拷贝)