【IO】【NIO】【详细介绍】

 

  最初NIO刚出来的相当一段时间里,我一直以为NIO是None-Blocking IO的意思,直到被同事纠正我的错误:原来是NewIO。NewIO里用的最多的就是异步Socket,其他的东西不是主要的。这里我主要讨论Socket相关的部分。 

    NewIO 真的new吗?在Java还没诞生之前,各个系统的socket API里就都有阻塞和非阻塞的方式。我在用java之前做过Windows下的Socket编程,阻塞和非阻塞方式都用过,而JavaNIO的SocketChannel/Selector机制也没有比传统的非阻塞Socket多什么功能,甚至还要少一些功能,几乎没有太多主要的功能可以配得上new,或者说在我看来JavaNIO里new的都是些鸡肋的东西。 

    对比BIO: 

    BIO相对NIO指的是java.io的InputStream/OutputStream机制。我们在使用Socket的时候绝大多数应用都是用TCP,很少用UDP,我个人也几乎没用过UDP。而且JavaNIO要解决过多线程阻塞的问题,这样的问题在UDP编程里也不存在。 

    我刚接触java.io的时候就非常喜欢InputStream/OutputStream机制,直到现在。TCP上传输的是数据流,而传统的Socket编程即使是同步阻塞的方式对数据流上的数据块进行合并和拆分都需要额外做很多工作。java的InputStream/OutputStream框架很好的解决了这些问题。甚至当时我写Windows程序都学java在Socket上面包装一层Stream。而且在统一的Stream模式下可以透明化得附加各种功能。加上Buffer就可以做到数据缓冲,以提高网络传输性能、加上Deflater(ZIP)就可以压缩、加上SSL就可以加密、加上协议转换就可以支持HTTP,而这些对应用几乎不产生影响。还有一些常用的DataInput/Output、ObjectInput/Output等都给应用带来很大方便。而且使用这些都很简单,多数功能能JDK都有提供,即使是定制开发的协议转换Input/Output也非常容易。真正该称得上NewIO的应该是java BIO! 

    JavaNIO对比我上面提到BIO的诸多好处,JDK很多功能都没有直接提供支持,在其上面开发应用又回归到了原始阶段。数据流要自己拆包、合并。压缩?ObjectInput/Output?HTTP?协议转换?很多东西都没有了,而且自己在上面开发这些内容也比BIO模式下复杂得多。用的比较多的MINA框架很好的解决了这些问题,但大家有没有想过在BIO里本来就没有这些问题的、而且即使用MINA框架,自定义协议转换、过滤器、处理器也要比BIO模式复杂得多。JavaNIO是解决了一些BIO存在的问题,这也是我们不得不用JavaNIO的原因,但JavaNIO相对BIO丢弃的东西就太多了。相对BIO,javaNIO太原始、初级了。 

    下面我再说说JavaNIO的事件机制。 

    在我看来这是JavaNIO最大的问题,JavaNIO的读写事件机制跟传统异步Socket有区别。传统Socket的事件机制,不记得资料出处了,大概意思是触发系统发送读(FD_READ)事件的条件是(1)第一次有数据到来;(2)接收数据遇到无数据可读后,再有数据到来时。触发写事件(FD_WRITE)的条件是(1)Socket成功建立连接后;(2)发送数据遇到缓冲区满之后,下一次缓冲区可写时。FD_READ/FD_WRITE事件只在满足条件后发送一次,如果一直有数据可读,就不会再发送FD_READ,如果发送缓冲区一直未满,就不会再发送FD_WRITE。这种机制下只需要一次性注册FD_READ和FD_WRITE事件监听,这是跟JavaNIO最大的区别。 

    这种机制有个bug,会偶尔发生收到FD_READ事件而无数据可读、收到FD_WRITE事件而缓冲区还是满的情况,不过这个bug不影响应用程序正常运行。因为收到FD_READ后应用程序会尝试接收数据,而收不到数据会使系统触发下一个FD_READ事件。发送数据的情况基本相同。 

    而在JavaNIO的Selector机制中,只要有数据可读、可写,Selector的select方法就不会等待,总是通知应用读写数据。这使得应用程序要么把数据全部读出、写数据直到缓冲区满,要么取消FD_READ/FD_WRITE事件监听。我一直没有搞明白为什么JavaNIO要跟传统异步Socket事件机制不一样,难道就是为了这个New字头,自作聪明地造出一些与众不同?!,下面我说说JavaNIO这种机制的带来的麻烦。 

    一直通知FD_READ的麻烦:一旦注册FD_READ事件监听后,应用程序就必需把到来的数据接收完或者取消监听,否则Selector不会等待,陷入不停的循环。服务器开发并不是一味追求性能和网络吞吐,尤其不能只是在Socket这一层次上做这些。在大规模并发的情况下服务器经常会没有能力处理太多的请求,几乎所有的JavaNIO框架都会一直监听FD_READ事件,并把网上上到来的请求数据接收完,这使得JavaNIO框架程序会消耗大量的内存资源以缓存收到的数据。而传统异步Socket和BIO不会有这种问题,在Socket层次上适当的阻塞以减缓服务器压力、平衡网络性能是必要的。另外一种解决方法是重复的注册/取消FD_READ事件监听,这种方式很少有框架在READ中使用,带来的问题下面会讲。 

    一直通知FD_WRITE的麻烦:这个更要命,没数据要发送怎么办?传统异步Socket不用理会,可以一次性注册FD_WRITE事件监听,因为系统不会重复发送FD_WRITE事件。在JavaNIO里就要另想办法了,方法是发送数据一直到缓冲区满不能连续发送后再注册FD_WRITE监听,当数据发送完毕之后要立即注销FD_WRITE事件监听。JavaNIO的Selector是非线程安全的、里面保存事件监听注册的SelectionKey是个Set,如果连接数量很多时,频繁注册/取消事件监听的效率会非常差,甚至会低到比传统异步Socket性能慢好数倍的程度。即使是系统SocketAPI,也受不了频繁的注册/取消事件监听,不过不需要这样做。 

    在网上看到有文章说MINA框架建议Selector的数量最好是处理器内核数量+1,这个不敢苟同。个人认为JavaNIO不能管理过多的连接是需要频繁注册/取消事件监听的原因,一个Selector能管理的连接数量跟应用数据特点和系统压力有关,跟处理器数量并无直接关系,在大量连接、大量请求的压力下,几个Selector数量根本不够用。 

    真实的对比实验来了,我自己做个简单的支持RPC的NIO(以下的NIO表示None-Blocking IO,而不是NewIO)并发框架,通讯上分别支持进程内驱动、BIO驱动、自己开发的JavaNIO驱动、自己用JNI开发的Win32平台下的NIO驱动(抱歉我本人只会在Win32平台写程序)、用MINA框架开发的NIO驱动。以下分别简称BIO、JavaNIO、Win32NIO、MINA。测试内容和测试环境可能有些片面,但应该能说明关键问题(我上面提到的问题). 测试的内容一律是小数据压力测试,分别有不同的连接数量和并发数量不停的访问服务器,单个访问是简单RPC机制,同步的请求/应答,即客户端发送完请求后同步等待服务器应答,而服务器端只经过简单的处理产生应答数据。下面没有集体的量化数值,只是针对我上面提到的问题说明不同方式下的差别。如果有读者存在质疑,欢迎批评指正。 

    进程内驱动:在这里提到进程内驱动是要说明这个测试的性能瓶颈是在网络传输部分,而其他部分对性能影响很小。为了说明问题,进程内测试也使用Java序列化。从单个线程客户端到数百的并发线程客户端,服务器端(同一进程内)每秒处理的请求都在数十万甚至百万级别以上。 

    网络测试:分别在Windows-Windows,Windows-Linux,Linux-Windows客户端-服务器平台进行对比测试,Linux上不能运行我的JNI驱动,线程启动有点慢,其他方面和Windows平台上的程序表现没有什么差别。客户端和服务器分别使用不同的(可以支持的)驱动。系统内核数量分别有4核8线程、双核、双核(虚拟linux)、单核(虚拟linux)几种情况,测试总体表现跟内核数量关系不大(指的是不同驱动对比),网络是100M局域网(家里没1000M的),分别测试了局域网和同一台机器上、同一台机器上的Windows系统和VMWare虚拟Linux.客户端并发测试需要并发线程、BIO每个连接需要一个接收线程,服务器请求服务处理有单独的线程池做并发控制,在NIO模式下使用,两个到十几个线程不等,BIO不用。由于测试的服务很简单,服务器端使用两个线程并发服务就可以达到最佳性能。 

   相对少量连接、低并发、BIO驱动能够允许的情况下,无论是单连接单线程、单连接多并发、还是多连接多并发,连接和并发数量可以达到几百个,BIO驱动的性能最好,其他几个驱动在性能表现上也比较接近BIO,所有测试NIO的几个驱动在参数理想的情况下性能表现也比较接近,而Win32NIO在连接量不大的情况下,也不比JavaNIO和MINA快。但是,BIO的CPU使用率最低,而且比其他驱动低很多,Win32NIO其次,JavaNIO和MINA最耗CPU。这只是专门的网络测试,要知道实际应用中过多的消耗CPU会影响服务器处理业务的能力。在相同的、能够允许的条件下,BIO总是消耗最少的CPU资源,这又是我喜欢BIO的地方。 实际上所谓的少量的连接,也达到了数百的连接(500个连接以上),除了Web服务器外,其他大多数服务器设计的并发连接数通常在两三百以内,BIO完全可以满足,而且消耗更少的CPU、开发、维护简单、系统也会相对的更加稳定可靠,除了BIO占用了两三百个线程。而且我个人观点认为现在的服务器系统多出两三百的线程完全没有问题,况且这只是在连接空闲的情况下,处理客户端请求同样需要一些并发线程,实际在压力大的情况下和NIO模式下的线程使用差别没有那么多了,而空闲的情况下?服务器闲着没事,理会多出几百个线程完全是蛋疼的问题。(Web服务器可能会挂着数千个连接,我后面会提到我的一些看法)。在我看来,多数情况下大家研究和使用NIO、MINA框架等应该更多的把范围限定在学习、研究、对比上,而实际应用尽量使用BIO,可能许多java开发人员对JavaNIO有些迷信,或者说对线程数量问题有些迷信,虽然线程问题现在依然是个问题,但已经比过去好很多了(在Windows系统上线程也不是什么大问题,不过在这里我说Windows好可能会被踩的)。况且线程问题应该是操作系统的问题,把这个问题转嫁到应用开发人员上真的很无奈。 

    现在开始要把BIO排除在外了,由于线程问题,BIO无法满足连接数量、并发数量更大的情况,这也是我们不得不使用JavaNIO的原因。在JavaNIO/MINA上有个重要的参数是每个Selector线程管理的连接数量,在我自己写的Win32NIO驱动上也同样支持了这个参数,可实际测试结果连我自己都感到意外(这也是我写这篇文章的原因之一)。 

    直接说一万个连接,客户端建立一万个Socket连接,同时建立100-200个并发线程轮流在这一万个连接上访问服务器。 

    JavaNIO和MINA:每个Selector线程在管理100个以内的连接性能最佳,管理200个连接以内会下降一些,差距不大。每个Selector线程在管理超过100个连接以上时性能会逐渐下降、CPU使用率也逐渐上升。超过一千个以上时已经比最佳性能成倍的慢。在单机环境下,最佳性能可以达到每秒接近两万个左右,无论是我写的JavaNIO驱动还是MINA,在每个Selector线程管理超过一千连接时性能已经下降到每秒几千个访问。在100M网络环境下,每秒六七千个访问,同样也是每个Selector管理超过100个连接后逐渐下降,到单个Selector管理所有连接,已经下降到了不到一千个,此时CPU使用率已经到了100%,而在最佳性能的时候CPU使用率都在80%以下(不同的机器和系统不同,分别有两个台式机、一个笔记本及其在一台PC下的虚拟Linux)。 

    Win32NIO:使用WindowsAPI的异步Socket机制,单个Selector线程就可以管理一万个连接,而且跟多个线程在性能上没有差别。在同一台机器上的测试性能更是达到了接近每秒三万个的水平,在局域网络环境下慢很多,比JavaNIO和MINA快一点,有八千左右。数据很漂亮,服务器环境下只有4个系统线程、一个Selector线程、一个超时检测线程和两个服务线程。 

    前面说过在连接数不大的情况下Win32NIO的性能不比JavaNIO/MINA快,现在问题就集中在Selector线程数量及其管理连接数量上,我上面说到的频繁注册/取消事件监听就是我得出的结论。实际上我写的Win32NIO也只是用JNI写的驱动,应用程序同样运行在JVM上,如果JavaNIO使用跟传统异步Socket同样的机制,一样可以做到一个Selector管理一万个连接。对JavaNIO的这个机制我再次表示纠结,JavaNIO同样也是访问的系统SocketAPI,为什么不简单的保持同样的机制,就算是在某些系统存在兼容性问题(好像没有,我看到的资料介绍的异步Socket的事件通知机制是各个系统都支持的),也可以在Selector.select方法上屏蔽掉重复的通知,性能也会比现在机制好得多。 

 

    和BIO对比和JavaNIO的事件机制是我主要想说的问题,下面再说几个关于JavaNIO的其他的一些看法。 

    读写超时:BIO里有read/write/connect超时机制,JavaNIO不管,Socket.setSoTimeout无效,你要自己想办法。MINA框架会专门提供SessionTimeout机制。 

    ByteBuffer:JavaNIO并未提供ByteBuffer的缓存功能,设计上用来缓存的东西不支持完整。而且ByteBuffer里面好多方法在package外无法访问,不方便继承。我写的程序自己用到buffer缓存的地方都自己实现, 

