1. Reactor 模型

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

1.1 Reactor多线程模型

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

 

Reactor 多线程模型

Reactor 多线程模型的特点:

1)有专门一个 NIO 线程 -Acceptor 线程用于监听服务端,接收客户端的 TCP 连接请求;

2)网络 IO 操作 - 读、写等由一个 NIO 线程池负责,线程池可以采用标准的 JDK 线程池实现,它包含一个任务队列和 N 个可用的线程,由这些 NIO 线程负责消息的读取、解码、编码和发送;

3)1 个 NIO 线程可以同时处理 N 条链路,但是 1 个链路只对应 1 个 NIO 线程,防止发生并发操作问题。

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

1.2 主从Reactor多线程模型

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

它的线程模型如下图所示:

 

主从 Reactor 多线程模型

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

它的工作流程总结如下:

1)      从主线程池中随机选择一个 Reactor 线程作为 Acceptor 线程,用于绑定监听端口,接收客户端连接;

2)      Acceptor 线程接收客户端连接请求之后创建新的 SocketChannel,将其注册到主线程池的其它 Reactor 线程上,由其负责接入认证、IP 黑白名单过滤、握手等操作;

3)      步骤 2 完成之后,业务层的链路正式建立,将 SocketChannel 从主线程池的 Reactor 线程的多路复用器上摘除,重新注册到 Sub 线程池的线程上,用于处理 I/O 的读写操作。

2. Netty 线程模型

2.1 Netty 线程模型分类

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

2.1.1 Netty服务端线程模型

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

 

Netty 服务端线程工作流程

 

服务端创建线程工作流程:

1)第一步,从用户线程发起创建服务端操作。

通常情况下,服务端的创建是在用户进程启动的时候进行,因此一般由 Main 函数或者启动类负责创建,服务端的创建由业务线程负责完成。在创建服务端的时候实例化了 2 个 EventLoopGroup,1 个 EventLoopGroup 实际就是一个 EventLoop 线程组,负责管理 EventLoop 的申请和释放。

EventLoopGroup 管理的线程数可以通过构造函数设置。默认情况下,Netty会创建“2*CPU 核数”个 EventLoop

bossGroup 线程组实际就是 Acceptor 线程池,负责处理客户端的 TCP 连接请求,如果系统只有一个服务端端口需要监听,则建议 bossGroup 线程组线程数设置为 1。

workerGroup 是真正负责 I/O 读写操作的线程组,通过 ServerBootstrap 的 group 方法进行设置,用于后续的 Channel 绑定。

2)第二步,Acceptor 线程绑定监听端口,启动 NIO 服务端。

服务端 Channel 创建完成之后,将其注册到多路复用器 Selector 上,用于接收客户端的 TCP 连接。

3)第三步,如果监听到客户端连接,则创建客户端 SocketChannel 连接,重新注册到 workerGroup IO 线程上。

第四步,选择 IO 线程之后,将 SocketChannel 注册到多路复用器上,监听 READ 操作。

4)第五步,处理网络的 I/O 读写事件。

2.1.2 Netty客户端线程模型

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

 

Netty 客户端线程模型

 

Netty客户端创建,线程模型如下:

  1. 由用户线程负责初始化客户端资源,发起连接操作;
  2. 如果连接成功,将 SocketChannel 注册到 IO 线程组的 NioEventLoop 线程中,监听读操作位;
  3. 如果没有立即连接成功,将 SocketChannel 注册到 IO 线程组的 NioEventLoop 线程中,监听连接操作位;
  4. 连接成功之后,修改监听位为 READ,但是不需要切换线程。

 

2.2 Reactor 线程 NioEventLoop

Netty 的实现虽然参考了 Reactor 模式,但是并没有完全照搬,Netty 中最核心的概念是事件循环(EventLoop),其实也就是 Reactor 模式中的 Reactor,负责监听网络事件并调用事件处理器进行处理。在 4.x 版本的 Netty 中,网络连接和 EventLoop 是稳定的多对 1 关系,而 EventLoop 和 Java 线程是 1 对 1 关系,这里的稳定指的是关系一旦确定就不再发生变化。也就是说一个网络连接只会对应唯一的一个 EventLoop,而一个 EventLoop 也只会对应到一个 Java 线程,所以一个网络连接只会对应到一个 Java 线程。一个网络连接对应到一个 Java 线程上,有什么好处呢?最大的好处就是对于一个网络连接的事件处理是单线程的,这样就避免了各种并发问题。一个 NioEventLoop 聚合了一个多路复用器 Selector,因此可以处理成百上千的客户端连接。

Netty 中的线程模型可以参考下图,这个图和前面我们提到的理想的线程模型图非常相似,核心目标都是用一个线程处理多个网络连接。

 

Netty 中的线程模型

 

Netty 中还有一个核心概念是 EventLoopGroup,顾名思义,一个 EventLoopGroup 由一组 EventLoop 组成。实际使用中,一般都会创建两个 EventLoopGroup,一个称为 bossGroup,一个称为 workerGroup。为什么会有两个 EventLoopGroup 呢?这个和 socket 处理网络请求的机制有关,socket 处理 TCP 网络连接请求,是在一个独立的 socket 中,每当有一个 TCP 连接成功建立,都会创建一个新的 socket,之后对 TCP 连接的读写都是由新创建处理的 socket 完成的。也就是说处理 TCP 连接请求和读写请求是通过两个不同的 socket 完成的。上面我们在讨论网络请求的时候,为了简化模型,只是讨论了读写请求,而没有讨论连接请求。在 Netty 中,bossGroup 就用来处理连接请求的,而 workerGroup 是用来处理读写请求的。bossGroup 处理完连接请求后,会将这个连接提交给 workerGroup 来处理, workerGroup 里面有多个 EventLoop,那新的连接会交给哪个 EventLoop 来处理呢?这就需要一个负载均衡算法,Netty 中目前使用的是轮询算法。

Netty 是一个款优秀的网络编程框架,性能非常好,为了实现高性能的目标,Netty 做了很多优化,例如优化了 ByteBuffer、支持零拷贝等等,和并发编程相关的就是它的线程模型了。Netty 的线程模型设计得很精巧,每个网络连接都关联到了一个线程上,这样做的好处是:对于一个网络连接,读写操作都是单线程执行的,从而避免了并发程序的各种问题。

 

 

参考文章:

 

https://www.infoq.cn/article/netty-threading-model

https://time.geekbang.org/column/article/97622?cid=100023901