9.线程模型
1.Reactor单线程模型
- Reactor单线程模型是指所有的IO操作都在同一个NIO线程上面完成。NIO线程的职责如下:
(1)作为NIO服务端,接收客户端的TCP连接
(2)作为NIO客户端,向服务端发起TCP连接
(3)读取通信对端的请求或者应答消息
(4)向通信对端发送消息请求或者应答消息 - 由于Reactor模式使用的是异步非阻塞IO,所有的IO操作都不会导致阻塞,理论上一个线程可以独立处理所有IO相关的操作。
- 在一些小容量应用场景下,可以使用单线程模型。但是对于高负载、大并发的应用却不合适:
(1)一个NIO线程同时处理成百上千的链路,性能上无法支撑。
(2)当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时成为系统的性能瓶颈
(3)可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用。造成节点故障
2.Reactor多线程模型
- Reactor多线程模型的特点如下:
(1)有一个专门的NIO线程,Acceptor线程用于监听服务端,接收客户端的TCP连接请求
(2)网络IO操作——读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些线程负责消息的读取、解码、编码和发送。
(3)一个NIO线程可以同时处理N条链路,但是一个链路只对应一个NIO线程,防止发生并发操作问题 - 在绝大多数场景下,Reactor多线程模型可以满足性能需求。在并发百万客户端连接或服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能,在这类场景下,单独一个Acceptor线程可能会存在性能不足的问题。为了解决性能问题,产生了主从Reactor多线程模型。
3.主从Reactor多线程模型
- 主从Reactor多线程模型的特点是:服务端用于接收用户端连接的不再是一个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端TCP连接请求并处理完成后,将新创建的SocketChannel注册到IO线程池的某个IO线程上,由它负责SocketChannel的读写和编解码工作。Acceptor线程池仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的IO线程上,由IO线程负责后续的IO操作。
- 利用主从NIO线程模型,可以解决一个服务端监听线程无法有效处理所有客户端连接的性能不足问题,
4.Netty的线程模型
- Netty的线程模型并不是一成不变的,它实际取决于用户的启动参数配置。
- 服务器启动的时候,创建了2个NioEventLoopGroup,它们实际是2个独立的Reactor线程池,一个用于接收客户端的TCP连接,另一个用于处理IO相关的读写操作,或者执行系统Task、定时任务Task
- Netty用于接收客户端请求的线程池职责如下:
(1)接收客户端TCP连接,初始化Channel参数
(2)将链路状态变更事件通知给ChannelPipeline - Netty处理IO操作的Reactor线程池职责如下:
(1)异步读取通信对端的数据报,发送读事件到ChannelPipeline
(2)异步发送消息到通信对端,调用ChannelPipeline的消息发送接口
(3)执行系统调用Task
(4)执行定时任务Task,例如链路空闲状态监测定时任务 - 为了尽可能地提升性能,Netty在很多地方进行了无锁化的设计。例如在IO线程内部进行串行操作,编码多线程竞争导致的性能下降问题。这种局部无锁化的串行线程设计性能更优。
- Netty的NioEventLoop读取到消息之后,直接调用ChannelPipeline的ChannelRead。只要用户不主动切换线程,一直都是由NioEventLoop调用用户的Handler,期间不进行线程切换。
5.最佳实践
- 创建2个NioEventLoopGroup,用于逻辑隔离NIO Acceptor和NIO IO线程。
- 尽量不要在ChannelHandler中启动用户线程
- 解码要放在NIO线程调用的解码Handler中进行,不要切换到用户线程中完成消息的解码
- 如果业务逻辑操作非常简单,可以直接在NIO线程上完成业务逻辑编排,不需要切换到用户线程
- 如果业务逻辑处理复杂,不要在NIO线程上完成,建议将解码后的POJO消息封装成Task,派发到业务线程池中由业务线程执行,以保证NIO线程尽快被释放
- 推荐的线程数量计算公式有以下2种:
(1)线程数量=(线程总时间/瓶颈资源时间)×瓶颈资源的线程并行数
(2)QPS=1000/线程总时间×线程数