博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
java io
阅读量:5978 次
发布时间:2019-06-20

本文共 6161 字,大约阅读时间需要 20 分钟。

hot3.png

java io 博客分类: java

NIO 的工作方式

BIO 带来的挑战

BIO 即阻塞 I/O,不管是磁盘 I/O 还是网络 I/O,数据在写入 OutputStream 或者从 InputStream 读取时都有可能会阻塞。一旦有线程阻塞将会失去 CPU 的使用权,这在当前的大规模访问量和有性能要求情况下是不能接受的。虽然当前的网络 I/O 有一些解决办法,如一个客户端一个处理线程,出现阻塞时只是一个线程阻塞而不会影响其它线程工作,还有为了减少系统线程的开销,采用线程池的办法来减少线程创建和回收的成本,但是有一些使用场景仍然是无法解决的。如当前一些需要大量 HTTP 长连接的情况,像淘宝现在使用的 Web 旺旺项目,服务端需要同时保持几百万的 HTTP 连接,但是并不是每时每刻这些连接都在传输数据,这种情况下不可能同时创建这么多线程来保持连接。即使线程的数量不是问题,仍然有一些问题还是无法避免的。如这种情况,我们想给某些客户端更高的服务优先级,很难通过设计线程的优先级来完成,另外一种情况是,我们需要让每个客户端的请求在服务端可能需要访问一些竞争资源,由于这些客户端是在不同线程中,因此需要同步,而往往要实现这些同步操作要远远比用单线程复杂很多。以上这些情况都说明,我们需要另外一种新的 I/O 操作方式。

 

I/O 调优

 

下面就磁盘 I/O 和网络 I/O 的一些常用的优化技巧进行总结如下:

 

磁盘 I/O 优化

 

性能检测

 

我们的应用程序通常都需要访问磁盘读取数据,而磁盘 I/O 通常都很耗时,我们要判断 I/O 是否是一个瓶颈,我们有一些参数指标可以参考:

 

如我们可以压力测试应用程序看系统的 I/O wait 指标是否正常,例如测试机器有 4 个 CPU,那么理想的 I/O wait 参数不应该超过 25%,如果超过 25% 的话,I/O 很可能成为应用程序的性能瓶颈。Linux 操作系统下可以通过 iostat 命令查看。

 

通常我们在判断 I/O 性能时还会看另外一个参数就是 IOPS,我们应用程序需要最低的 IOPS 是多少,而我们的磁盘的 IOPS 能不能达到我们的要求。每个磁盘的 IOPS 通常是在一个范围内,这和存储在磁盘的数据块的大小和访问方式也有关。但是主要是由磁盘的转速决定的,磁盘的转速越高磁盘的 IOPS 也越高。

 

现在为了提高磁盘 I/O 的性能,通常采用一种叫 RAID 的技术,就是将不同的磁盘组合起来来提高 I/O 性能,目前有多种 RAID 技术,每种 RAID 技术对 I/O 性能提升会有不同,可以用一个 RAID 因子来代表,磁盘的读写吞吐量可以通过 iostat 命令来获取,于是我们可以计算出一个理论的 IOPS 值,计算公式如下所以:

 

( 磁盘数 * 每块磁盘的 IOPS)/( 磁盘读的吞吐量 +RAID 因子 * 磁盘写的吞吐量 )=IOPS

 

这个公式的详细信息请查阅参考资料 。

 

提升 I/O 性能

 

提升磁盘 I/O 性能通常的方法有:

 

  1. 增加缓存,减少磁盘访问次数
  2. 优化磁盘的管理系统,设计最优的磁盘访问策略,以及磁盘的寻址策略,这里是在底层操作系统层面考虑的。
  3. 设计合理的磁盘存储数据块,以及访问这些数据块的策略,这里是在应用层面考虑的。如我们可以给存放的数据设计索引,通过寻址索引来加快和减少磁盘的访问,还有可以采用异步和非阻塞的方式加快磁盘的访问效率。
  4. 应用合理的 RAID 策略提升磁盘 IO,每种 RAID 的区别我们可以用下表所示:

 

表 2.RAID 策略

 

