微信号:gh_7b30a45c1595

介绍:致力于营造国内一流的应用服务器的爱好者技术讨论,服务器相关资讯新闻分享,业界开源商用服务器相关动态,服务器端程序员职业发展相关的圈子; 以JAVA技术为基础扩展其它语言,讨论tomcat,JAVA EE规范,JVM,操...

聊聊Tomcat中的NIO2通道

2016-09-28 11:48 feiying

前面一周,我们一直再谈NIO2,那么结合Tomcat,我们来看看Tomcat中的是怎么使用NIO2的;

基于Java Nio.2的新特性,Tomcat8搞出了一个NIO2的通道,这个通道利用NIO2中的最重要的特性,异步IO的java API;

从性能角度上来说,从纸面上该IO模型是非常优秀的,这也是很多书籍推崇的最优秀的IO模型,例如Unix网络编程这本圣经,但取决于目前操作系统的支持程度和环境,还有业务逻辑代码的编写,NIO2的程序调用并不一定比NIO,甚至与BIO的效率要高;

我们在没有实测的情况之下,本文从源码的角度去分析一下Tomcat8中的这个NIO2通道,后续在相应的文章中,我们会进一步的分析一下Tomcat的4个通道的性能差异;


1.NIO2的框图

前面我们已经了解了Tomcat的BIO,NIO,APR这三个通道,对于NIO2的通道框图大体上和这些没有太大的区别:

和其他通道一样,Tomcat最前端工作的依然是Endpoint类中的Acceptor线程,该线程主要任务是接收socket包,简单解析并封装socket,对其进行包装为SocketWrapper后,交给工作线程;


在NIO2的通道下,Acceptor线程结束之后,并不会直接调用工作线程也就是SocketProcessor,而是利用NIO2的机制,CompleteHandler完成处理器去去处理任务:

processSocket0的方法就是开启工作线程,初始化一个SocketProcessor处理类,然后调用Tomcat的线程池一个线程进行执行:

上述的流程是socket接收和读取的NIO2的流程,正是利用了NIO2的CompleteHandler完成处理器这一个特性;


再对比NIO,BIO两个通道:

我不用像BIO通道那样去拿着SockerWrapper在工作线程进行阻塞读,这样工作线程中的时间会占据网络IO读取的时间,导致大并发模式下工作线程暴涨,这也就是经常我们看到很多cpu为什么被占到99%的原因,再怎么设置工作线程无济于事,因为大量的cpu线程切换太耗时间了;


NIO通道采用Reactor的模式去做这个事,Selector承担了多路分离器这个角色,对于BIO是一大改进,其次java NIO的牛B之处就是操作系统内核缓冲区的就绪通知,如下图所示:

当内核缓冲区就绪以后,Selector得到这个通知后,将关注的SelectKey值取出来,并与客户端的感兴趣的事件进行匹配,如果发现是客户端感兴趣的事件,才会执行读的操作;

诚然,在Tomcat的NIO通道中,工作线程进行http解析的时候,也需要socketChannel.read,但实质上这个read省去了网络IO漫长的等待过程,内核Buffer已有数据的情况下的读,基本不怎么耗时,这也是比BIO通道的一大改善;

NIO中,只有读取http header的时候,进行socketChannel.read操作,而对于http body的读取,Servlet流的写入等还是采用BIO这种读写方式,因为这个是Servlet的规范要求的,可以看看下面的表格对比:

NIO通道中完全可以做到,全部都是非阻塞读的;


对于NIO2来说,上图的阻塞和非阻塞和NIO是一样的,只不过NIO2采用的是Proactor模型,可以这么来理解:


Tomcat作为客户端,调用的是AsynchornSocketChannel进行异步操作,自定义的CompleteHandler作为完成事件被调用;

对于Proactor和上图Asynchrounous Operation Processor的实现在java层,也就是API和API的实现(如sun.xxx包中);

而上图中的事件队列,内核态的事件分离器,这些都是和不同操作系统实现不同的


通过上述,我们得知3件事:

1.NIO2这种纯异步IO,必须要有操作系统支持,并且性能和这个内核态的事件分离器有着非常大的关系;


