Java NIO

OverView

java.nio (NIO stands for non-blocking I/O) is a collection of Java programming language APIs that offer features for intensive I/O operations.

上述引用于New I/O (Java),这N就是NEW的意思,这命名好随意,像一个刚写程序的人命名的。如果再有更new的IO模型,该怎么命名?名字起的有点后无来者的意思,但也反映出,这IO通信模型也就这几种,不会再有更New的了。这NIO的名字为AsyncIO或者AIO,感觉更好理解点,至少题目中表现出来一点重点么。

JAVA中NIO只是一个封装了异步IO通信模型的包nio,当然java还有个大名鼎鼎的Netty的库,下面参考netty官网:

Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.

netty是个“framework”,自然有框架的属性,使用模式相对固定,按照框架的套路,实现一些方法,注册一些回调,框架会自己跑起来。从操作系统的系统调用角度看,有select、poll、epoll、完成端口的api,可以把java的NIO的库理解成对这些系统调用的通用的封装,而Netty就从系统调用的层次升级到了框架和模型,select模型和epoll模型,而模型的概念是更高层的,可以不用理解系统调用之间如何配合,只要知道这个模式是如何使用,那就可以用Netty进行开发了。

如何提高IO性能

NIO的存在目的主要是通信效率的提高,从使用的角度上看,其实没有改进,反而更难以理解。

虚拟机层面减少memory copy

其实也好理解,内存拷贝也是一种IO,如何提高IO,首先想到的就是减少IO,而JVM为了更方便的管理内存,会自己进行内存的管理,然而,对于IO来说,内存并不是JVM分配的(首先在操作系统的硬件驱动分配,然后到操作系统),如果还想管理,就只能拷贝到内存了。但是内存拷贝1秒也就1Gb多吧,对于千兆网卡来说,这内存拷贝的时间是不能忽略的。

对于Java而言,毕竟是跑在虚拟机上的,如果内存都要从虚拟机中获取那内存拷贝带来的开销是非常明显的,尤其是大块大批量的内存,虚拟机设计者意识到了这一点,NIO通信的内存,可以不用拷贝到JVM的内存中,而是JVM能够直接访问操作系统的缓冲区。

减少线程数目

如果调用IO操作的时候IO的是阻塞住的,那么该怎么办?可以开个线程,阻塞在那,一直等他返回,读到数据处理数据,这样每个socket一个线程会造成线程过多的问题。

线程的开销是比较大的,自身需要占用512k到1M的内存空间,如果线程多,那还有创建销毁的消耗、context switch的消耗、等等。所以尽量少用线程,像nb的ngnix和Redis都是单线程,但性能杠杠的。

那为了减少线程数目,一个线程管多个socket,从前向后,轮询read一组socket,然后设置个超时时间,比如1ms,也可以没有超时时间,不可读立刻返回个错误字,读到数据就找对用socket的处理方法处理,或者扔给socket的阻塞队列,但这样也会有个问题,如果一直没有数据,这线程还要轮询,这些轮询是多余的系统调用,也是性能的一个大问题。

NIO之前,read后可能没啥东西能read到,需要等待那边发来写数据到网卡,而NIO之后,是接收到可以read的事件通知后,才去read,这个时候的read是保证可以直接读取的。

从本质上看,这NIO减少了CPU的消耗。做一个假设,如果CPU的核心非常多,上下文切换非常快,CPU的工作都是瞬间完成的,那么最原始的,就是BIO模型,一个连接一个线程,还有天然的隔离的特性,阻塞就阻塞吧,反正也不影响别人,逻辑简单,当然一个前提,就是CPU不是问题。

IO模型

上面的几个模型在apue中也有提到过,讲的比较详细。

阻塞I/O

最古老的接口了,read()后就阻塞在那里。

非阻塞I/O

不停的检查是否有数据读了,如果不可操作,直接返回,一个线程能管多个socket,但是检查有多余的系统调用。

I/O 复用

这个分类有点大,这里指select的模型,当然,与非阻塞IO比比较相似,只是因为操作系统支持了一个这样的select系统调用接口,操作系统底层也是在一组socket中轮询,因为是操作系统底层的么,效率自然比上层用read+超时的方式高点。

