一、Reactor 模式
reactor
模式是一种事件驱动的应用层 I/O 处理模式,基于分而治之和事件驱动的思想,致力于构建一个高性能的可伸缩的 I/O 处理模式。维基百科对 Reactor pattern 的解释:
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
大致意思是说,reactor
设计模式是一种事件处理模式,用于同时有一个或多个请求发送到事件处理器(service handler),这个事件处理器会采用多路分离(demultiplexes )的方式,同步的将这些请求分发到请求处理器(request handlers)。
不难看出,上边介绍的 reactor
模式是一种抽象;从实现角度说,reactor
模式有许多变种,不同编程语言中的实现也有差异。就 java 而言,大师 Doug Lea 在其【Scalable IO in Java】中就讲述了几个reactor
模式的演进,如单线程版本
、多线程版
,阅读此文后,笔者对大师所讲reactor
模式演进的理解与网络中一些描述稍有差异。
在reactor
单线程版中,只有一个reactor
线程,线程中通过 select
(I/O 多路复用接口) 监听所有 I/O 事件,收到 I/O 事件后通过 dispatch
进行分发给 Handlers
处理,此版本容易实现,也容易理解,但性能不高。为了适配多处理器,充分利用多核并行处理的优势,实现高性能的网络服务,可以采用分治策略,关键环节采用多线程模式,于是就出现了reactor
多线程版本,而多线程的应用体现为worder
线程和reactor
线程,多线程应该被池化管理,这样才容易被调整和控制。线程池中的线程数会比客户端的数量少很多,实际数量可以根据程序本身是 CPU 密集型还是 I/O 密集型操作来进行合理的分配。
-
多个 worder 线程(池化管理)
- 属于网络 I/O 操作与业务处理的拆分,因为
reactors
监听到 I/O 事件后应该快速分发给handlers
来处理程序;但如果handler
中的非 I/O 操作慢了就会减慢reactor
中的 I/O 事件响应速度,所以把非 I/O 操作从reactors
的 I/O 线程转移到其他线程中,即由worker
线程来分担非 I/O 逻辑的操作处理。
- 属于网络 I/O 操作与业务处理的拆分,因为
-
多个 reactor 线程(池化管理)
- 属于网络建连操作与网络 I/O 读写操作的拆分,因为由一个
reactor
在一个线程中完成所有 I/O 操作也会遇到性能瓶颈,可采取拆分并增加reactor
策略,将 I/O 负载分配给多个reactor
(每个reactor
都有自己的线程、选择器和调度循环)以达到负载平衡。这看起来挺不错,但谁来执行分配以达到负载均衡呢?或许是因为这个问题,将reactor
拆分为两类角色,mainReactor
负责接收连接,之后采用一定的负载均衡策略将新连接分配给其他subReactor
来处理 I/O 读写,这样的拆分自然流畅。
- 属于网络建连操作与网络 I/O 读写操作的拆分,因为由一个
如此就演进出如上图中的主从reactor
多线程模型。请注意,结合【Scalable IO in Java】原文中的用词和描述看,上图中的mainReactor
和subReactor
可以有多个并做池化管理,所有也有一些文章中会看到如主ReactorGroup
、mainReactorGroup
、从ReactorGroup
、subReactorGroup
等这类名词用 Group 后缀来强调 Reactor 是池化管理。 或许是不好布局,也或许是为了凸显主从reactor
角色的协作关系,上图中都只展示了一个,另外服务端应用通常只暴露一个服务端口时,只需用一个 mainReactor
来监听端口上的连接事件并处理。
二、Netty 主从 reactor
多线程模型
Netty
中reactor
所对应的实现类是NioEventLoop
,其核心逻辑如下:
- 不同类型的 channel 向 Selector 注册所感兴趣的事件
- 扫描是否有感兴趣的事件发生
- 事件发生后做相应的处理
客户端和服务端分别会有不同类型的channel
,客户端创建SocketChannel
向服务端发起连接请求,服务端创建ServerSocketChannel
监听客户端连接,建连后创建SocketChannel
与客户端的SocketChannel
互相收发数据,这些channel
分工不同,向 Selector 注册所感兴趣的事件情况也不同:
客户端/服务端 | channel | OP_ACCEPT | OP_CONNECT | OP_WRITE | OP_READ |
---|---|---|---|---|---|
客户端 | SocketChannel | YES | |||
服务端 | ServerSocketChannel | YES | |||
服务端 | SocketChannel | YES | YES |
Netty
中 Nio 方式实现几种 reactor
模型如下:
mainReactor
对应 Netty
中配置的 bossGroup
线程组(下图中的主ReactorGroup
),主要负责接受客户端连接的建立。每 bind
一个端口就用掉一个bossGroup
中的线程。
subReactor
对应 Netty
中配置的 workerGroup
线程组(下图中的 reactorGroup
),bossGroup
线程组接受完客户端的连接后,将 channel
转交给 workerGroup
线程组,在 workerGroup
线程组内选择一个线程,执行 I/O 读写的处理,workerGroup
线程组默认是 2 * CPU 核数个线程。
主从 reactor
模式的核心流程:
-
如果只监听一个端口,那么只需一个主
reactor
干活儿,所以通常看到boosGroup
只配置一个线程。主reactor
运行在独立的线程中 ,该线程中只负责与客户端的连接请求 -
从
reactor
在服务器端可以不止一个, 通常运行多个从reactor
, 每个从reactor
也运行在一个独立的线程中 ,负责与客户端的读写操作 -
主
reactor
检测到客户端的链接后,创建NioSocketChannel
,按照一定的算法循环选取(负载均衡)一个从reactor
,并把刚创建的NioSocketChannel
注册到这个从reactor
中,这样建连和读写事件互不影响。 -
一个
reactor
中可被注册多个NioSocketChannel
,这个reactor
监听所有的被分配的NioSocketChannel
的读写事件 , 如果监听到客户端的数据发送事件 , 将对应的业务逻辑转发给NioSocketChannel
中的pipeline
里的handler
链进行处理 -
handler
最好只负责响应 I/O 事件,不处理具体的与客户端交互的业务逻辑 , 这样不会长时间阻塞 , 其read
方法读取客户端数据后 , 将消息数据交给业务线程池去处理相关业务逻辑 -
业务线程池完成相关业务逻辑的处理后,将结果返回,通过
NioSocketChannel
的的pipeline
里的handler
链将结果消息写回给客户端 -
当
buffer
不满足将结果消息写回给客户端时的条件时,注册写事件,等待可写时再写
三、Seata Server 端 的 reactor 模式应用
Seata Server 采用了 主从 reactor
多线程模型,对应这个模型的话是有四个线程池,其中自定义业务线程池是两个。
功能 | 线程池对象 | 备注 |
---|---|---|
接收客户端连接 | NettyServerBootstrap#eventLoopGroupBoss |
|
处理 IO 事件 | NettyServerBootstrap#eventLoopGroupWorker |
部分 RPC 消息在这里处理 |
处理客户端的 r |