论坛首页 入门技术论坛

关于Java NIO的一些看法

浏览 12044 次
该帖已经被评为新手帖
作者 正文
   发表时间:2011-07-02  
以下看法多数只是个人的好恶,而不是正确或错误。

  最初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也适应不了,干脆直接踢掉它。













   发表时间:2011-07-02  
既然是个人好恶,就不应该发在论坛,放在个人blog更合适。
0 请登录后投票
   发表时间:2011-07-02  
首先有几个疑问
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
0 请登录后投票
   发表时间:2011-07-02  
哎,你在局域网使用bio会有什么问题呢,真是的网络场景下还是有区别的,这样nio会强很多的
0 请登录后投票
   发表时间:2011-07-02  
rain2005 写道
哎,你在局域网使用bio会有什么问题呢,真是的网络场景下还是有区别的,这样nio会强很多的

强在哪里?而且是局域网里不存在的?能具体说明一下吗?
0 请登录后投票
   发表时间:2011-07-02  
dennis_zane 写道
首先有几个疑问
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

1/3:我用的WindowsAPI既不是select也不是iocp,是WSASyncSelect。这也是我的一些看法不太正确的原因。
2/4:select确实是水平触发的,需要重复注册/取消。我的理解确实有不正确的地方,谢谢你的指正。
4:我对系统触发FD_READ/FD_WRITE的机制的描述应该还是正确的。我理解错误的地方在被触发后事件不会自动复位,JavaNIO没有改变这一规则,确实是继承了这一特性。

但频繁的注册/取消不会影响性能?我不这么认为,在JavaNIO的层次上是一定会影响性能的,我后面的实验结果很明显的反应了这一现象。如果用C/C++频繁地注册/取消不会影响性能,那JavaNIO在这点上需要优化。如果这是select的问题,那这个问题确实无法避免。程序运行在CPU内核上,那里可没有线程的概念,无论是单线程还是多线程,不停的被打断,然后做低效率的数组遍历动作,不可能不影响性能。尤其是单线程,那会影响到所有管理的连接。
你可以想象select/poll的过程,每一次注册就必需打断当前的select/poll,然后重新构建数组,以及每一次有时间触发到来后,你经常需要遍历整个数组以查找有哪些socket的哪些事件被触发,而这个遍历数量和实际被触发的Socket数量的比例会很大,这可以肯定是个低效的算法,只能用在数组不大的场合。Windows里fd_set的结构Socket个数被限制最多64个,不知道其他系统这个数值是多少。当然你可能可以强制改变这个限制,但这样做不会给你带来好处。Windows自己另外有一套异步机制,可以做到只通知和捕获单个被触发的Socket,而不必遍历查找和重新构建数组,可以只注册一次事件监听,不需要重复的注册、取消。如果select/poll没有效率问题的话,Windows的这套机制岂不是多次一举?而我的测试也证明了不同机制在管理大量连接的明显差别。

如果说select/poll确实存在这样的问题,那JavaNIO更是应该注意在这方面的优化,就算不改变这个机制,也不要使情况变得更恶劣。

其他:
1:FD_READ问题,这里没有问题,我在文章中也提到了是可以注销FD_READ的。但这样做问题还是集中在频繁注册上。
2、频繁地注册取消事件,效率并不会很差,我不知道你有没有发现,这些nio框架其实都是单线程地做这个事情,就是为了规避锁的开销。
   效率差不差我已经讲很多了,单线程问题上面也有解释。锁开销?不用锁就效率高?这跟许多人认为只要是Concurrent Nono-Blocking就效率高,只要是锁粒度越小就效率越高一样。普遍的经验而已,不是绝对的事情。具体问题一定要具体分析。锁竞争和线程切换是需要有开销的,但你用不用锁是不同的算法和机制。selector的这种机制跟用不用锁没有什么直接关系,它的效率问题也不在这里。
   我这文章提到的测试结果也表明,无论是BIO还是Windows异步机制,都比JavaNIO/select机制在效率高很多。BIO在线程允许的情况下有最高的性能和最低的CPU使用率,其次是Windows异步机制。BIO使用几百个线程就一定效率低?Selector单线程不用锁就一定效率高?实际上selector在单线程里不停的循环遍历所消耗的CPU资源已经远远超过了系统切换BIO的几百个线程的消耗。

