专栏首页洁癖是一只狗如何解决高并发I/O瓶颈

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

在现在这个大数据时代下,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方法

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在理论上可以轮询成千上万的客户端

本文分享自微信公众号 - 洁癖是一只狗(rookie-dog),作者:洁癖汪

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-11-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Mysql为什么会抖一下呢

    在之前我们说过酒店记账的故事,其中酒店掌柜记账的的黑板就类似我们的redo log,而掌柜的记账本就是数据文件,掌柜的记忆就是内存。

    小土豆Yuki
  • 面试Mybatis之基本操作(collection和association)

    我们把级联关系基本操作都已经演示完毕,为了让大家更加深刻,我们在再介绍一下基本的概念。

    小土豆Yuki
  • 自动化运维实践 | Ansible入门

    ssh-keyscan remore_servers >> ~./ssh/known_hosts

    小土豆Yuki
  • jQuery第二十篇 位置和尺寸操作的方法

    用户7873631
  • 基于mxnet的LSTM实现RNN理论基础代码实现参考文献

    RNN理论基础 基本RNN结构 ? rnn_base.png RNN的基本结构如上左图所示,输出除了与当前输入有关,还与上一时刻状态有关。RNN结构展开可视为上...

    月见樽
  • PHP 数据类型

    PHP 支持三大类 8 种数据类型。 官方文档:http://php.net/manual/zh/language.types.php 标量(4) 布尔 boo...

    康怀帅
  • 深度学习公司Maluuba发布世界上最大的人造问答集来推动人工智能的研究

    MALUUBA是一家深度学习公司,位于加拿大魁北克省蒙特利尔市,致力于从事促进机器人像人类一样思考、推理和交流的事业。该公司今天宣布即将公开发行两个复杂的自然...

    AI科技大本营
  • php开启CURL扩展

    1、将PHP文件夹下的三个文件php_curl.dll,libeay32.dll,ssleay32.dll复制到system32下;

    似水的流年
  • 【案例】戏说十个有趣的“大数据”经典案例

    近两年,“大数据”这个词越来越为大众所熟悉,“大数据”一直是以高冷的形象出现在大众面前,面对大数据,相信许多人都一头雾水。下面我们通过十个经典案例,让大家实打实...

    小莹莹
  • flex-grow、flex-shrink、flex-basis详解

    flex-grow、flex-shrink、flex-basis这三个属性的作用是:在flex布局中,父元素在不同宽度下,子元素是如何分配父元素的空间的。

    Joel

扫码关注云+社区

领取腾讯云代金券