系统性能调优必知必会
陶辉
智链达CTO、前阿里云高级技术专家
立即订阅
4101 人已学习
课程目录
已更新 6 讲 / 共 34 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 万变不离其宗,性能优化也有章可循
免费
基础设施优化 (5讲)
01 | CPU缓存:怎样写代码能够让CPU执行得更快?
02 | 内存池:如何提升内存分配的效率?
03 | 索引:如何用哈希表管理亿级对象?
04 | 零拷贝:如何高效地传输文件?
05 | 协程:如何快速地实现高并发服务?
系统性能调优必知必会
15
15
1.0x
00:00/00:00
登录|注册

04 | 零拷贝:如何高效地传输文件?

陶辉 2020-05-06
你好,我是陶辉。
上一讲我们谈到,当索引的大小超过内存时,就会用磁盘存放索引。磁盘的读写速度远慢于内存,所以才针对磁盘设计了减少读写次数的 B 树索引。
磁盘是主机中最慢的硬件之一,常常是性能瓶颈,所以优化它能获得立竿见影的效果。
因此,针对磁盘的优化技术层出不穷,比如零拷贝、直接 IO、异步 IO 等等。这些优化技术为了降低操作时延、提升系统的吞吐量,围绕着内核中的磁盘高速缓存(也叫 PageCache),去减少 CPU 和磁盘设备的工作量。
这些磁盘优化技术和策略虽然很有效,但是理解它们并不容易。只有搞懂内核操作磁盘的流程,灵活正确地使用,才能有效地优化磁盘性能。
这一讲,我们就通过解决“如何高效地传输文件”这个问题,来分析下磁盘是如何工作的,并且通过优化传输文件的性能,带你学习现在热门的零拷贝、异步 IO 与直接 IO 这些磁盘优化技术。

你会如何实现文件传输?

