Netty 线程模型(Reactor 线程模型)
更多内容,前往个人博客
当说到 Netty 线程模型的时候,一般首先会想到经典的 Reactor 线程模型,尽管不同的 NIO 框架对于 Reactor 模式的实现存在差异,但本质上还是遵循了 Reactor 的基础线程模式。
一、Reactor 单线程模型
无论是C++ 还是 Java 编写的网络框架,大多数都是基于 Reactor 模型进行设计和开发,Reactor 模型基于事件驱动,特别适合海量的 I/O 事件。
【1】Reactor 单线程模型,是指所有的 I/O操作都是在同一个 NIO线程上面完成。NIO线程的职责如下(连接和消息应答):
■ 作为 NIO服务端,接受客户端的 TCP连接;
■ 作为 NIO客户端,向服务端发起 TCP连接;
■ 读取通信对端的请求和应答消息;
■ 向通信对端发送消息请求或者应答消息;
【2】Reactor 单线程模型如下图:
二、Reactor 多线程模型
【1】与单线程模型最大的区别就是有一组 NIO 线程来处理 I/O 操作,它的原理图如下:
三、主从 Reactor 多线程模型
【1】主从 Reactor 线程模型的特点是:服务端用于接收客户端连接的不再是一个单独的 NIO 线程,而是一个独立的 NIO 线程池。Acceptor 接受客户端 TCP 连接请求并处理完成后(可能包含接入认证等),将新创建的 SocketChannel 注册到 I/O 线程池(Sub reactor线程池)的某个 I/O 线程上,由它负责 SocketChannel 的读写和编解码工作。Acceptor 线程池不仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端 subReactor 线程池的 I/O 线程上,由 I/O 线程负责后续的I/O 操作。
【2】主从 Reactor 线程模型原理图:
四、Netty 的线程模型
Netty 的线程模型并不是一成不变的,它实际取决于用户的启动参数配置。通过设置不同的启动参数,Netty 可以同时支持 Reactor 单线程模型、多线程模型和主从 Reactor 多线程模型。
【1】Netty 的线程模型如下:
【2】可以通过如下 Netty 服务端启动代码来了解它的线程模型。
1 EventLoopGroup bossGroup = new NioEventLoopGroup(); 2 EventLoopGroup workerGroup = new NioEventLoopGroup(); 3 try { 4 ServerBootstrap bootstrap = new ServerBootstrap(); 5 bootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class) 6 .childHandler(new PersonChannelInitializer());
服务端启动时创建两个 NioEventLoopGroup,它们实际上是两个独立的 Reactor 线程池。一个用于接收客户端的 TCP 连接,另一个用于处理 I/O 相关的读写操作,或者执行系统 Task、定时任务 Task 等。
【3】Netty 用于接收客户端请求的线程池职责如下:
■ 接收客户端 TCP 连接,初始化 Channel 参数。
■ 将链路状态变更事件通知给 ChannelPipeline。
【4】Netty 处理 I/O 操作的 Reactor 线程池职责如下:
■ 异步读取通信对端的数据报,发送读事件到 ChannelPipeline;
■ 异步发送消息到通信对端,调用 ChannelPipeline 的消息发送接口;
■ 执行系统调用 Task;
■ 执行定时任务 Task,例如链路空闲状态监测定时任务。
通过调整线程池的线程个数,是否共享线程池等方式,Netty 的 Reactor 线程模型可以在单线程、多线程和主从多线程间切换,这种灵活的配置方式可以最大程度地满足不同用户的个性定制。
为了尽可能的提升性能,Netty 在很多地方进行了无锁化设计,例如在 I/O 线程内部进行串行操作,避免多线程竞争导致的性能下降问题。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列,多个工作线程的模型性能更优。设计原理如下图所示:
五、最佳实践
【1】Netty 的多线程编程最佳实践如下:
1)、创建两个 NioEventLoopGroup,用于逻辑隔离 NIO Acceptor 和 NIO I/O 线程。
2)、尽量不要在 ChannelHandler 中启动用户线程(解码后用于将 POJO 消息派发到后端业务线程除外)。
3)、解码要放在 NIO 线程调用的解码 Handler 中进行,不要切换到用户线程中完成消息的解码。
4)、如果业务逻辑操作非常简单、没有复杂的业务逻辑计算,没有可能会导致线程被阻塞的磁盘操作、数据库操作、网络操作等,可以直接在 NIO 线程上完成业务逻辑编排,不需要切换到用户线程。
5)、如果业务逻辑处理复杂,不要在 NIO 线程上完成,建议将编解码后的 POJO 消息封装成 Task 任务,派发到业务线程池中由业务线程执行,以保证尽快被释放,处理其他的 I/O 操作。
【2】推荐的线程数量计算公式有以下两种:
■ 线程数量 = (线程总时间/瓶颈资源时间)* 瓶颈资源的线程并行数。
■ QPS(每秒查询率) = 1000/线程总时间 * 线程数。
由于用户场景的不同,对于一些负责的系统,实际上很难计算出最优线程配置,只能是根据测试数据和用户场景,结合公式给出一个相对合理的范围,然后对范围内的数据进行性能测试,选择相对最优值。