磁盘阵列
说明
RAID 0 数据被平均写到多个磁盘阵列中,写数据和读数据都是并行的,所以磁盘的 IOPS 可以提高一倍。
RAID 1 RAID 1 的主要作用是能够提高数据的安全性,它将一份数据分别复制到多个磁盘阵列中。并不能提升 IOPS 但是相同的数据有多个备份。通常用于对数据安全性较高的场合中。
RAID 5 这中设计方式是前两种的折中方式,它将数据平均写到所有磁盘阵列总数减一的磁盘中,往另外一个磁盘中写入这份数据的奇偶校验信息。如果其中一个磁盘损坏,可以通过其它磁盘的数据和这个数据的奇偶校验信息来恢复这份数据。
RAID 0+1 如名字一样,就是根据数据的备份情况进行分组,一份数据同时写到多个备份磁盘分组中,同时多个分组也会并行读写。

 

网络 I/O 优化

 

网络 I/O 优化通常有一些基本处理原则:

 

  1. 一个是减少网络交互的次数:要减少网络交互的次数通常我们在需要网络交互的两端会设置缓存,比如 Oracle 的 JDBC 驱动程序,就提供了对查询的 SQL 结果的缓存,在客户端和数据库端都有,可以有效的减少对数据库的访问。关于 Oracle JDBC 的内存管理可以参考《 Oracle JDBC 内存管理》。除了设置缓存还有一个办法是,合并访问请求:如在查询数据库时,我们要查 10 个 id,我可以每次查一个 id,也可以一次查 10 个 id。再比如在访问一个页面时通过会有多个 js 或 css 的文件,我们可以将多个 js 文件合并在一个 HTTP 链接中,每个文件用逗号隔开,然后发送到后端 Web 服务器根据这个 URL 链接,再拆分出各个文件,然后打包再一并发回给前端浏览器。这些都是常用的减少网络 I/O 的办法。
  2. 减少网络传输数据量的大小:减少网络数据量的办法通常是将数据压缩后再传输,如 HTTP 请求中,通常 Web 服务器将请求的 Web 页面 gzip 压缩后在传输给浏览器。还有就是通过设计简单的协议,尽量通过读取协议头来获取有用的价值信息。比如在代理程序设计时,有 4 层代理和 7 层代理都是来尽量避免要读取整个通信数据来取得需要的信息。
  3. 尽量减少编码:通常在网络 I/O 中数据传输都是以字节形式的,也就是通常要序列化。但是我们发送要传输的数据都是字符形式的,从字符到字节必须编码。但是这个编码过程是比较耗时的,所以在要经过网络 I/O 传输时,尽量直接以字节形式发送。也就是尽量提前将字符转化为字节,或者减少字符到字节的转化过程。
  4. 根据应用场景设计合适的交互方式:所谓的交互场景主要包括同步与异步阻塞与非阻塞方式,下面将详细介绍。

 

同步与异步

 

所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。而异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。我们可以用打电话和发短信来很好的比喻同步与异步操作。

 

在设计到 IO 处理时通常都会遇到一个是同步还是异步的处理方式的选择问题。因为同步与异步的 I/O 处理方式对调用者的影响很大,在数据库产品中都会遇到这个问题。因为 I/O 操作通常是一个非常耗时的操作,在一个任务序列中 I/O 通常都是性能瓶颈。但是同步与异步的处理方式对程序的可靠性影响非常大,同步能够保证程序的可靠性,而异步可以提升程序的性能,必须在可靠性和性能之间做个平衡,没有完美的解决办法。

 

阻塞与非阻塞

 

阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成 CPU 才接着完成其它的事。非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估。

 

两种的方式的组合

 

组合的方式可以由四种,分别是:同步阻塞、同步非阻塞、异步阻塞、异步非阻塞,这四种方式都对 I/O 性能有影响。下面给出分析,并有一些常用的设计用例参考。

 

表 3. 四种组合方式

 

组合方式
性能分析
同步阻塞 最常用的一种用法,使用也是最简单的,但是 I/O 性能一般很差,CPU 大部分在空闲状态。
同步非阻塞 提升 I/O 性能的常用手段,就是将 I/O 的阻塞改成非阻塞方式,尤其在网络 I/O 是长连接,同时传输数据也不是很多的情况下,提升性能非常有效。
这种方式通常能提升 I/O 性能,但是会增加 CPU 消耗,要考虑增加的 I/O 性能能不能补偿 CPU 的消耗,也就是系统的瓶颈是在 I/O 还是在 CPU 上。
异步阻塞 这种方式在分布式数据库中经常用到,例如在网一个分布式数据库中写一条记录,通常会有一份是同步阻塞的记录,而还有两至三份是备份记录会写到其它机器上,这些备份记录通常都是采用异步阻塞的方式写 I/O。
异步阻塞对网络 I/O 能够提升效率,尤其像上面这种同时写多份相同数据的情况。
异步非阻塞 这种组合方式用起来比较复杂,只有在一些非常复杂的分布式情况下使用,像集群之间的消息同步机制一般用这种 I/O 组合方式。如 Cassandra 的 Gossip 通信机制就是采用异步非阻塞的方式。
它适合同时要传多份相同的数据到集群中不同的机器,同时数据的传输量虽然不大,但是却非常频繁。这种网络 I/O 用这个方式性能能达到最高。

 

