Netty:Reactor模型

导读:在Neyyu权威指南2 ”Netty服务端创建“这一章节开头中,有这么一个前提条件,“概述是想要深入到学习Netty原理,通过阅读源码是最有效的方式之一。那么阅读源码,需要了解一定的基础是必要的。同时Reactor模式,是高性能网络编程的必知必会模式。

Netty服务端的创建需要必备: 1)熟悉JavaNIO主要类与库的使用 ;2)熟悉JDK的多线程便车给你; 3)了解Reactor模式”。针对于1和2,在学习java基础的时候必然已经学过,那么对于Reactor模式又是什么,通过以下的文章可以进一步了解到什么是Reactor模式,且Netty是如何使用Reactor模式的。

1.什么是Reactor(反应器设计)模式

Reactor模式 是一种「事件驱动」模式。
「Reactor线程模型」就是通过 单个线程 使用Java NIO包中的Selector的select()方法,进行监听。当获取到事件(如accept、read等)后,就会分配(dispatch)事件进行相应的事件处理(handle)。
如果要给 Reactor线程模型 下一个更明确的定义,应该是:
Reactor线程模式 = Reactor(I/O多路复用)+ 线程池
其中Reactor负责监听和分配事件,线程池负责处理事件。

2.Reactor模式种定义的角色

Reactor模型中定义的三种角色:
Reactor:负责监听和分配事件,将I/O事件分派给对应的Handler。新的事件包含连接建立就绪、读就绪、写就绪等。
Acceptor:处理客户端新连接,并分派请求到处理器链中。
Handler:将自身与事件绑定,执行非阻塞读/写任务,完成channel的读入,完成处理业务逻辑后,负责将结果写出channel。可用资源池来管理。

3.多线程IO的致命缺陷

最开始的网络编程的原理就是服务器用一个while循环,while循环不断监听是否有套接字进行连接,调用如下类似的函数进行处理

while(true){
socket = accept();
handle(socket)
}

这种方法的最大问题是无法并发,效率太低,如果当前的请求没有处理完,那么后面的请求只能被阻塞,服务器的吞吐量太低。
之后,想到了使用多线程,也就是很经典的connection per thread,每一个连接用一个线程处理,类似:

package com.crazymakercircle.iodemo.base;

import com.crazymakercircle.config.SystemConfig;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

class BasicModel implements Runnable {
    public void run() {
        try {
            ServerSocket ss =
                    new ServerSocket(SystemConfig.SOCKET_SERVER_PORT);
            while (!Thread.interrupted())
                new Thread(new Handler(ss.accept())).start();
            //创建新线程的handle
            // or, single-threaded, or a thread pool
        } catch (IOException ex) { }
    }

    static class Handler implements Runnable {
        final Socket socket;
        Handler(Socket s) { socket = s; }
        public void run() {
            try {
                byte[] input = new byte[SystemConfig.INPUT_SIZE];
                socket.getInputStream().read(input);
                byte[] output = process(input);
                socket.getOutputStream().write(output);
            } catch (IOException ex) {  }
        }
        private byte[] process(byte[] input) {
            byte[] output=null;
            
            return output;
        }
    }
}

对于每一个请求都分发给一个线程,每个线程中都独自处理上面的流程。
tomcat服务器的早期版本确实是这样实现的。
多线程并发模式,一个连接一个线程的优点是:
一定程度上极大地提高了服务器的吞吐量,因为之前的请求在read阻塞以后,不会影响到后续的请求,因为他们在不同的线程中。这也是为什么通常会讲“一个线程只能对应一个socket”的原因。另外有个问题,如果一个线程中对应多个socket连接不行吗?语法上确实可以,但是实际上没有用,每一个socket都是阻塞的,所以在一个线程里只能处理一个socket,就算accept了多个也没用,前一个socket被阻塞了,后面的是无法被执行到的。
多线程并发模式,一个连接一个线程的缺点是:
缺点在于资源要求太高,系统中创建线程是需要比较高的系统资源的,如果连接数太高,系统无法承受,而且,线程的反复创建-销毁也需要代价。
改进方法是:
采用基于事件驱动的设计,当有事件触发时,才会调用处理器进行数据处理。使用Reactor模式,对线程的数量进行控制,一个线程处理大量的事件。

