网络通信优化之I/O模型:如何解决高并发下I/O瓶颈

Hi,我是阿昌,今天学习记录的是关于网络通信优化之I/O模型:如何解决高并发下I/O瓶颈

提到 Java I/O,相信你一定不陌生。

可能使用 I/O 操作读写文件,也可能使用它实现 Socket 的信息传输…这些都是在系统中最常遇到的和 I/O 有关的操作。

都知道,I/O 的速度要比内存速度慢,尤其是在现在这个大数据时代背景下,I/O 的性能问题更是尤为突出,I/O 读写已经成为很多应用场景下的系统性能瓶颈,不容忽视。


一、什么是 I/O

I/O机器获取和交换信息的主要渠道,而流是完成 I/O 操作的主要方式。

在计算机中,流是一种信息的转换。流是有序的,因此相对于某一机器或者应用程序而言,通常把 机器或者应用程序接收外界的信息 称为输入流(InputStream),从机器或者应用程序向外输出的信息 称为输出流(OutputStream),合称为输入 / 输出流(I/O Streams)。

机器间或程序间在进行信息交换或者数据交换时,总是先将对象或数据转换为某种形式的流,再通过流的传输,到达指定机器或程序后,再将流转换为对象数据。

因此,就可以被看作是一种数据的载体,通过它可以实现数据交换和传输

Java 的 I/O 操作类在包 java.io 下,其中 InputStream、OutputStream 以及 Reader、Writer 类是 I/O 包中的 4 个基本类,它们分别处理字节流和字符流。

如下图所示:


一个疑问:“ 不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?

知道字符到字节必须经过转码,这个过程非常耗时,如果不知道编码类型就很容易出现乱码问题。所以 I/O 流提供了一个直接操作字符的接口,方便平时对字符进行流操作。

下面就分别了解下“字节流”和“字符流”。

1、字节流

InputStream/OutputStream 是字节流的抽象类,这两个抽象类又派生出了若干子类,不同的子类分别处理不同的操作类型。

  • 如果是文件的读写操作,就使用 FileInputStream/FileOutputStream;

  • 如果是数组的读写操作,就使用 ByteArrayInputStream/ByteArrayOutputStream;

  • 如果是普通字符串的读写操作,就使用 BufferedInputStream/BufferedOutputStream。

具体内容如下图所示:

2、字符流

Reader/Writer 是字符流的抽象类,这两个抽象类也派生出了若干子类,不同的子类分别处理不同的操作类型,具体内容如下图所示:


二、传统 I/O 的性能问题

I/O 操作分为磁盘 I/O 操作网络 I/O 操作

  • 前者是从磁盘中读取数据源输入到内存中,之后将读取的信息持久化输出在物理磁盘上;

  • 后者是从网络中读取信息输入到内存,最终将信息输出到网络中。

不管是磁盘 I/O 还是网络 I/O,在传统 I/O 中都存在严重的性能问题

1、多次内存复制

在传统 I/O 中,可以通过 InputStream 从源数据中读取数据流输入到缓冲区里,通过 OutputStream 将数据输出到外部设备(包括磁盘、网络)。

可以先看下输入操作在操作系统中的具体流程,如下图所示:

  • JVM 会发出 read() 系统调用,并通过 read 系统调用向内核发起读请求;
  • 内核向硬件发送读指令,并等待读就绪;
  • 内核把将要读取的数据复制到指向的内核缓存中;
  • 操作系统内核将数据复制到用户空间缓冲区,然后 read 系统调用返回。

在这个过程中,数据先从外部设备复制到内核空间,再从内核空间复制到用户空间,这就发生了两次内存复制操作。

这种操作会导致不必要的数据拷贝和上下文切换,从而降低 I/O 的性能。

2、阻塞

在传统 I/O 中,InputStream 的 read() 是一个 while 循环操作,它会一直等待数据读取,直到数据就绪才会返回。

这就意味着如果没有数据就绪,这个读取操作将会一直被挂起,用户线程将会处于阻塞状态。在少量连接请求的情况下,使用这种方式没有问题,响应速度也很高。

在发生大量连接请求时,就需要创建大量监听线程,这时如果线程没有数据就绪就会被挂起,然后进入阻塞状态。

一旦发生线程阻塞,这些线程将会不断地抢夺 CPU 资源,从而导致大量的 CPU 上下文切换,增加系统的性能开销。


三、如何优化 I/O 操作

面对以上两个性能问题,不仅编程语言对此做了优化,各个操作系统也进一步优化了 I/O。

  • JDK1.4 发布了 java.nio 包(new I/O 的缩写),NIO 的发布优化了内存复制以及阻塞导致的严重性能问题。

  • JDK1.7 又发布了 NIO2,提出了从操作系统层面实现的异步 I/O。