2.对于内核分离器通知CompleteHandler的时机是什么,对比NIO的缓冲区,实质是当内核态缓冲区的数据已经复制到用户态缓冲区时候,这个时候触发CompleteHandler,这相当于比NIO的模式更进一步:


NIO只是内核缓冲区就绪才告诉客户端去读,这个时候用户态缓冲区是空的,你得执行完socketChannel.read之后,用户态缓冲区才会填满;


3.因为NIO2的优势,事件分离器分离器实际是在操作系统内核态的功能,所以不需要用户态搞一个Selector做事件分发,因此,对比NIO的通道框图,可以看到缺少了Poller线程这一个环节;


2.异步IO的利用

从代码的角度来看看,Tomcat的NIO2的通道,主要集中在NIO2Endpoint这个类的bind方法:



关注2点:

1.AsynchronousChannelGroup是异步通道线程组,通过这个类可以给AsynchronousChannel定义线程池的环境,上述代码中,ExecutorService是Tomcat中的特有的线程池:


TaskQueue是队列,Thread工厂针对于创建的线程名称进行了一下修改,并且对于线程池的最大,最小,时间都进行了限定;

这个线程池在BIO,NIO通道中也是这个,都是一样的;

定义完AsynchronousChannelGroup的通道线程组,AsynchronousChannel的read就是运行在通道组中的线程组中,包括从操作系统的内核态多路分离器响应的CompleteHandler,也是从该线程池中取出线程进行运行,这个是很重要的,如果每一次都new Thread的话,会有很大的消耗,所以不如都放在一个线程组中随取随用,用完再还;


2.随即开启 AsynchronousChannel通道,并绑定到对应的端口中,这个API使用的就是JAVA NIO2的API;


之后,Acceptor线程获得socket包,直接进行包装为SocketWrapper,之后的流程如第一节中的源码分析一样,随着读取的执行,异步操作就执行完了,转而Acceptor线程进行下一个循环,读取新socket包

这时候需要注意的是,在NIO模式下,这个时刻是将SocketWrapper扔给Poller线程,Poller线程中的Selector去轮询key值,而不是NIO2这种的直接就不管不问了,从这一点上也可以看出,NIO2的异步优势就在这,事件触发的机制直接由内核通知,我搞一个CompleteHandler就行,无需在用户态轮询;


上面分析的是Tomcat前端读取的部分,对于写入也是一样,以ServletOutPutStream为例:


对于ServletOutPutStream来说,从Servlet的规范角度就是阻塞写的,上述代码中block是默认的,在阻塞写的过程中,也使用了AsynchronousChannel进行write,只不过这个write之后获得的future,直接就get阻塞住,一直等到数据写完为止;

另一个条件分支,采用的是CompleteHandler的异步写,这个在Tomcat的内部可以进行设置,也有使用场景,而这个就是和前面的Tomcat前端读的逻辑是一样的;

上面值得注意的一个细节是,因为CompleteHandler可能执行的很快,而且它和ServletOutPutStream的写线程是两个不同的线程,容易产生冲突,你可以看到在CompleteHandler中多加了一个Nio2Endpoint.isInline的判断,这个判断主要基于一个标识来进行校验;


总结:

从账面上来讲,NIO2通道相比NIO应该还要效率高,因为proactor模式本来就比reactor模式要好,另外还省去了Poller线程,但由于多路事件分离器是内核提供的,不同内核提供的多路事件分离器的事件处理效率不一,对NIO2的通道需要基于实际环境和场景压测才能得出最终的结论;


在后续的文章中,会对Tomcat各通道进行压力实际测试对比中,并基于各个通道的实测结果进行详细的对比和分析,敬请期待;


 
应用服务器技术讨论圈 更多文章 (JAVA NIO2 系列之十) 增加JAVA NIO.2的文件copy性能评测(JAVA NIO2 系列之九) SeekableChannel接口引入与FileChannel增强(JAVA NIO2 系列之八) 文件夹监视WatchService(JAVA NIO2 系列之七) 访问者模式与FileVistor(JAVA NIO2系列之六)
猜您喜欢 7月12日,暑假班首班首开课!! 亚马逊AWS认证攻略 安装包立减1M--微信Android资源混淆打包工具 什么是好的API设计? iOS高性能图片架构与设计