您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iProcess 课程 角色 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
  
每天15篇文章
不仅获得谋生技能
更可以追随信仰
 
     
   
 订阅
  捐助
深入理解Netty线程模型
 
37 次浏览     评价:  
 2019-9-12
 
编辑推荐:
本文来自于微信公众号: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

官方连接

https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6670302

官方给出的测试代码如下:

重点在while(true)循环里边,也就是说

int numKeys = selector.select();

这行代码如果没有轮询到准备就绪的Channel,本该阻塞,但JDK epoll并没有阻塞返回一个空的集合,导致while陷入死循环中。很多服务器应用,比如上文的Netty,Jetty等对其作了修复,rebuildSelector。

 
   
37 次浏览  评价: 差  订阅 捐助
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程
每天2个文档/视频
扫描微信二维码订阅
订阅技术月刊
获得每月300个技术资源
 
 

关于我们 | 联系我们 | 京ICP备10020922号 京公海网安备110108001071号