1、使用缓冲区优化读写流操作

在传统 I/O 中,提供了基于流的 I/O 实现,即 InputStream 和 OutputStream,这种基于流的实现以字节为单位处理数据。

NIO 与传统 I/O 不同,它是基于块(Block)的,它以块为基本单位处理数据。在 NIO 中,最为重要的两个组件是缓冲区(Buffer)和通道(Channel)。

Buffer 是一块连续的内存块,是 NIO 读写数据的中转地。Channel 表示缓冲数据的源头或者目的地,它用于读取缓冲或者写入数据,是访问缓冲的接口。

传统 I/O 和 NIO 的最大区别就是传统 I/O 是面向流,NIO 是面向 Buffer。

Buffer 可以将文件一次性读入内存再做后续处理,而传统的方式是边读文件边处理数据。虽然传统 I/O 后面也使用了缓冲块,例如 BufferedInputStream,但仍然不能和 NIO 相媲美。

使用 NIO 替代传统 I/O 操作,可以提升系统的整体性能,效果立竿见影。

2、使用 DirectBuffer 减少内存复制

NIO 的 Buffer 除了做了缓冲块优化之外,还提供了一个可以直接访问物理内存的类 DirectBuffer。

普通的 Buffer 分配的是 JVM 堆内存,而 DirectBuffer 是直接分配物理内存 (非堆内存)。

我们知道数据要输出到外部设备,必须先从用户空间复制到内核空间,再复制到输出设备,而在 Java 中,在用户空间中又存在一个拷贝,那就是从 Java 堆内存中拷贝到临时的直接内存中,通过临时的直接内存拷贝到内存空间中去。

此时的直接内存和堆内存都是属于用户空间。


为什么 Java 需要通过一个临时的非堆内存来复制数据呢?

如果单纯使用 Java 堆内存进行数据拷贝,当拷贝的数据量比较大的情况下,Java 堆的 GC 压力会比较大,而使用非堆内存可以减低 GC 的压力。

DirectBuffer 则是直接将步骤简化为数据直接保存到非堆内存,从而减少了一次数据拷贝。以下是 JDK 源码中 IOUtil.java 类中的 write 方法:

        if (src instanceof DirectBuffer)return writeFromNativeBuffer(fd, src, position, nd);// Substitute a native bufferint pos = src.position();int lim = src.limit();assert (pos <= lim);int rem = (pos <= lim ? lim - pos : 0); ByteBuffer bb = Util.getTemporaryDirectBuffer(rem);try {bb.put(src);bb.flip();// ...............

由于 DirectBuffer 申请的是非 JVM 的物理内存,所以创建和销毁的代价很高。

DirectBuffer 申请的内存并不是直接由 JVM 负责垃圾回收,但在 DirectBuffer 包装类被回收时,会通过 Java Reference 机制来释放该内存块。DirectBuffer 只优化了用户空间内部的拷贝,而之前我们是说优化用户空间和内核空间的拷贝,那 Java 的 NIO 中是否能做到减少用户空间和内核空间的拷贝优化呢?

答案是可以的DirectBuffer 是通过 unsafe.allocateMemory(size) 方法分配内存,也就是基于本地类 Unsafe 类调用 native 方法进行内存分配的。

在 NIO 中,还存在另外一个 Buffer 类:MappedByteBuffer,跟 DirectBuffer 不同的是,MappedByteBuffer 是通过本地类调用 mmap 进行文件内存映射的,map() 系统调用方法会直接将文件从硬盘拷贝到用户空间,只进行一次数据拷贝,从而减少了传统的 read() 方法从硬盘拷贝到内核空间这一步。

3、避免阻塞,优化 I/O 操作

NIO 很多人也称之为 Non-block I/O,即非阻塞 I/O,因为这样叫,更能体现它的特点。

为什么这么说呢?传统的 I/O 即使使用了缓冲块,依然存在阻塞问题。

由于线程池线程数量有限,一旦发生大量并发请求,超过最大数量的线程就只能等待,直到线程池中有空闲的线程可以被复用。

而对 Socket 的输入流进行读取时,读取流会一直阻塞,直到发生以下三种情况的任意一种才会解除阻塞:

  • 有数据可读;
  • 连接释放;
  • 空指针或 I/O 异常。

阻塞问题,就是传统 I/O 最大的弊端。

NIO 发布后,通道和多路复用器这两个基本组件实现了 NIO 的非阻塞,下面我们就一起来了解下这两个组件的优化原理。