3、没有socket.setSoTimeout,很简单啊,非阻塞IO,读不了就返回,根本不会阻塞,哪来的超时概念
    服务器环境管理大量连接时必需要做的事情就是处理那些长时间不用、网络出现异常的连接。而这里必需存在超时的概念。读不了就返回,你如何控制长时间不用的连接?写不了就返回?网络出现异常写的数据总是发送不出去怎么办?还有你也不要完全信任系统,在压力非常大的情况下,服务器端可能会存在长时间不会关闭客户端已经关闭的连接的现象,在我的压力测试环境中就经常出现这种现象,服务器必需自己清理这些不管是正常还是非正常的长时间没有读写响应的连接。
4、ByteBuffer的使用,我个人觉的很方便,况且你在用DataInputStream之类本质上也是在内部做缓冲,但是ByteBuffer给你更大控制权。
   这是我们不同的看法,不是什么正确和错误的问题。
   InputStream/OutputStream机制怎么用缓冲、在哪个层次上用缓冲,是完全可控的,比JavaNIO灵活得多,而且过滤、缓冲、转换、处理、压缩、加密都非常简单。建议你多用用BIO,你会喜欢上它的。

6、java的序列化慢跟FileMappping没啥关系,FileMapping也没有所谓不能gc的bug,只是gc不可控
   我可没说FileMapping跟Java序列化有直接关系,而是说通过DirectBuffer操作FileMapping和Java序列化有着同样需要一个数据一个数据读写的效率问题。
   既然你也承认gc不可控,那是不是bug并不是正确或错误的问题。具体说FileMapping的问题在于,在Java编程中,涉及到物理资源(文件资源)应该在不用的时候立即close,而系统提供的close支持也必需立即释放物理资源。如果FileMapping相关的所有资源都close了,文件还是没有被正常关闭,你说算不算bug?
7、web服务器的问题,在nio之前,resin就是你说那样干的。既然有了nio,很多app server就希望用纯java实现,jni的问题也不少。
   什么叫纯java?不用JNI?不可能!JDK/JRE里用JNI支持的功能就是纯java,第三方开发的,哪怕是各个平台都支持的也不是。在这点上我跟你的观点一样。我的意思说在Web服务器上需要JavaNIO的功能不多,我说的那几百行代码的功能第三方做不行,最好还是JDK/JRE自己提供。
8、java nio写的服务器,支撑百万连接的案例早就有了,看这里 http://www.dbanotes.net/arch/c10k_c500k.html
   很佩服能做到这样的成绩。不过这跟我的观点正确错误没有关系。