虽然异步和非阻塞能够提升 I/O 的性能,但是也会带来一些额外的性能成本,例如会增加线程数量从而增加 CPU 的消耗,同时也会导致程序设计的复杂度上升。如果设计的不合理的话反而会导致性能下降。在实际设计时要根据应用场景综合评估一下。

 

下面举一些异步和阻塞的操作实例:

 

在 Cassandra 中要查询数据通常会往多个数据节点发送查询命令,但是要检查每个节点返回数据的完整性,所以需要一个异步查询同步结果的应用场景,部分代码如下:

 

清单 3.异步查询同步结果

 

class AsyncResult implements IAsyncResult{     private byte[] result_;     private AtomicBoolean done_ = new AtomicBoolean(false);     private Lock lock_ = new ReentrantLock();     private Condition condition_;     private long startTime_;     public AsyncResult(){                condition_ = lock_.newCondition();// 创建一个锁        startTime_ = System.currentTimeMillis();     }     /*** 检查需要的数据是否已经返回,如果没有返回阻塞 */  public byte[] get(){         lock_.lock();         try{             if (!done_.get()){condition_.await();}         }catch (InterruptedException ex){             throw new AssertionError(ex);         }finally{lock_.unlock();}         return result_;  }  /*** 检查需要的数据是否已经返回 */     public boolean isDone(){return done_.get();}  /*** 检查在指定的时间内需要的数据是否已经返回,如果没有返回抛出超时异常 */     public byte[] get(long timeout, TimeUnit tu) throws TimeoutException{         lock_.lock();         try{            boolean bVal = true;             try{                 if ( !done_.get() ){            long overall_timeout = timeout - (System.currentTimeMillis() - startTime_);                     if(overall_timeout > 0)// 设置等待超时的时间                        bVal = condition_.await(overall_timeout, TimeUnit.MILLISECONDS);                     else bVal = false;                 }             }catch (InterruptedException ex){                 throw new AssertionError(ex);             }             if ( !bVal && !done_.get() ){// 抛出超时异常                throw new TimeoutException("Operation timed out.");             }         }finally{lock_.unlock();      }         return result_;  }  /*** 该函数拱另外一个线程设置要返回的数据,并唤醒在阻塞的线程 */     public void result(Message response){                try{             lock_.lock();             if ( !done_.get() ){                                result_ = response.getMessageBody();// 设置返回的数据                done_.set(true);                 condition_.signal();// 唤醒阻塞的线程            }         }finally{lock_.unlock();}            }     }

 


 

 

总结

 

本文阐述的内容较多,从 Java 基本 I/O 类库结构开始说起,主要介绍了磁盘 I/O 和网络 I/O 的基本工作方式,最后介绍了关于 I/O 调优的一些方法。

 

转载于:https://my.oschina.net/xiaominmin/blog/1597968

你可能感兴趣的文章
Mysql 监视工具
查看>>
Linux Namespace系列(09):利用Namespace创建一个简单可用的容器
查看>>
博客搬家了
查看>>
Python中使用ElementTree解析xml
查看>>
linux的日志服务器关于屏蔽一些关键字的方法
查看>>
mysql多实例实例化数据库
查看>>
javascript 操作DOM元素样式
查看>>
HBase 笔记3
查看>>
【Linux】Linux 在线安装yum
查看>>
Atom 编辑器系列视频课程
查看>>
[原][osgearth]osgearthviewer读取earth文件,代码解析(earth文件读取的一帧)
查看>>
mybatis update返回值的意义
查看>>
expdp 详解及实例
查看>>
通过IP判断登录地址
查看>>
深入浅出JavaScript (五) 详解Document.write()方法
查看>>
Beta冲刺——day6
查看>>
在一个程序中调用另一个程序并且传输数据到选择屏幕执行这个程序
查看>>
代码生成工具Database2Sharp中增加视图的代码生成以及主从表界面生成功能
查看>>
关于在VS2005中编写DLL遇到 C4251 警告的解决办法
查看>>
提高信息安全意识对网络勒索病毒说不
查看>>