好像不用JavaNIO的情况下没人会用ByteBuffer的。我个人认为SocketChannel使用ByteBuffer就是个鸡肋,而且还限制了别人使用SocketChannel的时候必须把自己的数据wrap成ByteBuffer,又多了一次转换。实际在压力测试中如果频繁的wrap ByteBuffer也会影响性能,我还得定义个变量为了自己的buffer保存wrap的Byteuffer。ByteBuffer上提供的各种数据类型的读写方法(asXXXBuffer)我几乎不用,比BIO的DataInput/DataOutput难用的多,相反我在使用BIO的InputStream/OutputStream时经常套上一层DataInputStream/DataOutputStream,即方便,也几乎不影响性能。MINA框架提供了了ByteBuffer缓存,而且还在外面封装了一些其他功能。 

    DirectBuffer:在SocketChannel上它几乎又是个鸡肋。网络上许多文章都表达的同样的观点,DirectBuffer实际对IO性能并没有显著的提高,同时它又脱离了JVM内存管理之外,仅靠PhantomReference机制在gc时把它释放。给服务器系统带来内存管理的隐患。虽然DirectBuffer在直接用于SocketChannel时会有比较高的效率,但应用程序频繁读写DirectBuffer又会慢很多,如果想提高效率,应用程序还要有自己管理的buffer缓存数据,然后一次性读写DirectBuffer。既然应用程序有自己的Buffer,为什么还要先写到DirectBuffer上再写SocketChannel,那不是多此一举吗?干吗不直接写SocketChannel。我看DirectBuffer大概只适用于Socket代理程序,收到的数据原封不动地再转发出去。 

    FileMapping:在操作系统上FileMapping这个东西可是个高性能的好东西。直接把文件区块映射到内存中,应用程序可以直接访问内存来读写文件数据。本人也一直想用Java的FileMapping写一个数据存储引擎,可惜一直没有动手,纠结的原因是如果用C/C++或是其他语言,可以直接用指针读写内存,java不可以,只能通过DirectBuffer。更重要的原因是C/C++可以在连续的内存上定义数据结构,可以一次性的读写、复制大量数据,Java不能,要用DirectBuffer一个数据一个数据的读写,实在是太不方便、效率太低了。Java的序列化同样就是慢在这里。还有就是据说FileMapping还有在关闭后不能立即释放的bug。 

    Web服务器,在J2EE6(不包括J2EE6)以前,Servlet是单线程同步的,应用服务器使用JavaNIO只是挂在那里等待HTTP请求到来,并解析HTTP Header,等HTTPHeader完全收到后,要把异步Socket转成同步的InputStream/OutputStream在交给Servlet容器处理HTTP请求,请求完成后如果Keep-Alive,再改成异步模式挂在Selector上。应用服务器的HTTP Header最多只允许几K的数据,这意味着HTTP请求到来后几乎可以立即收到HTTP Header。实际上JavaNIO Selector承担的任务只是把空闲的连接挂在那里,当然现存的JDK除了JavaNIO的Selector外还没有其他方式可以做到。但反过来说,Web服务器用JavaNIO就只用了这一小点功能,搞那么大一个JavaNIO框架完全没必要。只需要几百行甚至更少的C/C++代码做个JNI就可以支持这个功能。我的Win32NIO的驱动程序C++代码也只有300行,还包括空行和注释,还有Socket非阻塞读写功能,如果把Socket非阻塞读写功能去掉的话会更少(我的Win32NIO直接把BIO Socket改成异步模式,如果不用非阻塞读写的话,可以在取消异步模式后当普通的BIO Socket一样用)。也就是说,只需要几百行代码就可以让BIO的Socket在空闲的时候挂在一个Selector上,而不单独占用线程。 

    J2EE6里增加了异步Servlet和异步EJB的机制,异步EJB的机制完全可以不用在Socket层次上提供支持。通常异步功能只用在最外层的客户端请求应答,EJB如果再嵌套异步EJB的话,意味着需要挂起当前事务,而挂起本地事务去等待一个既不可靠、又耗时的远程应答,这是非常忌讳的做法。如果一定要需要异步Socket的话,也不需要非阻塞读写,支持方法和支持异步Servlet一样。 支持异步Servlet可能需要异步Socket,但本人对异步Servlet这个机制并不感冒,这个机制对Web页面生成没什么用处,倒是对WebService或是类似的用于通过HTTP协议做代理转发的服务有些用处,但同样不需要非阻塞读写,只需要在转发完请求后,等待应答的时间内把Socket挂在Selector上就行了。如果有哪个应用客户端那么不靠谱的写写停停地折腾Socket,JavaNIO和异步Servlet也适应不了,干脆直接踢掉它。 

 

 

