前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >如何解决高并发I/O瓶颈

如何解决高并发I/O瓶颈

作者头像
小土豆Yuki
发布2020-11-19 17:39:17
发布2020-11-19 17:39:17
3K00
代码可运行
举报
文章被收录于专栏:洁癖是一只狗洁癖是一只狗
运行总次数:0
代码可运行

在现在这个大数据时代下,IO的性能问题更是尤为突出,IO读写已经成为应用场景的瓶颈,不容我们忽视,今天,我们就深入了解下Java IO在高并发,大数据场景下暴露出的性能问题.

什么是IO

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

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

机器间或程序间在进行信息交换和数据交换时,总是先将对象或数据转换成某种形式的流,再通过流的传输,到达指定机器或者程序后,再将流转换为对象数据,因此流就可以被看做一种数据的载体,通过它可以实现数据交换和传输

我们发现不管是文件读写还是网络发送接收,信息的最小单元都是字节,那为什么I/O流操作要分为字节流操作和字符流操作

我们在通常在通信时候,使用的是字节流FileInputStream来实现数据的传输,你会发现,我们在读取read()和写入write()的时候都是讲字符转换成字节在进行写入操作,同样读操作类似,如果是中文,在GBK中一般占两个字节,如果通过字节流的方式只读取一个字节,是无法转成一个中文汉字,而字符流就是为了解决这个问题,而字符流会根据默认编码读取字符,比如是GBK编码,字符流读取两个字节,因此字符流是根据字符所占的字节大小而决定读取多少字节的,

字节流

InputStream/OutputStream是字节流的抽象类,这两个抽象类又派生若干子类,不同子类处理不同的操作类型,如果是文件的读写操作,使用FileInputStream/FileOutputStream,如果是数组的读写操作,使用ByteArrayInputStream/ByteArrayOutputStream.如果是普通字符串的读写操作,使用BufferedInputStream/BufferedOutputStream,

字符流

Reader/Writer是字符流的抽象类,这个抽象类也衍生出了若干子类

传统I/O性能问题

我们知道传统的I/O操作分为网络I/O和磁盘I/O,但是都是存在严重的性能问题

多次内存复制

传统的I/O中,我们可以使用InputStream从数据中读取数据输入到缓冲区里,通过OutputStream将数据输出到外部设备,具体操作如下图

JVM会发出read()系统调用,并通过read系统调用发起读写请求

内核向硬件发送读指令,并等待读就绪

内核把将要读取的数据复制到指定的内核缓冲中

操作系统内核将数据复制到用户空间缓冲区,然后read系统调用返回

在上面操作中,数据先从外部设备复制到内核空间,在从内核空间复制到用户空间,这就是发生了两次复制操作,这就会导致不必要的数据拷贝和上下文切换,从而降低I/O的性能

阻塞

传统的I/O操作,inputStream的read是一个while循环操作,他等待数据读取,直到数据就绪才会返回,这就意味如果没有数据就绪,这个读取将一直被挂起,用户线程将会处于阻塞状态,在请求量少的情况写,使用这种方式,没有太大的问题,但是如果请求量大的时候,线程没有数据就会挂起,导致阻塞,线程就会竞争CPU,从而导致大量的CPU上下文切换,增加性能开销

如何优化I/O操作

JDK1.4发布了java.nio包,NIO的发布优化了内存复制以及阻塞导致的严重性能,而JDK1.7发布了NIO2,提出从操作系统层面实现了异步I/O.

使用缓存优化读写流操作

传统的I/O操作是基于字节为单位处理数据,而NIO是基于块(Block)的,他以块基于单位处理数据,在NIO中,最为重要的两个组件buffer和channel,Buffer是一块连续的内存块,是NIO读取数据的中转地,Channel表示缓存数据的源头和目的地,他是读取缓存或写入数据,是访问缓存的接口

传统I/O和NIO最大的区别传统的I/O是面向流,而NIO是面向buffer,buffer可以一次性把数据读入内存在处理数据,而传统的是边读取边处理数据,虽然传统的I/O也提供的缓存,但任然不能和NIO相媲美.

使用DirectBuffer减少内存复制

NIO的Buffer除了做缓冲优化以外,还提供一个可以直接访问物理内存的类DirectBuffer,而普通的Buffer分配的是JVM堆内存,而DirectBuffer是直接的物理内存(非堆内存)

我们知道数据输出到外部设备,必须先把用户空间复制到内核空间,在复制到外部设备,而java中,在用户空间还存在一种复制,就是把Java堆内存数据拷贝到临时的直接内存中,通过临时的直接内存拷贝到内存空间中去,此时的直接内存和堆内存都是用户空间。

但是java为什么要通过一个临时的非堆内存来复制数据呢,如果单纯使用java堆内存进行拷贝,当拷贝量大的时候,就会对GC带来压力,而使用非堆内存可以减少GC的压力,DirectBuffer则直接将简化数据直接保存到非堆内存中,从而减少一次数据拷贝,下面是JDK源码中IOUtil.java类中的write方法

代码语言:javascript
代码运行次数:0
复制
if (src instanceof DirectBuffer)
return writeFromNativeBuffer(fd, src, position, nd);

// Substitute a native buffer
int 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只优化了用户空间内部的拷贝,但是如何优化用户空间和内核空间的拷贝呢,答案是DirectBuffer是通过unsafe.allocateMemory(size)方法分配内存的,也就是基于本地类Unsafe调用native方法进行内存分配的,而在NIO中,还存在另外一个Buffer类:MappedByteBuffer跟DirectBuffer不同的是,MappedByteBuffer通过本地类调用mmap进行文件内存映射,map系统调用会直接将硬盘的文件复制到用户空间,只进行一步拷贝,从而减少传统read方法从硬盘拷贝到内核空间这一步

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

NIO很多人称为阻塞IO,这样更能体现他的特点,与之相比传统的I/O即使使用了缓存块,依然存在阻塞问题,由于线程数量有限,一旦发生大量并发请求,超过了最大线程就必须等待,知道线程池有空闲线程可以复用,而对于Socket的输入流进行读取时候,读取流会一直阻塞,直到发生以下三种情况任意一种才会接触阻塞

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

阻塞问题是传统问题最大的弊病,但NIO发布之后,通道和多路复用两个基本组件实现了NIO的非阻塞.

通道

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

最开始,在应用程序调用操作系统的I/O时候,是通过CPU完成分配的,这种方式会导致发生大量I/O请求时候,非常消耗CPU,之后,就引入了DMA(直接存储器存储),且需要借助DMA总线分配内存空间和磁盘之间的存取,但是这种方式依然需要向CPU申请权限,且借助DMA总线来完成数据的复制操作,如果DMA总线过多,就会造成总线冲突

通道就是解决以上问题,Channel有自己的处理器,可以完成内存空间和磁盘之间的I/O操作,在NIO中,我们读取和写入都要通过channel,由于channel是双向的,所以读写可以同时进行。

多路复用器

Selector是Java I/O的基础,他是用来检查一个或多个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的限制,所以Selelctor在理论上可以轮询成千上万的客户端

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-11-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 洁癖是一只狗 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档