信号驱动I/O

这个看上去挺高级,系统信号方式通知,但是,操作系统的信号量数目是有限制的,具体原因不清楚。很少听说IO模型用这的。

异步I/O

其实也可以放在I/O复用中的一类,唯一的区别就是通知的时候,连带的数据都接收好了,不光IO调用是异步的,底层从网卡到内存的数据接收也是异步的。windows上有完成端口很好的支持这模型,在linux上只有epoll模式,对这支持不好,但也可以通过上层的进一步的封装让linux也支持这模式,把Reactor封装成Proactor。

为什么在linux上没有类型windows完成端口一样,对网卡到内存的数据拷贝也是异步的相关的系统调用呢?简单分析下,网卡到内存异步拷贝有啥优势呢?memory级别的拷贝非常快,即使支持了,性能可能epoll模式也差不多?

误区

使用NIO != 高性能,当连接数<1000,并发程度不高或者局域网环境下NIO并没有显著的性能优势。

直接引用原文Java NIO浅析中的解释:

现在的多线程一般都使用线程池,可以让线程的创建和回收成本相对较低。在活动连接数不是特别高(小于单机1000)的情况下,这种模型(BIO)是比较不错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。

局域网有什么影响呢?文章中没有具体的说明,但是原因不难分析。如果在局域网中,网络环境好,发送一个请求一般一次read()操作就读到了完整的信息,如果客户端连续发送,甚至会读到多条完整的信息,一次read()读取到的数据体积大,相对而言比较高效。而在公网中,一个消息,可能会断断续续的分很多次才能read()到,每次都是个系统调用,都可能有上下文切换等等。

Reactor的事件通知机制也是有性能消耗的,至少有个事件通知、通知队列、还要考虑队列的同步、重复通知等问题,如果阻塞的read()效率高,那么假设系统吞吐量为1w消息每秒,如果业务对消息延迟不敏感,那么可能一次read()操作的系统掉用获取1000条消息,而在NIO中,如果处理消息速度快,每个消息每次通知都有处理,那1000条消息可能有1000甚至更多次的系统调用。

还有一种情况,在连接或并发数不多,但是IO又非常繁忙的时候,如果连接的socket大多数一直是繁忙的状态,那么select模型是要比epoll模型要好的,如果socket大多繁忙,那select每次轮训相当于不是浪费cpu的,是可以发起有效的IO操作的,而epoll模型因为要维护一个通知队列,需要考虑的因素比较多,如优先级,锁,LT模式的重通知等问题,在每个select的IO负载都高的时候,效率反而不如select。

当然上面的分析只是理论上的,实际怎么样,肯定要测试下。而且上述的场景比较特殊,一般情况下,如果不是对性能有严格的要求,不用刻意区分场景,直接Netty框架就完事。

Reactor 与 Proactor

这Reactor的名字有些霸气,反应堆,但这Proactor竟然是个生生造出来的词。之所以叫反应堆,是从服务端看,封装的这网络IO的这个东西不停的上报一些事件,可以读了,可以写了,可以处理长连接了,而且速度非常快,名字中能看出这Reactor的异步的特性。

在windows上,对于Proactor的支持比较好,而linux上需要上层做些封装。

这Proactor性能比Reactor好么?

这不好说,只分析下。因为Proactor从网卡读到内存是异步的IO,也就是说不用上层显示调用就能从网卡搬数据到内存了,自动的,就想到了java的垃圾回收,垃圾回收时有系统波动的,这Proactor有没有?但是,从另一个角度看,如果底层按照一定方式,非常高效地把数据从网卡拷贝到内存,但只是个假设,可能会比上层接到通知后,多次拷贝的效率要高点。从总体上看,有可能减少CPU的使用。

PS:自动的从网卡到内存搬运数据,虽然可能效能好,但是如上文所说,线程池本身就是个天然的漏斗,而如果这样理解,网卡的缓存区是不是也是一个天然的漏斗的特性?

Table of Contents