你给我的连接里提到一些具体的应用场景,我们来分析一下,其实这种高性能的框架应该做到尽量简单的框架,然后最求每个细节的性能。所以我们不是很难分析其框架,或者说我们分析的结果不会跟事实相差太远。
   首先,做这种极致性能的系统先要抛开JDK或是什么第三方提供的所谓控件或工具类,每个细节都要自己定义。
   管理这么大的连接一会用异步网络IO,而JavaNIO是JDK唯一支持的机制,别无选择。这不是谁选择JavaNIO的问题,而是他被JavaNIO强制选择了。
   “46.5万 Connections, 两块网卡, 1.5G 输出。10万请求处理每秒,每个响应 2k 左右”这些事相对具体的数据,
   46.5万 Connections,远远超过了正常的系统限制,实际环境只是简单地改变了连接限制,还是有其他的系统定制,不得而知,不过要做到极致的性能,我更倾向于后者。
    两块网卡、1.5G输出,这是很重要的硬件资源,网卡不行、带宽不够,啥都没用。这跟JavaNIO没关系。
    每秒处理10万个请求,这个跟他的后台服务系统有很大关系。我为公司做的另外一套系统的网络驱动,在只是简单测试网络响应的情况下,在我的开发用的i5PC机上就可以达到每秒60万请求/应答,而且同时支持BIO和JavaNIO,两种驱动性能接近。不过这没有意义,实际应用每秒最多一万多个,处理请求和产生应答主要还是看后台服务系统,这个限制的瓶颈很可能不在网络IO上。
   每个响应2k左右(网页中提到是HTTP协议)。这个就比较含糊了,请求数据有多大?这个才是关键,因为应答数据已经很少涉及系统调度机制了,而且只有2K左右,而且是被选择接收处理的连接,这2K数据就是最后发送到网络上的事情(大部分情况下还是一次发送成功)。那么请求数据就不一样了,如果追求性能的话,在内存足够的情况下一定会先读到内存在缓存,而不会把它们阻塞在系统缓存或是网络中。而且如果请求数据不大的话(应该也不会超过2K,就算是50万个连接都有请求的话,1G左右的内存可以满足,HTTP请求尤其是用于手机的,不会有连续请求,或者有连续请求也可以不处理),先把这些请求读进来,同时做缓存和解析。
   缓存和解析在哪里做?很大可能是在Selector线程里做,至少是请求数据合并、分割是会在Selector里做的,解析工作可能交给服务调度在处理服务时再解析,也可能是在Selector线程里解析完了再提交给服务调度。由于HTTP请求解析工作很简单(用于手机的定制化服务),在哪里做都可能。在Selector里做的好处是可以立即释放缓存Buffer。
   现在的重点也可能是实际系统的优化重点就在这个Selector线程上了。我们假设它用的就是JavaNIO的Selector。这里必需说明它的处理机制和我说的Web服务器的处理机制有个本质的区别,即请求数据可以全部读取完再提交给服务调度,而Web服务器最多只能解析完HTTP Header,然后需要转成同步模式再交给Sevlet容器,我的测试程序的JavaNIO也在Selector线程里不读数据,或不读完数据。在Selector里读完数据意味可以不用重复注册FD_READ事件监听,也就正好回避了提到的频繁注册/取消事件监听会影响性能的问题。还有就是这个2K数据大小(即使不是全部或是严格的限制,少部分超过限制也不会影响性能)也很重要,这意味着大部分情况下读一次可以读完全部的请求,写一次不会写到缓冲区满,也就不需要注册FD_WRITE事件监听。影响性能的地方它都有效的回避了,因此在这种特定的系统环境下,使用这种方式可以用一个Selector管理更大量的连接。
   有多少个Selector线程?每个Selector线程被设计成最多管理多少连接?这是个需要实际验证的数据,所有所谓经验值都只能用于最初的设定,然后一定会在实际运行环境中调整到最优的。过大的连接数以及它们不断的连接和关闭,也会打断Selector并重构数组,这也是一个影响Selector效率的地方。Processor数量+1?那意味着一个Selector管理超过10万个连接,那是绝对不可能的。
   另外,数十万个连接不断第创建和关闭,普通的系统配置可能是受不了的,这跟应用程序无关。该系统一定会在快速创建和关闭连接上做了定制化工作。


那个Urban Airship 是做手机消息 Push 服务的(Android Push 架构),这个系统跟杨建的那个系统应该都很类似,是定制化的系统。和我对Web服务器的普遍应用场景的观点没有直接关系。

就这样的系统,我也有我自己的感想,能够有机会接触并实践这样的应用环境很重要。像我们这样只是看书本或在家里、公司的局域网里研究网络,很多细节都不了解,永远不可能做出(或者说没这个机会)这样的系统的。
0 请登录后投票
   发表时间:2011-07-02  
我一直以为nio是net io
0 请登录后投票
   发表时间:2011-07-02  
taolei0628 写道


