一般会如何实现文件传输?
服务器提供文件传输功能,需要将磁盘上的文件读取出来,通过网络协议发送到客户端。如果需要你自己编码实现这个文件传输功能,你会怎么实现呢?
通常,你会选择最直接的方法:从网络请求中找出文件在磁盘中的路径后,如果这个文件比较大,假设有 320mb,可以在内存中分配 32kb 的缓冲区,再把文件分成一万份,每份只有 32kb,这样,从文件的起始位置读入 32kb 到缓冲区,再通过网络 api 把这 32kb 发送到客户端。接着重复一万次,直到把完整的文件都发送完毕。如下图所示:
不过这个方案性能并不好,主要有两个原因。
上下文切换:
首先,它至少经历了 4 万次用户态与内核态的上下文切换。因为每处理 32kb 的消息,就需要一次 read 调用和一次 write 调用,每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。可见,每处理 32kb,就有 4 次上下文切换,重复 1 万次后就有 4 万次切换。
上下文切换的成本并不小,虽然一次切换仅消耗几十纳秒到几微秒,但高并发服务会放大这类时间的消耗。
内存拷贝:
其次,这个方案做了 4 万次内存拷贝,对 320mb 文件拷贝的字节数也翻了 4 倍,到了 1280mb。很显然,过多的内存拷贝无谓地消耗了 cpu 资源,降低了系统的并发处理能力。
所以要想提升传输文件的性能,需要从降低上下文切换的频率和内存拷贝次数两个方向入手。
零拷贝如何提升文件传输性能?
首先,我们来看如何降低上下文切换的频率。
为什么读取磁盘文件时,一定要做上下文切换呢?这是因为,读取磁盘或者操作网卡都由操作系统内核完成。内核负责管理系统上的所有进程,它的权限最高,工作环境与用户进程完全不同。只要我们的代码执行 read 或者 write 这样的系统调用,一定会发生 2 次上下文切换:首先从用户态切换到内核态,当内核执行完任务后,再切换回用户态交由进程代码执行。
因此,如果想减少上下文切换次数,就一定要减少系统调用的次数。解决方案就是把 read、write 两次系统调用合并成一次,在内核中完成磁盘与网卡的数据交换。
其次,我们应该考虑如何减少内存拷贝次数。
每周期中的 4 次内存拷贝,其中与物理设备相关的 2 次拷贝是必不可少的,包括:把磁盘内容拷贝到内存,以及把内存拷贝到网卡。但另外 2 次与用户缓冲区相关的拷贝动作都不是必需的,因为在把磁盘文件发到网络的场景中,用户缓冲区没有必须存在的理由。
如果内核在读取文件后,直接把 pagecache 中的内容拷贝到 socket 缓冲区,待到网卡发送完毕后,再通知进程,这样就只有 2 次上下文切换,和 3 次内存拷贝。
如果网卡支持 sg-dma(the scatter-gather direct memory access)技术,还可以再去除 socket 缓冲区的拷贝,这样一共只有 2 次内存拷贝。
实际上,这就是零拷贝技术。
它是操作系统提供的新函数,同时接收文件描述符和 tcp socket 作为输入参数,这样执行时就可以不需要用户层缓存,完全在内核态完成内存拷贝,既减少了内存拷贝次数,也降低了上下文切换次数。
而且,零拷贝取消了用户缓冲区后,不只降低了用户内存的消耗,还通过最大化利用 socket 缓冲区中的内存,间接地再一次减少了系统调用的次数,从而带来了大幅减少上下文切换次数的机会!
你可以回忆下,没用零拷贝时,为了传输 320mb 的文件,在用户缓冲区分配了 32kb 的内存,把文件分成 1 万份传送,然而,这 32kb 是怎么来的?为什么不是 32mb 或者 32 字节呢?这是因为,在没有零拷贝的情况下,我们希望内存的利用率最高。如果用户缓冲区过大,它就无法一次性把消息全拷贝给 socket 缓冲区;如果用户缓冲区过小,则会导致过多的 read/write 系统调用。
那用户缓冲区为什么不与 socket 缓冲区大小一致呢?这是因为,socket 缓冲区的可用空间是动态变化的,它既用于 tcp 滑动窗口,也用于应用缓冲区,还受到整个系统内存的影响。尤其在长肥网络中,它的变化范围特别大。
零拷贝使我们不必关心 socket 缓冲区的大小。比如,调用零拷贝发送方法时,尽可以把发送字节数设为文件的所有未发送字节数,例如 320mb,也许此时 socket 缓冲区大小为 1.4mb,那么一次性就会发送 1.4mb 到客户端,而不是只有 32kb。这意味着对于 1.4mb 的 1 次零拷贝,仅带来 2 次上下文切换,而不使用零拷贝且用户缓冲区为 32kb 时,经历了 176 次(4 * 1.4mb/32kb)上下文切换。
综合上述各种优点,零拷贝可以把性能提升至少一倍以上!对文章开头提到的 320mb 文件的传输,当 socket 缓冲区在 1.4mb 左右时,只需要 4 百多次上下文切换,以及 4 百多次内存拷贝,拷贝的数据量也仅有 640mb,这样,不只请求时延会降低,处理每个请求消耗的 cpu 资源也会更少,从而支持更多的并发请求。
此外,零拷贝还使用了 pagecache 技术,通过它,零拷贝可以进一步提升性能,我们接下来看看 pagecache 是如何做到这一点的。
pagecache,磁盘高速缓存
回顾上文中的几张图,你会发现,读取文件时,是先把磁盘文件拷贝到 pagecache 上,再拷贝到进程中。为什么这样做呢?有两个原因所致。
第一,由于磁盘比内存的速度慢许多,所以我们应该想办法把读写磁盘替换成读写内存,比如把磁盘中的数据复制到内存中,就可以用读内存替换读磁盘。但是,内存空间远比磁盘要小,内存中注定只能复制一小部分磁盘中的数据。
选择哪些数据复制到内存呢?通常,刚被访问的数据在短时间内再次被访问的概率很高(这也叫“时间局部性”原理),用 pagecache 缓存最近访问的数据,当空间不足时淘汰最久未被访问的缓存(即 lru 算法)。读磁盘时优先到 pagecache 中找一找,如果数据存在便直接返回,这便大大提升了读磁盘的性能。
第二,读取磁盘数据时,需要先找到数据所在的位置,对于机械磁盘来说,就是旋转磁头到数据所在的扇区,再开始顺序读取数据。其中,旋转磁头耗时很长,为了降低它的影响,pagecache 使用了预读功能。
也就是说,虽然 read 方法只读取了 0-32kb 的字节,但内核会把其后的 32-64kb 也读取到 pagecache,这后 32kb 读取的成本很低。如果在 32-64kb 淘汰出 pagecache 前,进程读取到它了,收益就非常大。这一讲的传输文件场景中这是必然发生的。
从这两点可以看到 pagecache 的优点,它在 90% 以上场景下都会提升磁盘性能,但在某些情况下,pagecache 会不起作用,甚至由于多做了一次内存拷贝,造成性能的降低。在这些场景中,使用了 pagecache 的零拷贝也会损失性能。
具体是什么场景呢?就是在传输大文件的时候。比如,你有很多 gb 级的文件需要传输,每当用户访问这些大文件时,内核就会把它们载入到 pagecache 中,这些大文件很快会把有限的 pagecache 占满。
然而,由于文件太大,文件中某一部分内容被再次访问到的概率其实非常低。这带来了 2 个问题:首先,由于 pagecache 长期被大文件占据,热点小文件就无法充分使用 pagecache,它们读起来变慢了;其次,pagecache 中的大文件没有享受到缓存的好处,但却耗费 cpu 多拷贝到 pagecache 一次。
所以,高并发场景下,为了防止 pagecache 被大文件占满后不再对小文件产生作用,大文件不应使用 pagecache,进而也不应使用零拷贝技术处理。
异步 io + 直接 io
高并发场景处理大文件时,应当使用异步 io 和直接 io 来替换零拷贝技术。
仍然回到本讲开头的例子,当调用 read 方法读取文件时,实际上 read 方法会在磁盘寻址过程中阻塞等待,导致进程无法并发地处理其他任务,如下图所示:
异步 io(异步 io 既可以处理网络 io,也可以处理磁盘 io,这里我们只关注磁盘 io)可以解决阻塞问题。它把读操作分为两部分,前半部分向内核发起读请求,但不等待数据就位就立刻返回,此时进程可以并发地处理其他任务。当内核将磁盘中的数据拷贝到进程缓冲区后,进程将接收到内核的通知,再去处理数据,这是异步 io 的后半部分。如下图所示:
从图中可以看到,异步 io 并没有拷贝到 pagecache 中,这其实是异步 io 实现上的缺陷。经过 pagecache 的 io 我们称为缓存 io,它与虚拟内存系统耦合太紧,导致异步 io 从诞生起到现在都不支持缓存 io。
绕过 pagecache 的 io 是个新物种,我们把它称为直接 io。对于磁盘,异步 io 只支持直接 io。
直接 io 的应用场景并不多,主要有两种:第一,应用程序已经实现了磁盘文件的缓存,不需要 pagecache 再次缓存,引发额外的性能消耗。比如 mysql 等数据库就使用直接 io;第二,高并发下传输大文件,我们上文提到过,大文件难以命中 pagecache 缓存,又带来额外的内存拷贝,同时还挤占了小文件使用 pagecache 时需要的内存,因此,这时应该使用直接 io。
当然,直接 io 也有一定的缺点。除了缓存外,内核(io 调度算法)会试图缓存尽量多的连续 io 在 pagecache 中,最后合并成一个更大的 io 再发给磁盘,这样可以减少磁盘的寻址操作;另外,内核也会预读后续的 io 放在 pagecache 中,减少磁盘操作。直接 io 绕过了 pagecache,所以无法享受这些性能提升。
有了直接 io 后,异步 io 就可以无阻塞地读取文件了。现在,大文件由异步 io 和直接 io 处理,小文件则交由零拷贝处理,至于判断文件大小的阈值可以灵活配置(参见 nginx 的 directio 指令)。
工业4.0需求推动光学传感器市场发展,2026年销售额将达360亿美元
车规级磁环电感生产厂家科普磁环电感选型要点
什么是UWB芯片?UWB芯片市场如何?
三极管pnp和npn的区别
数字温度计的重要设计考虑因素
如何高效实现文件传输
传iPhone8将取消Lightning接口 改用无线充电
LTC3105升压型DC/DC转换器简化能量收集
宁夏大唐国际青铜峡沙石墩梁风电场扩建项目首台机组并网发电
浅谈连接器的耐压检测
谷歌新研究使用1024块TPU,将BERT的训练时间从3天成功缩短到76分钟
射频技术大不同|RFID技术及其在电子政务中的应用
MAX15041 内置开关的同步DC-DC转换器
华为Mate 40于今日正式全网开售
智慧环卫通信系统的工作原理及功能分析
模拟万用表交流电压测量能力的内部电路及工作原理
美的智能冰箱, 带你领略生活原鲜之美
微星MEG X570 UNIFY主板高清图集
宁德时代拟投资不超过人民币290亿元,用于新建扩建产能
华为鸿蒙OS正小规模测试