服务器提供文件传输功能,需要将磁盘上的文件读取出来,通过网络协议发送到客户端。如果需要你自己编码实现这个文件传输功能,你会怎么实现呢?
通常,你会选择最直接的方法:从网络请求中找出文件在磁盘中的路径后,如果这个文件比较大,假设有 320MB,可以在内存中分配 32KB 的缓冲区,再把文件分成一万份,每份只有 32KB,这样,从文件的起始位置读入 32KB 到缓冲区,再通过网络 API 把这 32KB 发送到客户端。接着重复一万次,直到把完整的文件都发送完毕。如下图所示:
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《系统性能调优必知必会》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(16)

  • 忆水寒
    我觉得有两种情况:
    1、如果这里的异步IO指的是传统的异步阻塞IO,比如select,epoll等。这种情况下是可能阻塞进程的,在内核通知用户进程数据准备好了以后,用户进程发起read调用,此时内核拷贝数据期间,进程实际上是阻塞的。
    解决方案:可以在内核完成拷贝后再通知用户进程,或者使用mmap方式,用户态和内核实际上是同一块内存就不存在拷贝了。
    2、如果这里的异步IO指的是Linux内核2.6版本之后的AIO,那么不太可能阻塞进程,除非系统内核之后一直调度不到该进程(比如其他任务不断的抢占式或绑定内核运行)。
    解决方案:可以使用绑定内核的运行机制阻值其他高优先级进程或中断进行抢占。
    只能想到这么多了,希望看看其他大佬的答案。
    2020-05-06
    2
    8
  • 每天晒白牙
    今日得到
    服务器提供文件传输功能,首先从磁盘读取文件,然后通过网络协议发送给客户端。最直接的办法是根据客户端的请求从磁盘上找到文件位置,然后从磁盘把部分文件(一般文件比较大时,需要对文件进行切分)读入到缓冲区,然后再通过网络把数据发送给客户端。
    方法的不足
    1.上下文切换
    该方案在一次收发过程中涉及到 4 次用户态和内核态的上下文切换,没处理缓冲区大小的数据需要一次 read 调用和一次 write 调用,每次调用都需要从用户态切换到内核态,然后等内核态完成任务后,再切回用户态。因为文件比较大,读取次数比较多,所以上下文切换的成本不容小觑

    2.多次内存拷贝
    磁盘 -> PageCache
    PageCache -> 用户缓冲区
    用户缓冲区 -> Socket 缓冲区
    Socket 缓冲区 -> 网卡
    因为涉及到多次内存拷贝,消耗过多的 CPU 资源,降低系统并发处理能力
    想要优化传输文件的性能,需要从降低上下文切换的频率和内存拷贝次数入手

    零拷贝
    降低上下文切换频率
    读取磁盘文件的上下文切换是一定会做的,因为读取磁盘和操作网卡都是由操作系统内核完成。所以我们在执行 read 或 write 这种系统调用时,一定会经过 2 次上下文切换:先从用户态切换到内核态,当内核态任务完成后,再切换回用户态交由进程代码执行
    所以,要想降低上下文切换频率的要点就是减少系统调用的次数。解决办法是把 read 和 write 两次系统调用合并为一次(可以通过 sendfile 一次系统调用完成),在内核态中完成磁盘与网卡的数据交换操作

    减少内存拷贝次数
    一次收发过程中有两次与物理设备相关的内存拷贝是必不可少的:把磁盘的数据拷贝到内存;把内存的数据拷贝到网卡。而与用户缓冲区相关的内存拷贝不是必须的

    综上所述,可以在内核读取文件后,直接把 PageCache 中的数据拷贝到 socket 缓冲区中,这样就只有 2 次上下文切换 和 3 次内存拷贝。如果网卡支持 SG-DMA 技术,还可以把拷贝到 socket 缓冲区的步骤给省略掉

    PageCache 磁盘高速缓存
    根据时间局部性原理(刚被访问到的数据在短时间被再次访问的概率高),通常将最近访问的数据放到 PageCache 中,当空间不足时通过 LRU 算法或变种算法淘汰最久未被访问的数据
    PageCache 还提供预读功能

    但 PageChache 不适应传输大文件的场景,大文件容易把 PageCache 占满,而且由于文件太大,文件中某一个部分被再次访问的概率低。这样会导致大文件在 PageCache 中没有享受到缓存的优势,同时也因为 PageCache 被大文件占据,影响其他热点小文件的缓存

    异步 IO 可以把读操作分为两部分,前半部分向内核发起读请求,但不用等待数据就位就返回,然后可以继续处理其他任务。当内核把磁盘中的数据拷贝到进程缓冲区后,会通知进程去处理数据。异步 IO 是不会阻塞用户进程的
    对于磁盘,异步 IO 只支持直接 IO

    直接 IO
    直接 IO 是应用程序绕过 PageCache,即不经过内核缓冲区,直接访问磁盘中的数据,从而减少了内核缓存与用户程序之间的数据拷贝
    应用场景
    1.应用程序已经自己实现了磁盘文件的缓存,不需要 PageCache 再次进行缓存,引发额外的性能消耗
    2.高并发下传输大文件,因为大文件难以命中 PageCache 缓存,又会影响其他热点小文件的缓存

    直接 IO 的不足
    因为直接 IO 不适用 PageCache 缓存,所以享受不到内核针对 PageCache 做的一些优化,比如内核会试图缓存更多的连续 IO 在 PageCache 中,然后合并成一个更大的 IO 后发给磁盘,可以减少磁盘的寻址操作;另外,内核还会进行数据的预读,把数据缓存到 PageCache 中,较少磁盘操作

    方法论
    1.大文件交给异步 IO 和直接 IO 处理,小文件交给零拷贝处理
    文件传输的性能优化思路
    1.减少磁盘的工作量(PageCache 技术)
    2.提高内存的利用率(零拷贝)
    3.较少 CPU 的工作量(直接 IO)

    案例
    Kafka 的高性能的原因就包括使用了零拷贝技术和 PageCache 缓存
    2020-05-07
    2
  • Bitstream
    到现在一共更了5讲,除了开篇,每讲都是干货。不是因为不知道老师讲的知识点,而是您讲的很系统,以场景带理论,学习起来很高效。

    作者回复: 你好Bitstream,谢谢你的反馈,我会继续按照这个思路写后续的几讲

    2020-05-06
    2
  • test
    有异步阻塞IO,取决于进程在IO读取点时候在做什么
    2020-05-06
    2
  • 每天晒白牙
    疑惑点
    1.在介绍零拷贝时,降低上下文切换频率提到的将 read 和 write 两次系统调用合并为一次是通过 sendfile 实现吗?
    2.异步 IO 没有使用 PageCache,因为与虚拟内存系统耦合太紧,这块没有看明白

    思考题
    异步 IO 一定不会阻塞进程吗?如果阻塞了进程,该如何解决?
    这个问题我觉得要看怎么定义异步 IO
    Posix 对异步 IO 的定义为:异步 IO 操作不引起请求进程阻塞。老师在文中对异步 IO 的定义也类似,当内核把磁盘中的数据拷贝到进程缓冲区后,会通知进程去处理数据,所以按照这样的定义,异步 IO 不会阻塞进程

    《UNIX网络编程》这本书把 IO 模型分成了5类
    1.阻塞 IO
    2.非阻塞 IO
    3.IO 复用(select 和 poll)
    4.信号驱动
    5.异步 IO(Posix.1 的 aio 系统函数)

    一篇介绍IO模型的文章
    https://mp.weixin.qq.com/s/RiWEVd9IvG0Vlbz6Swn5bg

    如果把信号驱动的 IO 模型也看成异步 IO,因为用户进程在调用 sigaction 后,会继续执行其他任务,这里是非阻塞的,内核会在数据准备好时通知用户进程,然后由用户进程发起 recvfrom 的系统调用,把数据从内核缓冲区拷贝到用户空间,此时用户进程是阻塞的

    所以还是看如何定义异步 IO,至于那些伪异步 IO,怎么解决,我觉得解决办法就一个,就是等内核把数据准备好后,自己把数据从内核缓冲区复制到用户空间,然后通知用户进程进行数据的处理
    2020-05-07
    1
  • test
    真的是每篇都是干货,买对了。
    2020-05-06
    1
  • 问题大师
    从图中可以看到,异步 IO 并没有拷贝到 PageCache 中,这其实是异步 IO 实现上的缺陷。经过 PageCache 的 IO 我们称为缓存 IO,它与虚拟内存系统耦合太紧,导致异步 IO 从诞生起到现在都不支持缓存 IO。
    陶老师 我在异步IO图中,看到的是把磁盘数据拷贝到PageCache,是图错了嘛,如果是直接IO的话,是直接拷贝到用户进程缓存区嘛,这个过程就是绕过了内核态嘛

    作者回复: 你好问题大师,谢谢你的提醒,图上的文字错啦,我马上联系编辑小姐姐更正!

    2020-05-06
    5
    1
  • 陈建斌红了..
    您好,从经验上来讲,文件多大算大文件呢
    2020-05-08
  • Ken
    长肥网络定义

    一个具有大带宽时延乘积的网络也被称之为长胖网络(long fat network,简写为LFN,经常发音为“elephen”)。根据RFC 1072中的定义,如果一个网络的带宽时延乘积显著大于105比特(12500字节),该网络被认为是长肥网络。
    2020-05-08
  • C家族铁粉
    看到陶辉老师在部落里说,最开始文章有6000字,后来不停删删删,变成了现在的版本,太可惜了,删减掉的部分可以考虑放到其他地方供读者阅读啊。

    作者回复: 没有啦,我思维中的知识是网状的,文字是线性的,你在线性阅读中能够坚持下去,最后还原为树状、网状知识,其实要求挺高的。之前我的行文枝节太多,很难让多数读者坚持下来,是编辑小姐姐对我各种指导,集中炮火攻击一点,才有现在比较通顺流畅的文章,^_^

    2020-05-08
  • 那时刻
    异步IO可能会阻塞进程,我理解的是内核向磁盘发送完读请求之后,才返回到调用方。可以采用多线程读文件的方式来解决?类似于Netty处理网络数据采用的reactive方式。
    2020-05-07
  • hanazawakana
    windows的IOCP才是真正的异步IO吧,linux现在的AIO应该不能做到真正的不阻塞吧
    2020-05-06
  • hanazawakana
    直接IO还是需要4次内核态用户态切换吗
    2020-05-06
    1
  • 𢘫
    请教您个问题,陶老师。我们有个业务要上传二百多个文件,用户量是几十万,这种情形下怎么很好的控制(或估量)文件传输所占用的带宽呢?
    2020-05-06
  • 一步
    PageCache 中的大文件没有享受到缓存的好处,但却耗费 CPU 多拷贝到 PageCache 一次
    --------------------------------------
    为什么说多拷贝一次 到 PageCache 呢? 不是都是会从磁盘拷贝数据到内核态呢? 只是 零拷贝会把数据拷贝到 PageCache 中;不使用 零拷贝 也会把数据拷贝到内核态其他地方,总的来说不都是一次数据拷贝吗?
    2020-05-06
  • 木木匠
    课后思考题:
    我觉得异步IO还是会阻塞进程,在图中的时序图中,当进程收到IO处理完毕的通知后,进程需要进行IO处理,这个时候需要从内核态复制数据到用户态,这个过程是阻塞的。
    解决方法:我只想到一个思路,不知道是否可行,如果能够异步IO能把数据复制到用户态之后再通知,那就是真正的异步非阻塞了。

    希望老师和同学们指正交流。
    2020-05-06
收起评论
16
返回
顶部