①通道(Channel)

传统 I/O 的数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。

最开始,在应用程序调用操作系统 I/O 接口时,是由 CPU 完成分配,这种方式最大的问题是“发生大量 I/O 请求时,非常消耗 CPU“;

之后,操作系统引入了 DMA(直接存储器存储),内核空间与磁盘之间的存取完全由 DMA 负责,但这种方式依然需要向 CPU 申请权限,且需要借助 DMA 总线来完成数据的复制操作,如果 DMA 总线过多,就会造成总线冲突。

通道的出现解决了以上问题,Channel 有自己的处理器,可以完成内核空间和磁盘之间的 I/O 操作。

在 NIO 中,读取和写入数据都要通过 Channel,由于 Channel 是双向的,所以读、写可以同时进行。

②多路复用器(Selector)

Selector 是 Java NIO 编程的基础。

用于检查一个或多个 NIO Channel 的状态是否处于可读、可写。Selector 是基于事件驱动实现的,我们可以在 Selector 中注册 accpet、read 监听事件,Selector 会不断轮询注册在其上的 Channel,如果某个 Channel 上面发生监听事件,这个 Channel 就处于就绪状态,然后进行 I/O 操作。

一个线程使用一个 Selector,通过轮询的方式,可以监听多个 Channel 上的事件

可以在注册 Channel 时设置该通道为非阻塞,当 Channel 上没有 I/O 操作时,该线程就不会一直等待了,而是会不断轮询所有 Channel,从而避免发生阻塞。

目前操作系统的 I/O 多路复用机制都使用了 epoll,相比传统的 select 机制,epoll 没有最大连接句柄 1024 的限制。所以 Selector 在理论上可以轮询成千上万的客户端。


一个生活化的场景来举例,可以把监听多个 I/O 连接请求比作一个火车站的进站口。

以前检票只能让搭乘就近一趟发车的旅客提前进站,而且只有一个检票员,这时如果有其他车次的旅客要进站,就只能在站口排队。这就相当于最早没有实现线程池的 I/O 操作。

后来火车站升级了,多了几个检票入口,允许不同车次的旅客从各自对应的检票入口进站。这就相当于用多线程创建了多个监听线程,同时监听各个客户端的 I/O 请求。

最后火车站进行了升级改造,可以容纳更多旅客了,每个车次载客更多了,而且车次也安排合理,乘客不再扎堆排队,可以从一个大的统一的检票口进站了,这一个检票口可以同时检票多个车次。这个大的检票口就相当于 Selector,车次就相当于 Channel,旅客就相当于 I/O 流。


四、总结

Java 的传统 I/O 开始是基于 InputStream 和 OutputStream 两个操作流实现的,这种流操作是以字节为单位,如果在高并发、大数据场景中,很容易导致阻塞,因此这种操作的性能是非常差的。

输出数据从用户空间复制到内核空间,再复制到输出设备,这样的操作会增加系统的性能开销。

传统 I/O 后来使用了 Buffer 优化了“阻塞”这个性能问题,以缓冲块作为最小单位,但相比整体性能来说依然不尽人意。

于是 NIO 发布,它是基于缓冲块为单位的流操作,在 Buffer 的基础上,新增了两个组件“管道和多路复用器”,实现了非阻塞 I/O,NIO 适用于发生大量 I/O 连接请求的场景,这三个组件共同提升了 I/O 的整体性能。


在 JDK1.7 版本中,Java 发布了 NIO 的升级包 NIO2,也就是 AIO。AIO 实现了真正意义上的异步 I/O,它是直接将 I/O 操作交给操作系统进行异步处理。这也是对 I/O 操作的一种优化,那为什么现在很多容器的通信框架都还是使用 NIO 呢?

Linux中,AIO并未真正使用操作系统所提供的异步I/O,它仍然使用poll或epoll,并将API封装为异步I/O的样子,但是其本质仍然是同步非阻塞I/O,加上第三方产品的出现,Java网络编程明显落后,所以没有成为主流