但频繁的注册/取消不会影响性能?我不这么认为,在JavaNIO的层次上是一定会影响性能的,我后面的实验结果很明显的反应了这一现象。如果用C/C++频繁地注册/取消不会影响性能,那JavaNIO在这点上需要优化。如果这是select的问题,那这个问题确实无法避免。程序运行在CPU内核上,那里可没有线程的概念,无论是单线程还是多线程,不停的被打断,然后做低效率的数组遍历动作,不可能不影响性能。尤其是单线程,那会影响到所有管理的连接。
你可以想象select/poll的过程,每一次注册就必需打断当前的select/poll,然后重新构建数组,以及每一次有时间触发到来后,你经常需要遍历整个数组以查找有哪些socket的哪些事件被触发,而这个遍历数量和实际被触发的Socket数量的比例会很大,这可以肯定是个低效的算法,只能用在数组不大的场合。Windows里fd_set的结构Socket个数被限制最多64个,不知道其他系统这个数值是多少。当然你可能可以强制改变这个限制,但这样做不会给你带来好处。Windows自己另外有一套异步机制,可以做到只通知和捕获单个被触发的Socket,而不必遍历查找和重新构建数组,可以只注册一次事件监听,不需要重复的注册、取消。如果select/poll没有效率问题的话,Windows的这套机制岂不是多次一举?而我的测试也证明了不同机制在管理大量连接的明显差别。


你不这么认为,影响不了事实。select/poll有缺陷,所以在linux上我们用epoll。

taolei0628 写道

其他:
1:FD_READ问题,这里没有问题,我在文章中也提到了是可以注销FD_READ的。但这样做问题还是集中在频繁注册上。
2、频繁地注册取消事件,效率并不会很差,我不知道你有没有发现,这些nio框架其实都是单线程地做这个事情,就是为了规避锁的开销。
   效率差不差我已经讲很多了,单线程问题上面也有解释。锁开销?不用锁就效率高?这跟许多人认为只要是Concurrent Nono-Blocking就效率高,只要是锁粒度越小就效率越高一样。普遍的经验而已,不是绝对的事情。具体问题一定要具体分析。锁竞争和线程切换是需要有开销的,但你用不用锁是不同的算法和机制。selector的这种机制跟用不用锁没有什么直接关系,它的效率问题也不在这里。
   我这文章提到的测试结果也表明,无论是BIO还是Windows异步机制,都比JavaNIO/select机制在效率高很多。BIO在线程允许的情况下有最高的性能和最低的CPU使用率,其次是Windows异步机制。BIO使用几百个线程就一定效率低?Selector单线程不用锁就一定效率高?实际上selector在单线程里不停的循环遍历所消耗的CPU资源已经远远超过了系统切换BIO的几百个线程的消耗。


仍然是臆测。世上当然无绝对,最根本的你就没理解我在说什么。我说的是什么锁。

taolei0628 写道

3、没有socket.setSoTimeout,很简单啊,非阻塞IO,读不了就返回,根本不会阻塞,哪来的超时概念
    服务器环境管理大量连接时必需要做的事情就是处理那些长时间不用、网络出现异常的连接。而这里必需存在超时的概念。读不了就返回,你如何控制长时间不用的连接?写不了就返回?网络出现异常写的数据总是发送不出去怎么办?还有你也不要完全信任系统,在压力非常大的情况下,服务器端可能会存在长时间不会关闭客户端已经关闭的连接的现象,在我的压力测试环境中就经常出现这种现象,服务器必需自己清理这些不管是正常还是非正常的长时间没有读写响应的连接。

任何一个nio框架都会检测session idle或者timeout,不知道的话,麻烦看看mina和netty的文档先。不知道怎么实现,可以看源码。

taolei0628 写道

4、ByteBuffer的使用,我个人觉的很方便,况且你在用DataInputStream之类本质上也是在内部做缓冲,但是ByteBuffer给你更大控制权。
   这是我们不同的看法,不是什么正确和错误的问题。
   InputStream/OutputStream机制怎么用缓冲、在哪个层次上用缓冲,是完全可控的,比JavaNIO灵活得多,而且过滤、缓冲、转换、处理、压缩、加密都非常简单。建议你多用用BIO,你会喜欢上它的。

