栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Netty Reactor模型

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Netty Reactor模型

无论是C++还是Java编写的网络框架,大多数都是基于Reactor模式进行设计和开发,Reactor模式基于事件驱动,特别适合处理海量的I/O事件。

一、Reactor三种线程模型
  1. 单线程模型

Reactor单线程模型,指的是所有的IO操作都在同一个NIO线程上面完成,NIO线程的职责如下:

  • 1)作为NIO服务端,接收客户端的TCP连接;
  • 2)作为NIO客户端,向服务端发起TCP连接;
  • 3)读取通信对端的请求或者应答消息;
  • 4)向通信对端发送消息请求或者应答消息。

Reactor单线程模型示意图如下所示:

Reactor 对象通过 select 监控连接事件,收到事件后通过 dispatch 进行转发。如果是连接建立的事件,则由 Acceptor 接收连接,并创建 Handler 处理后续的事件。如果不是建立连接事件,则 Reactor 会分发调用 Handler 来响应。Handler 会完成 read、decode、compute、encode、send等一整套流程。

对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发的应用场景却不合适,主要原因如下:

  • 1)一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送;
  • 2)当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;
  • 3)可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。

为了解决这些问题,演进出了Reactor多线程模型,如下。

  1. 单Reactor多线程模型
    Rector多线程模型与单线程模型最大的区别就是有一组NIO线程处理IO操作,它的原理图如下:

Reactor多线程模型的特点:

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

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

  1. 主从多线程模型

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

主从多线程模型如下图所示:

利用主从NIO线程模型,可以解决1个服务端监听线程无法有效处理所有客户端连接的性能不足问题。
它的工作流程总结如下:

  • 1)从主线程池中随机选择一个Reactor线程作为Acceptor线程,用于绑定监听端口,接收客户端连接;Acceptor线程接收客户端连接请求之后创建新的SocketChannel,将其注册到主线程池的其它Reactor线程上,由其负责接入认证、IP黑白名单过滤、握手等操作;
  • 2)步骤2完成之后,业务层的链路正式建立,将SocketChannel从主线程池的Reactor线程的多路复用器上摘除,重新注册到Sub线程池的线程上,用于处理I/O的读写操作。
二、Netty线程模型

事实上,Netty的线程模型与上面介绍的三种Reactor线程模型相似,下面章节我们通过Netty服务端和客户端的线程处理流程图来介绍Netty的线程模型。

  • 服务端线程模型
    一种比较流行的做法是服务端监听线程和IO线程分离,类似于Reactor的多线程模型,它的工作原理图如下:

  • 客户端线程模型
    相比于服务端,客户端的线程模型简单一些,它的工作原理如下:

三、详解Netty主从多Reactor模型
  • 我们知道主从多Reactor模型如下:

  1. 简单介绍一下上图中涉及的几个重要角色:
  • Main Reactor(BossGroup)

主 Reactor 线程组,主要负责连接事件,并将IO读写请求经由Acceptor转发到 SubReactor 线程池。当然在一些需要对客户端进行权限控制等场景下,权限校验的职责可以放到 Main Reactor 线程池,即 Main Reactor 也可以注册通道的读写事件,读取客户端权限校验相关的数据包,执行权限验证,权限验证通过后再将2通道注册到IO线程。

  • Acceptor

请求接收者,在实践时其职责类似服务器,并不真正负责连接请求的建立,而只将其请求委托 Main Reactor 线程池来实现,起到一个转发的作用。

  • Sub Reactor (WorkerGroup)

Main Reactor 通常监听客户端连接后会将通道的读写转发到 Sub Reactor 线程池中一个线程(负载均衡),负责数据的读写。在 NIO 中 通常注册通道的读(OPREAD)、写事件(OPWRITE)。

为了更加深刻的理解主从 Reactor 模型,我们来看一下网络通讯一般会包含哪些关键动作:

一个网络交互通常的几个步骤如下:

  1. 服务端启动,并在特定端口上监听,例如 web 应用的 80端口。
  2. 客户端发起TCP的三次握手,与服务端建立连接,这里以 NIO为例,连接成功建立后会创建NioSocketChannel对象。
  3. 服务端通过 NioSocketChannel 从网卡中读取数据。
  4. 服务端根据通信协议从二进制流中解码出一个个请求。
  5. 根据请求,执行对应的业务操作,例如 Dubbo 服务端接受一个查询用户ID为1的用户信息。
  6. 将业务执行结果返回到客户端,通常涉及到协议编码、压缩等。

线程模型需要解决的问题:连接监听、网络读写、编码、解码、业务执行这些操作步骤如何运用多线程编程,提升性能。

  1. 主从多Reactor模型是如何解决上面的问题呢?
  • 连接建立(OP_ACCEPT)由 Main Reactor 线程池负责,创建NioSocketChannel后,将其转发给SubReactor。
  • SubReactor 线程池主要负责网络的读写(从网络中读字节流、将字节流发送到网络中),即注册OPREAD、OPWRITE,并且同一个通道会绑定一个SubReactor线程。
  • 编码、解码、业务执行,则具体情况具体分析

