四Netty组件类--6Netty的线程模型
四Netty组件类--6Netty的线程模型
18.1 Netty的线程模型
18.1.0 Netty中IO线程的工作流程
IO线程处理的关键点:
每一IO线程在执行上述操作时是串行执行的,即注册在一个 Selector(事件选择器)中的所有通道,同一时间只有一个通道的事件被处理。这也是为什么NIO应对大文件传输时不具备优势的根本原因。
IO 线程在处理完所有就绪事件后,还会从任务队列(Task Queue)获取任务,例如上文中提到的业务线程在执行完业务后需要将返回结果写入网络,Netty 中所有的网络读写操作只能在IO线程中真正获得运行,故业务线程需要将带写入的响应结果封装成 Task,放入到 IO 线程任务队列中。
18.1.1 Reactor线程模型
当讨论netty的线程模型时候,首先想到Reactor的线程模型。主要有单线程模型、多线程模型和主从线程模型。
1 Reactor 单线程模型
所有的IO操作都在同一个NIO线程完成。
NIO线程职责:
- Server端:接受client端TCP连接
- Client端:向server端发起TCP连接
- read:读取通信对端的请求、应答
- write:向通信对端写请求、应答
reactor模式,采用异步非阻塞IO,所有的IO操作不会导致阻塞,理论上一个reactor线程可以独立处理所有IO操作。
操作流程:
用reactor线程接收client端tcp连接请求,当链路建立,通过dispatcher将对应的byteBuffer派发到指定的handler上,进行消息解码,以及逻辑处理。然后将response编码后通过NIO线程发送给client。
缺点:
- 一个NIO需要负责链接、encode、decode、read和write等操作,性能无法支撑成百上千的链路;
- NIO线程负载过重后,处理速度变慢,导致大量client连接超时,会重发request,继而加重NIO负担,并成为系统瓶颈;
- 可靠性:一个NIO线程,如果线程跑飞、死锁,导致通信模块不可用。
2 Reactor 多线程模型
同多线程不同,是一组NIO线程处理IO操作。
特点:
- 专门一个NIO线程——acceptor线程用于监听server端口,接收client的tcp连接请求;
- 网络IO操作:read、write由一个ThreadPool负责(可由JDK标准线程池实现),负责消息read、decode、encode、write;
- 一个NIO可以处理多个链路channel,但是一个channel只能由一个NIO线程处理。
缺点:
绝大多数场景,可以满足性能要求,但是对于百万连接、server端对client连接要做安全认证等耗性能的操作,一个acceptor线程,可能出现性能瓶颈。从此,产生主从多线程模型。
3 Reactor主从多线程模型
负责建立链路连接的reactor不是一个单独的线程,而是线程池。分别由raectorPool和subReactorPool组成。
操作流程:
Acceptor所在的acceptorPool接受到tcp连接请求,并处理完毕(包括一系列的认证)后,将新创建的SocketChannel注册到IO线程池(subReactorPool)的某个IO线程的selector上,由其负责socketChannel的的读写、编解码工作。
18.1.2 Netty线程模型
1 netty的主从线程模型
通过netty服务端代码的配置和启动,了解netty的线程模型配置:
netty线程池模型,不是一成不变,通过参数设置,可以支持所有reactor线程模型。
server端启动时候,创建两个EventLoopGroup线程组,是两个独立的reactor线程池main和sub。两个线程池职责如下:
1 bossGroup(MainReactorPool)职责:
该线程组用于接收clientTCP请求,通常设置1个线程。
- 接收client的TCP连接,初始化channel参数;
- 将链路状态变更事件通知给channelPipeline(就是对active、connect,通知给pipieline,然后fireXXX,进行对应的链式处理);
2 workerGroup(SubReactorPool)职责:
该线程用于处理IO的read、write操作,默认创建2倍CPU核心数的线程。
- 异步read数据,发送read事件到channelPipeline;
- 异步write,调用channelPipeline的发送接口;
- 执行系统调用的task;
- 执行定时任务task
注意:
subReactorPool中有两个任务队列,taskQueue和schduledTaskQueue,分别用来处理包装的普通任务和定时、延迟任务。由NioEventLoop.execute和NioEventLoop.scheduled方法,将task放入任务队列。(NioEventLoop继承ScheduledExecutorService,因此是个Executor执行器)
2 netty中IO线程的任务
a:一个channel的IO操作会绑定在一个IO线程中,但是一个IO线程可以绑定多个channel;
b:一个业务通信中,主要操作包含网络数据读写、编解码、业务处理。默认情况下,编解码等操作会在IO操作中,也可以执行其他线程池
addLast中可以更改运行IO线程的线程池。
c:业务处理可以单独开辟线程池(如果业务处理没有数据库操作等阻塞时间长的操作,可以放在IO线程处理,避免线程切换;如果含有耗时操作,避免线程阻塞,可以开辟单独的业务端线程池处理业务任务)
问题:业务逻辑交给外部线程池,如何处理的?
业务线程调用channel.write不会立刻写入channel,而是将数据写入channel的待写入队列(缓冲区),然后IO线程执行完事件选择后,会再从待写入缓冲区中获取写入task(当selector所在线程,select没有就绪事件时,才会从TaskQueue队列,获取写入任务),将数据真正写入网络。数据到达网卡前,会经历一系列handler(netty的事件传播机制),最终写入网卡。
3 netty无锁化设计
由于可以多个channel注册到一个IO线程上处理,因此可以串行处理channel中的事件,达到无锁化设计,避免资源并发访问。
总结(netty线程模型):
a Netty 的线程模型基于主从多Reactor模型。通常由一个线程负责处理OP_ACCEPT事件,拥有 CPU 核数的两倍的IO线程处理读写事件;
b 一个通道channel的IO操作会绑定在一个IO线程中,而一个IO线程可以注册多个channel;
c 在一个网络通信中通常会包含网络数据读写,编码、解码、业务处理。默认情况下编码、解码等操作会在IO线程中运行,但也可以指定其他线程池。
d 通常业务处理会单独开启业务线程池,但也可以进一步细化,例如心跳包可以直接在IO线程中处理,而需要时再转发给业务线程池,避免线程切换。
e 在一个IO线程中所有通道channel的事件是串行处理的
18.1.3 Netty是异步非阻塞的网络模型
//todo