不评论。

taolei0628 写道

6、java的序列化慢跟FileMappping没啥关系,FileMapping也没有所谓不能gc的bug,只是gc不可控
   我可没说FileMapping跟Java序列化有直接关系,而是说通过DirectBuffer操作FileMapping和Java序列化有着同样需要一个数据一个数据读写的效率问题。
   既然你也承认gc不可控,那是不是bug并不是正确或错误的问题。具体说FileMapping的问题在于,在Java编程中,涉及到物理资源(文件资源)应该在不用的时候立即close,而系统提供的close支持也必需立即释放物理资源。如果FileMapping相关的所有资源都close了,文件还是没有被正常关闭,你说算不算bug?


MappedByteBuffer确实有缺陷,这个没啥好说,通常只建议做长期存在的只读数据处理。

taolei0628 写道

7、web服务器的问题,在nio之前,resin就是你说那样干的。既然有了nio,很多app server就希望用纯java实现,jni的问题也不少。
   什么叫纯java?不用JNI?不可能!JDK/JRE里用JNI支持的功能就是纯java,第三方开发的,哪怕是各个平台都支持的也不是。在这点上我跟你的观点一样。我的意思说在Web服务器上需要JavaNIO的功能不多,我说的那几百行代码的功能第三方做不行,最好还是JDK/JRE自己提供。

几百行?你想的也太简单,世界上不是只有windows,处理平台差异和机制差异,不是几百行能搞定的,这也是nio API如此复杂的原因。

taolei0628 写道

8、java nio写的服务器,支撑百万连接的案例早就有了,看这里 http://www.dbanotes.net/arch/c10k_c500k.html
   很佩服能做到这样的成绩。不过这跟我的观点正确错误没有关系。
你给我的连接里提到一些具体的应用场景,我们来分析一下,其实这种高性能的框架应该做到尽量简单的框架,然后最求每个细节的性能。所以我们不是很难分析其框架,或者说我们分析的结果不会跟事实相差太远。
   首先,做这种极致性能的系统先要抛开JDK或是什么第三方提供的所谓控件或工具类,每个细节都要自己定义。
   管理这么大的连接一会用异步网络IO,而JavaNIO是JDK唯一支持的机制,别无选择。这不是谁选择JavaNIO的问题,而是他被JavaNIO强制选择了。
   “46.5万 Connections, 两块网卡, 1.5G 输出。10万请求处理每秒,每个响应 2k 左右”这些事相对具体的数据,
   46.5万 Connections,远远超过了正常的系统限制,实际环境只是简单地改变了连接限制,还是有其他的系统定制,不得而知,不过要做到极致的性能,我更倾向于后者。
    两块网卡、1.5G输出,这是很重要的硬件资源,网卡不行、带宽不够,啥都没用。这跟JavaNIO没关系。
    每秒处理10万个请求,这个跟他的后台服务系统有很大关系。我为公司做的另外一套系统的网络驱动,在只是简单测试网络响应的情况下,在我的开发用的i5PC机上就可以达到每秒60万请求/应答,而且同时支持BIO和JavaNIO,两种驱动性能接近。不过这没有意义,实际应用每秒最多一万多个,处理请求和产生应答主要还是看后台服务系统,这个限制的瓶颈很可能不在网络IO上。
   每个响应2k左右(网页中提到是HTTP协议)。这个就比较含糊了,请求数据有多大?这个才是关键,因为应答数据已经很少涉及系统调度机制了,而且只有2K左右,而且是被选择接收处理的连接,这2K数据就是最后发送到网络上的事情(大部分情况下还是一次发送成功)。那么请求数据就不一样了,如果追求性能的话,在内存足够的情况下一定会先读到内存在缓存,而不会把它们阻塞在系统缓存或是网络中。而且如果请求数据不大的话(应该也不会超过2K,就算是50万个连接都有请求的话,1G左右的内存可以满足,HTTP请求尤其是用于手机的,不会有连续请求,或者有连续请求也可以不处理),先把这些请求读进来,同时做缓存和解析。
   缓存和解析在哪里做?很大可能是在Selector线程里做,至少是请求数据合并、分割是会在Selector里做的,解析工作可能交给服务调度在处理服务时再解析,也可能是在Selector线程里解析完了再提交给服务调度。由于HTTP请求解析工作很简单(用于手机的定制化服务),在哪里做都可能。在Selector里做的好处是可以立即释放缓存Buffer。
   现在的重点也可能是实际系统的优化重点就在这个Selector线程上了。我们假设它用的就是JavaNIO的Selector。这里必需说明它的处理机制和我说的Web服务器的处理机制有个本质的区别,即请求数据可以全部读取完再提交给服务调度,而Web服务器最多只能解析完HTTP Header,然后需要转成同步模式再交给Sevlet容器,我的测试程序的JavaNIO也在Selector线程里不读数据,或不读完数据。在Selector里读完数据意味可以不用重复注册FD_READ事件监听,也就正好回避了提到的频繁注册/取消事件监听会影响性能的问题。还有就是这个2K数据大小(即使不是全部或是严格的限制,少部分超过限制也不会影响性能)也很重要,这意味着大部分情况下读一次可以读完全部的请求,写一次不会写到缓冲区满,也就不需要注册FD_WRITE事件监听。影响性能的地方它都有效的回避了,因此在这种特定的系统环境下,使用这种方式可以用一个Selector管理更大量的连接。
   有多少个Selector线程?每个Selector线程被设计成最多管理多少连接?这是个需要实际验证的数据,所有所谓经验值都只能用于最初的设定,然后一定会在实际运行环境中调整到最优的。过大的连接数以及它们不断的连接和关闭,也会打断Selector并重构数组,这也是一个影响Selector效率的地方。Processor数量+1?那意味着一个Selector管理超过10万个连接,那是绝对不可能的。
   另外,数十万个连接不断第创建和关闭,普通的系统配置可能是受不了的,这跟应用程序无关。该系统一定会在快速创建和关闭连接上做了定制化工作。


