zoukankan      html  css  js  c++  java
  • Reactor模式与Netty线程模型

    简介

    当我们讨论 Netty 线程模型的时候,一般首先会想到的是经典的 Reactor 线程模型,尽管不同的 NIO 框架对于 Reactor 模式的实现存在差异,但本质上还是遵循了 Reactor 的基础线程模型。
    英文定义如下:

    The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.

    从上述文字中我们可以看出以下关键点 :

    • 事件驱动(event handling)
    • 可以处理一个或多个输入源(one or more inputs)
    • 通过Service Handler同步的将输入事件(Event)采用多路复用分发给相应的Request Handler(多个)处理

    单Reactor单线程模型


    我们用 Netty API 手写一个服务端:

    public static void main(String[] args) throws IOException {
    	// 初始化
    	NioServerSocketChannel channel = new NioServerSocketChannel();
    	NioEventLoopGroup bossAndWorkGroup = new NioEventLoopGroup(1);
    	bossAndWorkGroup.register(channel);
    	channel.bind(new InetSocketAddress(8081));
    
    	channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
    		@Override
    		public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    			System.out.println("已建立连接, 当前接待线程" + Thread.currentThread().getName());
    			NioSocketChannel socketChannel = (NioSocketChannel) msg;
    			handleAccept(bossAndWorkGroup, socketChannel);
    		}
    	});
    	System.in.read();
    }
    
    private static void handleAccept(NioEventLoopGroup workGroup, NioSocketChannel socketChannel) {
    	workGroup.register(socketChannel);
    	socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
    		@Override
    		public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    			ByteBuf buf = (ByteBuf) msg;
    			byte[] data = new byte[buf.readableBytes()];
    			buf.readBytes(data);
    			System.out.println("线程" + Thread.currentThread().getName() + " 收到消息:" + new String(data));
    		}
    	});
    }
    

    另外,我们使用 telnet 127.0.0.1 8081 命令连接服务端

    实验结果:

    启动多个Telnet客户端,也都是交给 nioEventLoopGroup-2-1 这一个线程来完成。单线程既承担了 Acceptor 接受连接的工作,又承担了 Handler 的读写,编码解码,业务请求的任务。
    虽然一个 NIO 线程确实可以完成其承担的职责,但是对于高负载、大并发的应用场景却不适合,主要原因如下:

    • 一个 NIO 线程同时处理成百上千的链路,性能上无法支撑。海量请求的循坏读取、解码、编码和发送,其中必然有大量请求超时。
    • NIO 线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了 NIO 线程的负载,最终导致大量消息积压和处理超时,成为系统瓶颈。
      主要原因是导致 accept 队列消息积压,来不及处理,因此服务端拒绝了新的请求。可以看看这篇文章,浅谈 Java Socket 构造函数参数 backlog
    • 可靠性问题:客户端逻辑也在 NIO 线程中执行,如果出现异常,意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接接收和处理外部消息,造成结点故障。

    单 Reactor 多线程模型

    Reactor 多线程模型与单线程模型最大的区别就是有一组 NIO 线程来处理 I/O 操作。

    第一处修改,创建 NIO 线程池对象:

    // NioEventLoopGroup bossAndWorkGroup = new NioEventLoopGroup(1);
    // bossAndWorkGroup.register(channel);
    NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
    NioEventLoopGroup workGroup = new NioEventLoopGroup(8);
    bossGroup.register(channel);
    

    第二处修改,IO操作交给 NIO 线程池:

    // handleAccept(bossAndWorkGroup, socketChannel);
    handleAccept(workGroup, socketChannel);
    


    建立连接,都是交给 nioEventLoopGroup-2-1,但是读写分别分配给了 nioEventLoopGroup-3-1,nioEventLoopGroup-3-2,nioEventLoopGroup-3-3
    因此来说,Reactor 多线程模型的特点如下:

    • 有一个专门的 NIO 线程 ———— Acceptor 线程用于监听服务端,接收客户端的 TCP 连接请求。(本文中的 Acceptor 线程是 nioEventLoopGroup-2-1)
    • 网络 I/O 操作————读、写等由一个 NIO 线程池负责,线程池可以采用标准的 JDK 线程池实现,它包含一个任务队列和 N 个可用的线程,由这些 NIO 线程负责消息的读取、解码、编码和发送。
    • 一个 NIO 线程可以同时处理 N 条链路,但是一个链路只对应一个 NIO 线程,防止发生并发操作问题。

    在绝大多数场景下,Reactor 多线程模型可以满足性能需求。但是,在个别特殊场景中,一个 NIO 线程负责监听和处理所有客户端连接可能会存在性能问题。
    例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。在这类场景下,单独一个 Acceptor 线程可能存在性能不足的问题。
    为了解决性能问题,产生了第三种 Reactor 线程模型————主从 Reactor 多线程模型。

    主从 Reactor 多线程模型


    主从 Reactor 线程模型的特点是:服务器端用于接收客户端连接的不再是一个单独的 NIO 线程,而是一个独立的 NIO 线程池。
    Acceptor 线程(本文中 Reactor 主线程)接收到客户端 TCP 连接请求并完成后(可能包含接入认证等),将新创建的 SocketChannel 注册到线程池(sub reactor 线程池)的某个 I/O 线程上,由它负责 SocketChannel 的读写和编解码工作。
    Acceptor 线程池仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端 subReactor 线程池的 I/O 线程上,由 I/O 线程负责后续的 I/O 操作。

    这里可能要结合 SSL 协议才能更好的理解,因此这里就不展开了

    Netty 的线程模型


    NioEventLoop 因为需要事件驱动和实现多路复用,自然少不了 Selector 成员变量。
    另外因为 channel.register()Selector.select() 在不同线程并发时可能造成死锁,因此 NioEventLoop 设计了 taskQueue:Queue<Runnable> ,并且通过调用 runAllTasks() 实现同步执行任务,避免死锁。

    参考

    《Netty 权威指南2》

  • 相关阅读:
    hadoop中namenode发生故障的处理方法
    开启虚拟机所报的错误:VMware Workstation cannot connect to the virtual machine. Make sure you have rights to run the program, access all directories the program uses, and access all directories for temporary fil
    Hbase的安装与部署(集群版)
    分别用反射、编程接口的方式创建DataFrame
    用Mapreduce求共同好友
    SparkSteaming中直连与receiver两种方式的区别
    privot函数使用
    Ajax无刷新显示
    使用ScriptManager服务器控件前后台数据交互
    数据库知识
  • 原文地址:https://www.cnblogs.com/kendoziyu/p/netty-reactor-design-pattern.html
Copyright © 2011-2022 走看看