4.Reactor模型

应用Java NIO构建Reactor模式,Doug Lea大爷在“Scalable IO in Java”中有了很好的阐述。以下的图片截取其PPT中经典的图例说明 Reactor模式的典型实现

4.1 单Reactor单线程模型

Reactor模型使用的是异步非阻塞IO,进行所有的IO操作都不会造成阻塞,因此,在理论上,单个线程可以独立处理所有的IO操作、IO流程等。
单Reactor单线程模式图
在这里插入图片描述
单线程的处理方式:

  • Reactor内部通过slector轮询监听连接事件,监听到事件后,内部dispatch对事件进行分发处理
  • 如果是连接建立事件,通过accptor监听连接,并创建一个Handler来处理后续各种事件的发生
  • 如果是读写事件,直接调用连接对应的Handler来处理,Handler完成 read => (decode => compute => encode) => send 的全部流程
  • 注:在以上过程中,事件监听、事件分发、还是事件处理,不论事件的多与少,均是一个线程执行以上的操作,没有更多的线程参与到其中。

该模式的缺点:

  • 当某个 handler 出现问题发生阻塞, 会导致其他所有的 client 的 handler 都得无法到执行。
  • handler 的阻塞也会导致整个服务不能接收新的 client 请求(因为 acceptor 也被阻塞了)。
  • 单线程模型不能充分利用多核资源,单个线程处理使用的CPU资源有限,不能充分利用资源。

参考代码:待补充

4.2 单Reactor多线程模型

由于单Reactor单线程模式不能充分的利用服务器资源以及当其中一个Handle出现阻塞时就会导致整体瘫痪,因此,又出现了一种新的模型,即单Reactor多线程模型。
在这里插入图片描述

处理方式:

  1. 单独分一个NIO线程,既acceptor线程专门用于监听链接请求,即监听服务端接收客户端的TCP连接请求
  2. 网络I/O操作–读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送
  3. 1个NIO的线程可以对应多个链路,但是1个链路只能对应一个线程,防止出现并发问题

该模式的缺陷:

  1. 作为低量级使用时,能够有效的提升性能,但是如果存在百万级客户端发出链接请求,一个线程进行轮询会出现严重的性能问题,鉴权认证,与客户端进行握手创建连接,本身就是很耗费性能资源的存在,当出现百万级别时会导致出现排队,此时便不适用。

参考代码:待补充

4.3 主从Reactor多线程模型

正因为单Reactor线程模型中存在的性能隐患,因此才会引申出主从Reactor多线程模型,具体的模型图见下图
在这里插入图片描述
消息处理方式:

  1. 从主线程池中随机选择一个Reactor线程作为acceptor线程,用于绑定监听端口,接收客户端连接
  2. acceptor线程接收客户端连接请求之后创建新的SocketChannel,将其注册到主线程池的其它Reactor线程上,由其负责接入认证、IP黑白名单过滤、握手等操作
  3. 步骤2完成之后,业务层的链路正式建立,将SocketChannel从主线程池的Reactor线程的多路复用器上摘除,重新注册到Sub线程池的线程上,并创建一个Handler用于处理各种连接事件
  4. 当有新的事件发生时,SubReactor会调用连接对应的Handler进行响应
  5. Handler通过Read读取数据后,会分发给后面的Worker线程池进行业务处理
  6. Worker线程池会分配独立的线程完成真正的业务处理,如何将响应结果发给Handler进行处理
  7. Handler收到响应结果后通过Send将响应结果返回给Client

Reactor该模型具有如下的优点:

  1. 响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;
  2. 编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
  3. 可扩展性,可以方便地通过增加Reactor实例个数来充分利用CPU资源;
  4. 可复用性,Reactor模型本身与具体事件处理逻辑无关,具有很高的复用性

参考代码:待补充

注:本文部分内容摘自《Java高并发核心编程》;图文编辑来自Scalable IO in Java; Scalable IO in Java地址是:http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf


版权声明:本文为JNqidian原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。