Netty-11 EventLoop和EventLoopGroup

一、Reactor线程模型

(一)Reactor单线程模型

在这里插入图片描述

  • 所有的操作都在同一个NIO线程里面完成。如:接受客户端的请求(Acceptor),读数据,解码,编码,写数据等等,这些操作都有相应的Handler进行处理,Acceptor接收到客户端的请求之后,根据不同的消息类型将ByteBuf进行Dispatch。
  • 由于使用的是异步非阻塞I/O,所有的I/O操作并不会阻塞,所以从理论上讲一个线程就可以处理所有的I/O操作;
  • 然而,一个线程同时处理成百上千的链路的时候,就无法支撑;
  • 当NIO线程负载过程的时候,处理速度变慢,客户端大量超时会导致超时重连,从而再次加NIO线程压力,恶性循环下去,导致系统性能出现问题。

(二)Reactor多线程模型

在这里插入图片描述

  • 有一个单独的NIO线程来监听接受客户端的连接请求,也就是图里面的Acceptor线程,而在单线程模型下,Acceptor只是其中的一个操作而已。
  • 网络的I/O读写请求会有一个独立的NIO线程池进行处理,也就是右边的你那个Reactor Pool,这个线程池里面的NIO线程负责消息的读取、解码、编码、发送等工作;
  • 一个NIO线程可以同时处理多个链路,但是一个链路只能被一个NIO线程进行处理,从而防止线程安全问题;
  • 有效的解决了单线程模式I/O请求过多导致的性能瓶颈
  • 然后,当并发百万的客户端进行连接的时候,会导致Acceptor线程的性能问题,特别是这些连接需要进行安全认证和客户端握手等比较耗时的操作的时候,性能会受到更大的局限;

(三)主从Reactor多线程模式

在这里插入图片描述

  • 有两个线程池,把多线程模式下的Acceptor线程升级为一个线程池,专门来处理客户端的连接,认证等。
  • 连接成功后,会将连接创建出的SocketChannel交给另外一个线程池进行处理,后续的I/O操作就交给了另外一个线程池。
  • 由于有多个线程来处理客户端的接入请求,所以可以同时接入成千上万的客户端连接。

二、Netty线程模型

Netty的线程模型并不是一成不变的,它实际上取决于用户的启动配置参数。通过设置不同的启动参数,Netty可以支持Reactor单线程、多线程、主从多线程等模式。
原理图:
在这里插入图片描述
启动服务代码:

    //服务端NIO线程组,用于网络事件处理,实际上就是Reactor线程组
    EventLoopGroup bossGroup = new NioEventLoopGroup(); // 用于接受客户端的连接
    EventLoopGroup workGroup = new NioEventLoopGroup(); // 用户SocketChannel进行网络读写请求
    try {
      ServerBootstrap bootstrap = new ServerBootstrap(); //用于启动NIO的辅助启动类
      bootstrap.group(bossGroup, workGroup)
          .channel(NioServerSocketChannel.class) //这个类似于NIO中的NioServerSocketChannel,IO中的ServerSocket,用于绑定端口
          .option(ChannelOption.SO_BACKLOG, 1024) //这是TCP参数,这里是设置了backlog的大小
          //设置网络IO事件处理器信息,如处理器类、编码、解码等等。
          .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
              socketChannel.pipeline().addLast(new MessageServerHandler());
            }
          });
      //绑定端口,同时进行阻塞直到服务关闭
      ChannelFuture future = bootstrap.bind(port).sync();
      System.out.println("启动服务端监听端口:" + port);
      //关闭通道
      future.channel().closeFuture().sync();
  • 启动的时候,创建了两个EventLoopGroup,它们实际上是两个独立的Reactor线程池,一个用于接受客户端的TCP连接,另一个用于处理I/O相关的读写操作,或者执行系统Task,用户自定义Task等等;
  • Netty用户接受客户端连接的线程池的职责如下:
  1. 接受客户端TCP连接,初始化ChannelPipeline;
  2. 将链路状态变更事件通知给ChannelPipeline;
  • 处理I/O操作的线程池职责:
  1. 异步读取通讯对端的数据包,发送给ChannelPipeline;
  2. 异步将消息发送给对端,调用ChannelPipeline的消息发送接口;
  3. 执行系统调用Task
  4. 指定定时任务Task,如链路空闲状态监测;

为了尽可能的提高性能,Netty在很多地方都进行了无锁化的设计,例如在I/O线程内部进行串行化,避免多线程竞争导致的性能下降问题。表面上看,这种串行化的设计对CPU利用率不高,但是通过调整NIO线程池的参数,可以启动多个串行化的线程并行运行,所以比多线程切换的效率更加合理。
原理如图:
在这里插入图片描述

三、Netty线程模型实战建议

  1. 创建两个NioEventLoopGroup,用于隔离Acceptor线程和I/O操作线程;
  2. 尽量不要再ChannelHandler中启动线程(解码后将POJO发送给后端业务的代码除外);
  3. 解码操作要放在NIO线程调用的ChannelHandler中进行,不要切换到用户线程;
  4. 如果业务操作逻辑比较简单,没有复杂的业务计算,如连接数据库、磁盘读写等操作,那么就在NIO线程中编排逻辑比较好,不要切换到用户线程;
  5. 如果业务逻辑比较复杂,不要再NIO线程中编排,否则会导致NIO线程释放较慢。建议把POJO交给用户自定义的Task中,给用户线程进行处理。

四、NioEventLoop浅析

(一)继承体系

在这里插入图片描述

(二)重要方法

从上面的继承图可以看到,NioEventLoop的继承体系比较复杂,他的功能比较复杂。并不是一个纯粹的I/O线程,除了负责I/O读写之外,还有以下两类任务:

  1. 系统Task:通过调用NioEventLoop的execute(Runnable task)方法来实现。Netty有很多系统Task,创建他们的主要原因是:当I/O线程和用户线程同时操作网络资源时,为了防止并发操作导致的锁竞争,将用户线程的操作封装成Task放入消息队列中,由I/O线程负责执行,从而实现局部无锁化;
  2. 定时任务:通过调用schedule(Callable callable, long delay, TimeUnit unit)方法来实现。

猜你喜欢

转载自blog.csdn.net/mail_liuxing/article/details/91043193