首先有几个疑问

1、用的jdk版本是多少?windows上早期nio的实现是基于select,后来改成了poll。

2、其次,select和poll都是水平触发,你自己写的windows nio难道不是基于这两个调用?还是说是用iocp?

3、如果用IOCP,那就无须讨论了,那是AIO。如果不是,那么水平触发都是每个IO事件一直通知,这跟java有何两样?还是你有特殊处理

4、既然使用select/poll,也同样是水平触发,难道你不需要FD_SET之类来修改fd set,这不就是所谓注册或者取消事件,谁能避免?

 

其实nio的逻辑跟select/poll的使用模型是完全一致的,但是为了屏蔽平台之间和各种poll机制的差异,搞的API比较恶心和复杂,也有很多陷阱。

 

文中还有一些错误的地方:

1、可读数据,你完全可以不读,取消注册OP_READ就可以做到输入的流量限制。我看到的nio框架也都是在读之前取消注册,读完之后继续注册。写也是一样,只在有可写数据的时候才注册OP_WRITE,否则不要。你可以认为这套机制复杂,但是你不能认为解决不了。况且你在使用原生select/poll时也是遇到同样的问题。

2、频繁地注册取消事件,效率并不会很差,我不知道你有没有发现,这些nio框架其实都是单线程地做这个事情,就是为了规避锁的开销。

3、没有socket.setSoTimeout,很简单啊,非阻塞IO,读不了就返回,根本不会阻塞,哪来的超时概念

4、ByteBuffer的使用,我个人觉的很方便,况且你在用DataInputStream之类本质上也是在内部做缓冲,但是ByteBuffer给你更大控制权。

5、DirectByteBuffer的确是个问题,通常来说也不建议用。

6、java的序列化慢跟FileMappping没啥关系,FileMapping也没有所谓不能gc的bug,只是gc不可控。

7、web服务器的问题,在nio之前,resin就是你说那样干的。既然有了nio,很多app server就希望用纯java实现,jni的问题也不少。

8、java nio写的服务器,支撑百万连接的案例早就有了,看这里 http://www.dbanotes.net/arch/c10k_c500k.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章