Reactor 线程模型以及在netty中的应用

这里咱们须要理解的一点是Reactor线程模型是基于同步非阻塞IO实现的。对于异步非阻塞IO的实现是Proactor模型html

 

一 Reactor 单线程模型

Reactor单线程模型就是指全部的IO操做都在同一个NIO线程上面完成的,也就是IO处理线程是单线程的。NIO线程的职责是: 
(1)做为NIO服务端,接收客户端的TCP链接;react

(2)做为NIO客户端,向服务端发起TCP链接;bootstrap

(3)读取通讯对端的请求或者应答消息;后端

(4)向通讯对端发送消息请求或者应答消息。数组

Reactor单线程模型图以下所示:安全

Reactor模式使用的是同步非阻塞IO(NIO),全部的IO操做都不会致使阻塞,理论上一个线程能够独立的处理全部的IO操做(selector会主动去轮询哪些IO操做就绪)。从架构层次看,一个NIO线程确实能够完成其承担的职责,好比上图的Acceptor类接收客户端的TCP请求消息,当链路创建成功以后,经过Dispatch将对应的ByteBuffer转发到指定的handler上,进行消息的处理。网络

对于一些小容量的应用场景下,可使用单线程模型,可是对于高负载、大并发的应用场景却不适合,主要缘由以下: 
(1)一个NIO线程处理成千上万的链路,性能没法支撑,即便CPU的负荷达到100%;多线程

(2)当NIO线程负载太重,处理性能就会变慢,致使大量客户端链接超时而后重发请求,致使更多堆积未处理的请求,成为性能瓶颈。架构

(3)可靠性低,只有一个NIO线程,万一线程假死或则进入死循环,就彻底不可用了,这是不能接受的。并发

 

 

二 Reactor 多线程模型

Reactor多线程模型与单线程模型最大的区别在于,IO处理线程再也不是一个线程,而是一组NIO处理线程。原理以下图所示:

 

Reactor多线程模型的特色以下: 
(1)有一个专门的NIO线程—-Acceptor线程用于监听服务端,接收客户端的TCP链接请求。

(2)网络IO操做—-读写等操做由一个专门的线程池负责,线程池可使用JDK标准的线程池实现,包含一个任务队列和N个可用的线程,这些NIO线程就负责读取、解码、编码、发送。

(3)一个NIO线程能够同时处理N个链路,可是一个链路只对应一个NIO线程。

Reactor多线程模型能够知足绝大多数的场景,除了一些个别的特殊场景:好比一个NIO线程负责处理客户全部的链接请求,可是若是链接请求中包含认证的需求(安全认证),在百万级别的场景下,就存在性能问题了,由于认证自己就要消耗CPU,为了解决这种情景下的性能问题,产生了第三种线程模型:Reactor主从线程模型。

 

 

三 主从Reactor 多线程模型

主从Reactor线程模型的特色是:服务端用于接收客户端链接的再也不是一个单独的NIO线程,而是一个独立的NIO的线程池。Acceptor接收到客户端TCP链接请求并处理完成后(可能包含接入认证),再将新建立的SocketChannel注册到IO线程池(sub reactor)的某个IO处理线程上并处理编解码和读写工做。Acceptor线程池仅负责客户端的链接与认证,一旦链路链接成功,就将链路注册到后端的sub Reactor的IO线程池中。 线程模型图以下:

利用主从Reactor模型能够解决服务端监听线程没法有效处理全部客户链接的性能不足问题,这也是netty推荐使用的线程模型。

 

 

四 netty的线程模型

netty的线程模型是能够经过设置启动类的参数来配置的,设置不一样的启动参数,netty支持Reactor单线程模型、多线程模型和主从Reactor多线程模型。

 

服务端启动时建立了两个NioEventLoopGroup,一个是boss,一个是worker。实际上他们是两个独立的Reactor线程池,一个用于接收客户端的TCP链接,另外一个用于处理Io相关的读写操做,或者执行系统/定时任务的task。

boss线程池做用: 
(1)接收客户端的链接,初始化Channel参数 
(2)将链路状态变动时间通知给ChannelPipeline

worker线程池做用: 
(1)异步读取通讯对端的数据报,发送读事件到ChannelPipeline 
(2)异步发送消息到通讯对端,调用ChannelPipeline的消息发送接口 
(3)执行系统调用Task
(4)执行定时任务Task

经过配置boss和worker线程池的线程个数以及是否共享线程池等方式,netty的线程模型能够在单线程、多线程、主从线程之间切换。

为了提高性能,netty在不少地方都进行了无锁设计。好比在IO线程内部进行串行操做,避免多线程竞争形成的性能问题。表面上彷佛串行化设计彷佛CPU利用率不高,可是经过调整NIO线程池的线程参数,能够同时启动多个串行化的线程并行运行,这种局部无锁串行线程设计性能更优。 

nettyd的NioEventLoop读取到消息以后,直接调用ChannelPipeline的fireChannelRead(Object msg),只要用户不主动切换线程,一直都是由NioEventLoop调用用户的Handler,期间不进行线程切换,这种串行化设计避免了多线程操做致使的锁竞争,性能角度看是最优的。

 

 

 1 import io.netty.bootstrap.ServerBootstrap;
 2 import io.netty.channel.ChannelFuture;
 3 import io.netty.channel.ChannelInitializer;
 4 import io.netty.channel.ChannelOption;
 5 import io.netty.channel.EventLoopGroup;
 6 import io.netty.channel.nio.NioEventLoopGroup;
 7 import io.netty.channel.socket.SocketChannel;
 8 import io.netty.channel.socket.nio.NioServerSocketChannel;
 9 
10 /**
11  * Created by xxx on 2018/1/5 PM5:23.
12  */
13 public class Test {
14     public void bind(int port) throws Exception {
15         // 配置服务端的NIO线程组
16         EventLoopGroup bossGroup = new NioEventLoopGroup();
17         EventLoopGroup workerGroup = new NioEventLoopGroup();
18         try {
19             ServerBootstrap b = new ServerBootstrap();
20             b.group(bossGroup, workerGroup)
21                     .channel(NioServerSocketChannel.class)
22                     .option(ChannelOption.SO_BACKLOG, 1024)
23                     .childHandler(new ChildChannelHandler());
24             // 绑定port,同步等待成功
25             ChannelFuture f = b.bind(port).sync();
26             // 等待服务端监听port关闭
27             f.channel().closeFuture().sync();
28         } finally {
29             // 优雅退出,释放线程池资源
30             bossGroup.shutdownGracefully();
31             workerGroup.shutdownGracefully();
32         }
33     }
34 
35     private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
36         @Override
37         protected void initChannel(SocketChannel arg0) throws Exception {
38             arg0.pipeline().addLast(new TimeServerHandler());
39         }
40     }
41 }

 

 

netty 服务端的建立过程

 

Netty 屏蔽NIO通讯的底层细节:

  1. 首先建立ServerBootstrap,他是Netty服务端的启动辅助类

  2. 设置并绑定Reactor线程池。

    Netty的Reactor线程池是EventLoopGroup,它实际就是EventLoop线 程的数组。

    EventLoop的职责是处理所有注冊到本线程多路复用器Selector上的Channel

  3. 设置NioServerSocketChannel。Netty经过工厂类,利用反射建立NioServerSocketChannel对象

  4. 设置TCP參数

  5. 链路创建的时候建立并初始化ChannelPipeline.它本质就是一个负责处理网络事件的职责链,负责管理和运行ChannelHandler。

    网络事件以事件流的形式在ChannelPipeline中流转,由ChannelPipeline依据ChannelHandler的运行策略调度ChannelHandler的运行

    1. 绑定并启动监听port
    2. 绑定port,并启动。将会启动NioEventLoop负责调度和运行Selector轮询操做,选择准备就绪的Channel集合。当轮询到准备就绪的Channel以后,就由Reactor线程NioEventLoop运行ChannelPipeline的对应方法。终于调度并运行ChannelHandler。

 

NioEventLoop IO线程浅析

作为Netty的Reactor线程,因为要处理网络IO读写,因此聚合一个多路复用器对象,它经过open获取一个多路复用器。他的操做主要是在run方法的for循环中运行的。

  1. 作为bossGroup的线程 他需要绑定NioServerSocketChannel 来监听client的connect请求,并处理链接和校验。
  2. 做为workGroup线层组的线程。需要将链接就绪的SocketChannel绑定到线程中。因此一个client链接至相应一个线程,一个线程可以绑定多个client链接。 

从调度层面看。也不存在在EventLoop线程中 再启动其余类型的线程用于异步运行其余的任务。这样就避免了多线程并发操做和锁竞争,提高了I/O线程的处理和调度性能。

 

 

五 epoll bug

screenshot

Selector.select 没有任务运行时,可能触发JDK的epoll BUG。这就是著名的JDK epoll BUG,JDK1.7早期版本号获得解决。

server直接表现为IO线程的CPU很是高,可能达到100%,可能会致使节点故障!

 

为何会发生epoll Bug

screenshot

Netty的修复策略为:

  1. 对Selector的select的操做周期进行统计

  2. 完成一次空的select操做进行一次计数

  3. 在某周期内(如100ms)连续N次空轮询, 说明触发了epoll死循环BUG

  4. 检测到死循环后,重建selector的方式让系统恢复正常

netty采用此策略,完美避免了此BUG的发生。

关于netty bug,更详细的能够参看: http://blog.csdn.net/huoyunshen88/article/details/45672295

proactor和reactor的详细图解(画的很是清晰)参看 http://www.javashuo.com/article/p-qarptrkw-bw.html

相关文章
相关标签/搜索