Day783.网络通信优化之I/O模型:如何解决高并发下I/O瓶颈 -Java 性能调优实战相关推荐

  1. 《Java性能调优实战》笔记(一)Java编程性能调优、多线程性能优化

    文章目录 一.Java性能调优概述 1.1 性能调优标准 1.2 制定性能调优策略 二.Java编程性能调优 2.1 字符串 2.2 正则表达式 2.3 ArrayList和LinkedList的选择 ...

  2. Day794.如何用协程来优化多线程业务 -Java 性能调优实战

    如何用协程来优化多线程业务 Hi,我是阿昌,今天学习记录的是关于如何用协程来优化多线程业务. 近一两年,国内很多互联网公司开始使用或转型 Go 语言,其中一个很重要的原因就是 Go 语言优越的性能表现 ...

  3. Day814.电商系统表设计优化案例分析 -Java 性能调优实战

    电商系统表设计优化案例分析 Hi,我是阿昌,今天学习记录的是关于电商系统表设计优化案例分析. 如果在业务架构设计初期,表结构没有设计好,那么后期随着业务以及数据量的增多,系统就很容易出现瓶颈. 如果表 ...

  4. 面试怕被问“后端优化”问题?看看这套java性能调优手册吧!

    对于很多研发人员来说,Java 性能调优都是很头疼的问题. 比如,一个简单的系统就囊括了应用程序.数据库.容器.操作系统.网络等技术,线上一旦出现性能问题,就可能要你协调多方面组件去进行优化.另外,很 ...

  5. Spark商业案例与性能调优实战100课》第3课:商业案例之通过RDD分析大数据电影点评系各种类型的最喜爱电影TopN及性能优化技巧

    Spark商业案例与性能调优实战100课>第3课:商业案例之通过RDD分析大数据电影点评系各种类型的最喜爱电影TopN及性能优化技 源代码 package com.dt.spark.coresi ...

  6. Java 程序性能优化《第一章》Java性能调优概述 1.4小结

    Java 程序性能优化<第一章>1.4小结 通过本章的学习,读者应该了解性能的基本概念及其常用的参考指标.此外,本章还较为详细的介绍了与性能调优相关的两个重要理论--木桶原理以及Amdah ...

  7. mysql性能优化与高可用_MySQL管理之道:性能调优、高可用与监控》迷你书

    MySQL管理之道:性能调优.高可用与监控>迷你书 MYSQL5.5.X主要改进 1.默认使用innodb存储引擎 2.充分利用CPU多核处理能力 3.提高刷写脏页数量和合并插入数量,改善I/O ...

  8. jvm性能调优实战 - 35电商APP后台系统如何对Full GC进行深度优化

    文章目录 业务背景 JVM性能问题 如何优化每次Full GC的性能? 调优后的效果 思考 业务背景 这个JVM性能优化的案例,很多核心的思想其实也跟之前是相同的,只不过在优化的过程中会带出来一些比较 ...

  9. jvm性能调优实战 -60 线上系统的JVM参数优化、GC问题定位排查、OOM分析解决

    文章目录 总结 总结 第一个是JVM运行我们写好的系统最根本的工作原理,包括: 内存各个部分的划分 代码在执行的过程中,各个内存区域是如何配合协调工作的 对象是如何分配的 GC如何触发 GC执行的原理 ...

最新文章

  1. nginx php页面打开404,nginx php页面 error_page 404不起作用解决
  2. servlet配置web.xml问题
  3. python运行方式特点_编程必修课:一文弄懂python的运行机制
  4. python递归函数的意思_Python 递归函数
  5. python统计分析--4.Logistic回归
  6. Taro+react开发(88):taro条件渲染
  7. 为了这个技术,操作系统把 CPU 害惨了!
  8. 两个空间点直接距离投影公式_HBAO(屏幕空间的环境光遮蔽)
  9. docker build mysql,Docker创建MySQL容器的方法
  10. dispatcherServlet流程图
  11. My first App Encrypt Wheel is Ready to Download!
  12. Summary on deep learning framework --- Torch7
  13. autohotkey循环
  14. 视频倍速插件(火狐,谷歌)
  15. java枚举类的作用及其使用
  16. [FAQ20527] 如何关闭OTG功能
  17. Photoshop学习(十六):利用蒙版合成图像
  18. 用python让excel飞起来 pdf_讯飞会议宝S8体验:以后开会请速记的钱,可以省了
  19. [Pyhon大数据分析] 五.人民网新闻话题抓取及Gephi构建主题知识图谱
  20. 使用蒲公英平台测试iOS APP(示例)

热门文章

  1. 极简轻奢全屋定制样板7天被打卡了100遍,栓Q了
  2. 初二生态系统思维导图_初中七、八年级生物思维导图大全21张.pdf
  3. NFS服务器搭建及配置
  4. 配置给127.0.0.1配置本地域名
  5. java 设置日期的格式_java设置日期格式
  6. 百度dueros人工智能-RDA5981(R01开发板)-学习心得-入门及硬件连接
  7. Allegro文件转AD
  8. 【一起学UniGUI】--UniGUI的安装(2)
  9. 【origin绘图】如何通过插入小图的方法放大局部图
  10. 英文字母和中文汉字在不同字符集编码下的字节数