那个Urban Airship 是做手机消息 Push 服务的(Android Push 架构),这个系统跟杨建的那个系统应该都很类似,是定制化的系统。和我对Web服务器的普遍应用场景的观点没有直接关系。

就这样的系统,我也有我自己的感想,能够有机会接触并实践这样的应用环境很重要。像我们这样只是看书本或在家里、公司的局域网里研究网络,很多细节都不了解,永远不可能做出(或者说没这个机会)这样的系统的。


哦,我个人写过nio框架,我写的东西应用在国内最大的电子商务网站上,每天经受着考验。
另外,如果你认为“46.5万 Connections,远远超过了正常的系统限制”,那我觉的,我们更没必要讨论了。

0 请登录后投票
   发表时间:2011-07-02   最后修改:2011-07-02
有没有发现?你总是在给我们介绍你的经验和猜测,已经完全偏离了主题。你本文的目的不就是说说nio的缺点,我说明了你大部分提出来的问题,而你一直纠结的其实是nio在windows系统上的底层实现select/poll的缺点,而非nio本身。实现可以改进,nio 2.0都要引入了AIO了,在windows上估计就是IOCP的实现,到时候可以再看看。

如果你的测试数据差距太大的话,我更偏向于代码写的有问题。
0 请登录后投票
   发表时间:2011-07-03  
dennis_zane 写道
有没有发现?你总是在给我们介绍你的经验和猜测,已经完全偏离了主题。你本文的目的不就是说说nio的缺点,我说明了你大部分提出来的问题,而你一直纠结的其实是nio在windows系统上的底层实现select/poll的缺点,而非nio本身。实现可以改进,nio 2.0都要引入了AIO了,在windows上估计就是IOCP的实现,到时候可以再看看。

如果你的测试数据差距太大的话,我更偏向于代码写的有问题。


java 7这部分 在windows应该是用iocp

之前多半不是...连接数一多, cpu 占用率就飙升..

0 请登录后投票
论坛首页 入门技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics