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


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

    一 Reactor 单线程模型

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

    (2)作为NIO客户端,向服务端发起TCP连接;

    (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的详细图解(画的非常清晰)参看 https://www.cnblogs.com/TomSnail/p/6158249.html

  • 相关阅读:
    动手动脑——登录界面
    异常处理动手动脑问题
    继承与多态
    课程作业04——动手动脑(字符串String)
    创建过的对象的个数
    类和对象动手动脑问题
    第二周进度条
    构建之法阅读笔记01
    新课程界面
    软件工程概论课后作业01
  • 原文地址:https://www.cnblogs.com/balfish/p/8205671.html
Copyright © 2020-2023  润新知