通常编码、解码会放在IO线程中执行,而业务逻辑的执行通常会采用额外的线程池,但不是绝对的,一个好的框架通常会使用参数来进行定制化选择,例如 ping、pong 这种心跳包,直接在 IO 线程中执行,无需再转发到业务线程池,避免线程切换开销。

温馨提示:在网络编程中,通常将用于网络读写的线程称为IO线程。

  1. Netty 的线程模型

Netty的线程模型是基于主从多Reactor模型。

  • Netty 中网络的连接事件(OP_ACCEPT)由Main Reactor 线程组实现,即 Boss Group,通常只需设置一个线程。
  • 网络的读写操作由 Work Group ( Sub Reactor) 线程组来实现,线程的个数默认为 2 * CPU Core,一个Channel 绑定到其中一个 Work 线程,一个 Work 线程中可以绑定多个 Channel。

在 Netty 中编码、解码等操作会被封装成一个一个事件处理器(ChannelHandler),那这些 Handler 是在IO线程池中执行?默认情况下ChannelHandler 是在 IO 线程中执行,那如何改变默认行为呢?其关键代码如下:

关键点:在将事件处理器添加到事件链时可以指定在哪个线程池中执行,如果不指定则为IO线程中执行。通常业务操作会专门开辟一个线程池,那业务处理完成之后,如何将响应结果通过 IO 线程写入到网卡中呢?

业务线程调用 Channel 对象的 write方法并不会立即写入网络,只是将数据放入一个待写入队列(缓存区),然后IO线程每次执行事件选择后,会从待写入缓存区中获取写入任务,将数据真正写入到网络中,数据到达网卡之前会经过一系列的Channel Handler(Netty事件传播机制),最终写入网卡。

  1. 最后再来介绍一下 Netty 中 IO 线程的大体工作流程。

IO线程处理的关键点:

  1. 每一IO线程在执行上述操作时是串行执行的,即注册在一个Selector(事件选择器)中的所有通道,同一时间只有一个通道的事件被处理。这也是为什么NIO应对大文件传输时不具备优势的根本原因。
  2. IO线程在处理完所有就绪事件后,还会从任务队列(Task Queue)获取任务,例如上文中提到的业务线程在执行完业务后需要将返回结果写入网络,Netty 中所有的网络读写操作只能在IO线程中真正获得运行,故业务线程需要将带写入的响应结果封装成 Task,放入到 IO 线程任务队列中。
四、总结
  1. Netty 的线程模型基于主从多Reactor模型。通常由一个appcetor线程负责处理OP_ACCEPT事件,拥有 CPU核数的两倍的IO线程处理读写事件。
  2. 一个通道的IO操作会绑定在一个IO线程中,而一个IO线程可以注册多个通道。
  3. 在一个网络通信中通常会包含网络数据读写,编码、解码、业务处理。默认情况下编码、解码等操作会在IO线程中运行,但也可以指定其他线程池。
  4. 通常业务处理会单独开启业务线程池,但也可以进一步细化,例如心跳包可以直接在IO线程中处理,而需要再转发给业务线程池,避免线程切换。
  5. 在一个IO线程中所有通道的事件是串行处理的。
  • 流程总结:
  • 当服务器程序启动时,会配置ChannelPipeline,ChannelPipeline中是一个ChannelHandler链,所有的事件发生时都会触发Channelhandler中的某个方法,这个事件会在ChannelPipeline中的ChannelHandler链里传播。然后,从bossGroup事件循环池中获取一个NioEventLoop来现实服务端程序绑定本地端口的操作,将对应的ServerSocketChannel注册到该NioEventLoop中的Selector上,并注册ACCEPT事件为ServerSocketChannel所感兴趣的事件。
  • NioEventLoop事件循环启动,此时开始监听客户端的连接请求。
  • 当有客户端向服务器端发起连接请求时,NioEventLoop的事件循环监听到该ACCEPT事件,Netty底层会接收这个连接,通过accept()方法得到与这个客户端的连接(SocketChannel),然后触发ChannelRead事件(即,ChannelHandler中的channelRead方法会得到回调),该事件会在ChannelPipeline中的ChannelHandler链中执行、传播。
  • ServerBootstrapAcceptor的readChannel方法会该SocketChannel(客户端的连接)注册到workerGroup(NioEventLoopGroup) 中的某个NioEventLoop的Selector上,并注册READ事件为SocketChannel所感兴趣的事件。启动SocketChannel所在NioEventLoop的事件循环,接下来就可以开始客户端和服务器端的通信了。

注意:ServerBootstrapAcceptor是ServerBootstrap的一个静态内部类

参考文章
参考文章
参考文章
参考文章
参考文章
参考文章
参考文章

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/276219.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号