编辑推荐: |
本文来自于微信公众号:OutOfMemoryError,本文主要介绍了什么是I/O多路复用, Reactor三种线程模型 ,Netty线程模型,NioEventLoop源码分析等内容 ,希望对您能有所帮助。 |
当我们谈论Netty的线程模型时,首先会想到的是经典的Reactor IO多路复用线程模型。从这篇文章中,大家可以学习到如下知识:
什么是I/O多路复用
Reactor三种线程模型
Netty线程模型
NioEventLoop源码分析
JDK epoll bug
什么是I/O多路复用
学习I/O多路复用之前,我们先来了解如下几个概念:
阻塞I/O:客户端从socket中读取数据或写入数据时,如果读取时流中没有数据,写入时缓冲区已满,就需要block,知道流中有数据或者缓冲区的数据被排空。
非阻塞I/O:客户端从流中读取数据,如果流中没有数据,则立即返回,不发生block。
同步I/O:同步I/O将导致请求的I/O操作一直被block,直到I/O完成。
异步I/O:异步I/O不会导致block,发出I/O请求后立即返回,直到完成I/O操作后再异步通知调用进程。
I/O多路复用可以监视多个FD(文件描述符),一旦某个FD准备就绪,就会通知相应进程处理。多路复用也是阻塞的,阻塞的方法是select/poll/epoll,可以在单个进程中同时处理多个I/O请求,原理是采用轮询的方式便利所有的I/O操作,当某些I/O有数据时,就通知用户进程处理。
select:系统提供select函数来实现多路复用输入/输出模型,select系统调用是用来让我们的程序监视多个文件句柄的状态变化。程序会阻塞在select函数上,直到被监视的文件句柄中有一个或多个发生了状态变化。
poll:poll函数与select函数的最大不同之处在于:select函数有最大文件描述符的限制,一般1024个,而poll函数对文件描述符的数量没有限制。
epoll:
监视的描述符数量不受限制,所支持的FD上限是Linux系统最大可以打开文件的数目;
I/O效率不会随着监视fd的数量增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的,只有就绪的fd才会执行回调函数。
Reactor三种线程模型
1. Reactor单线程模型
是指所有的I/O操作都在同一个NIO线程上完成,职责如下
作为NIO服务端,接收客户端TCP连接
作为NIO客户端,向服务端发起TCP连接
读取对端请求或者应答消息
向对端发送请求或者应答消息
在一些小容量场景下,可以使用多线程模型,但对于高负载场景下并不适用,原因如下:
一个NIO线程同时处理成百上千的连接,性能上无法保证。
NIO线程负载过重,处理速度会越来越慢,会导致大量客户端连接超时
一旦NIO线程跑飞,或者死循环,会导致整个系统的不可用
2. Reactor多线程模型
与单线程模型最大的区别是,有一组NIO来处理I/O请求,特点如下
有一个NIO线程——Acceptor线程用户监听客户端的连接
有一个NIO线程池负责I/O的读写操作,该线程池可以是基于JDK的线程池。
3. 主从Reactor多线程模型
如果并发百万的客户端连接,在Reactor多线程模型下只有一个NIO的Acceptor线程处理客户端连接会有性能问题。主从Reactor线程模型的特点是:服务端用于接收客户端的连接不再是一个单独的NIO线程,而是一个NIO的Acceptor线程池。Acceptor接收到客户端的TCP连接请求处理完成后,将新创建的ChannelSocket注册到I/O线程池(sub reactor线程池)上的某一个线程上,由I/O负责后续的I/O读写操作。
4. Netty线程模型
还记得Netty服务端启动时的代码吗??
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
服务端启动时创建了两个NioEventLoopGroup,他们实际上时两个独立的Reactor线程池,一个负责接收客户端的TCP连接,另一个用于处理I/O操作,或执行系统Task、定时任务Task等,如上图所示的两个方法:
NioEventLoop.execute(Runnable task)
NioEventLoop.schedule(Runnable task)
NioEventLoop源码分析
先让我们来看看NioEventLoop的继承关系图
NioEventLoop需要处理网络I/O操作,首先聚合了一个多路复用器Selector,并且NioEventLoop的构造方法中直接调用openSelector()方法完成初始化。
代码中常量DISABLE_KEYSET_OPTIMIZATION(selectedKeys优化开关)默认为false,则通过反射的方式获得Selector,否则直接返回。
接下来重点看一下run方法
整个run方法用一个while循环包围,首先将wakenUp参数设置为false并将旧值保存在oldWakenUp中。
1、调用hasTask()判断队列中是否有任务,如果有则执行selectNow()方法,该方法会立即出发Select的选择操作判断是否有准备就绪的Channel,如果有则返回Channel的集合,否则返回0。
2、如果没有任务,则调用select()方法轮询,看是否有准备就绪的Channel,select()代码如下:
判断队列中是否有已超时或即将执行的定时任务,如有,则调用selectNow()方法并将selectCnt置为1,并退出当前循环返回run()方法。否则将超时时间作为参数进行select(),每次select后都要将selectCnt++,如果满足如下条件则退出循环继续run()方法的后面的逻辑。
if (selectedKeys != 0 || this.oldWakenUp || this.wakenUp.get() || this.hasTasks()) {
break;
}
如果本次Selector的轮询结果为空,说明这是一个空轮询,有可能出发了JDK的epoll bug,这个bug会导致一直空轮询使I/O线程一直处于100%的状态,此时就需要重建Selector来避免发生这类问题,rebuildSelector()代码如下:
rebuildSelector的主要逻辑是:首先判断是否有其他线程在进行rebuild,如有则将本次操作封装成一个task放入队列,避免多线程同时rebuild。然后创建新的Selector,通过循环将注册在旧的Selector上的SocketChannel注册在新的Selector上并关闭旧的Selector。
让我们再返回run()方法看看后面的逻辑。
在openSelector()方法中我们得知,如果DISABLE_KEYSET_OPTIMIZTION为false时,通过反射获得多路复用器selector,和selectedKeys,所以run方法接下来会调processSelectedKeysPlain()方法
if (this.selectedKeys != null) { this.processSelectedKeysOptimized (this.selectedKeys.flip()); } else { this.processSelectedKeysPlain (this.selector.selectedKeys()); }
循环selectedKeys,获取sekectionKey和它的附件信息k.attachment(),
如果它是一个AbstractnioChannel的实例,说明它是一个NioServerSocketChannel或NioSocketChannel,需要进行I/O操作,否则它是个定时任务。
I/O操作时的源码如下:
NioUnsafe类是主要对ByteBuf进行读写的类,首先判断readyOps字段,如果是读请求,则调用unsafe.read()方法,写请求调用unsafe.forceFlush()方法,连接请求调用unsafe.finishConnect()方法。
JDK epoll bug
官方连接
官方给出的测试代码如下:
重点在while(true)循环里边,也就是说
这行代码如果没有轮询到准备就绪的Channel,本该阻塞,但JDK epoll并没有阻塞返回一个空的集合,导致while陷入死循环中。很多服务器应用,比如上文的Netty,Jetty等对其作了修复,rebuildSelector。