填坑Reactor模型和Netty线程模型
上面这篇幅文章我们讨论了IO相关的问题,文末留了个坑说要说下Netty的线程模型,今天来填坑。
在高性能的I/O设计中,有两个著名的模型:Reactor模型和Proactor模型,其中Reactor模型用于同步I/O,而Proactor模型运用于异步I/O操作。实际上Netty线程模型就是Reactor模型的一个实现。
Reactor模型
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.
以上来自wiki,我们可以看到以下重点。
事件驱动(event handling)
可以处理一个或多个输入源(one or more inputs)
通过Service Handler同步的将输入事件(Event)采用多路复用分发给相应的Request Handler(多个)处理
根据大神Doug Lea 在 《Scalable IO in Java 》中的介绍,Reacotr模型主要分为三个角色:
Reactor:把IO事件分配给对应的handler处理
Acceptor:处理客户端连接事件
Handler:处理非阻塞的任务
Reactor处理请求的流程:
同步的等待多个事件源到达(采用select()实现)
将事件多路分解以及分配相应的事件服务进行处理,这个分派采用server集中处理(dispatch)
分解的事件以及对应的事件服务应用从分派服务中分离出去(handler)
为什么使用Reactor?
传统阻塞IO模型的不足
每个连接都需要独立线程处理,当并发数大时,创建线程数多,占用资源
采用阻塞IO模型,连接建立后,若当前线程没有数据可读,线程会阻塞在读操作上,造成资源浪费
针对传统阻塞IO模型的两个问题,可以采用如下的方案
基于池化思想,避免为每个连接创建线程,连接完成后将业务处理交给线程池处理
基于IO复用模型,多个连接共用同一个阻塞对象,不用等待所有的连接。遍历到有新数据可以处理时,操作系统会通知程序,线程跳出阻塞状态,进行业务逻辑处理
Reactor线程模型分类
根据Reactor的数量和处理资源的线程数量的不同,分为三类:
单Reactor单线程模型
单Reactor多线程模型
多Reactor多线程模型
单Reactor单线程模型
消息处理流程:
Reactor对象通过select监控连接事件,收到事件后通过dispatch进行转发。
如果是连接建立的事件,则由acceptor接受连接,并创建handler处理后续事件。
如果不是建立连接事件,则Reactor会分发调用Handler来响应。
handler会完成read->业务处理->send的完整业务流程。
该线程模型的不足
仅用一个线程处理请求,对于多核资源机器来说是有点浪费的
当处理读写任务的线程负载过高后,处理速度下降,事件会堆积,严重的会超时,可能导致客户端重新发送请求,性能越来越差
单线程也会有可靠性的问题
针对上面的种种不足,就有了下面的线程模型
单Reactor多线程模型
消息处理流程:
Reactor对象通过Select监控客户端请求事件,收到事件后通过dispatch进行分发。
如果是建立连接请求事件,则由acceptor通过accept处理连接请求,然后创建一个Handler对象处理连接完成后续的各种事件。
如果不是建立连接事件,则Reactor会分发调用连接对应的Handler来响应。
Handler只负责响应事件,不做具体业务处理,通过Read读取数据后,会分发给后面的Worker线程池进行业务处理。
Worker线程池会分配独立的线程完成真正的业务处理,如何将响应结果发给Handler进行处理。
Handler收到响应结果后通过send将响应结果返回给Client。
相对于第一种模型来说,在处理业务逻辑,也就是获取到IO的读写事件之后,交由线程池来处理,handler收到响应后通过send将响应结果返回给客户端。这样可以降低Reactor的性能开销,从而更专注的做事件分发工作了,提升整个应用的吞吐。
但是这个模型存在的问题:
多线程数据共享和访问比较复杂。如果子线程完成业务处理后,把结果传递给主线程Reactor进行发送,就会涉及共享数据的互斥和保护机制。
Reactor承担所有事件的监听和响应,只在主线程中运行,可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。
为了解决性能问题,产生了第三种主从Reactor多线程模型。
主从Reactor多线程模型
比起第二种模型,它是将Reactor分成两部分:
mainReactor负责监听server socket,用来处理网络IO连接建立操作,将建立的socketChannel指定注册给subReactor。
subReactor主要做和建立起来的socket做数据交互和事件业务处理操作。通常,subReactor个数上可与CPU个数等同。
Nginx、Memcached和Netty都是采用这种实现。
消息处理流程:
从主线程池中随机选择一个Reactor线程作为acceptor线程,用于绑定监听端口,接收客户端连接
acceptor线程接收客户端连接请求之后创建新的SocketChannel,将其注册到主线程池的其它Reactor线程上,由其负责接入认证、IP黑白名单过滤、握手等操作
步骤2完成之后,业务层的链路正式建立,将SocketChannel从主线程池的Reactor线程的多路复用器上摘除,重新注册到Sub线程池的线程上,并创建一个Handler用于处理各种连接事件
当有新的事件发生时,SubReactor会调用连接对应的Handler进行响应
Handler通过Read读取数据后,会分发给后面的Worker线程池进行业务处理
Worker线程池会分配独立的线程完成真正的业务处理,如何将响应结果发给Handler进行处理
Handler收到响应结果后通过Send将响应结果返回给Client
Reactor三种模式形象比喻
餐厅一般有接待员和服务员,接待员负责在门口接待顾客,服务员负责全程服务顾客
Reactor的三种线程模型可以用接待员和服务员类比
单Reactor单线程模型:接待员和服务员是同一个人,一直为顾客服务。客流量较少适合
单Reactor多线程模型:一个接待员,多个服务员。客流量大,一个人忙不过来,由专门的接待员在门口接待顾客,然后安排好桌子后,由一个服务员一直服务,一般每个服务员负责一片中的几张桌子
多Reactor多线程模型:多个接待员,多个服务员。这种就是客流量太大了,一个接待员忙不过来了
Netty线程模型
上文说Netty就是采用Reactor模型实现的。下面是Netty使用中很常见的一段代码
public class Server {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.TCP_NODELAY, true)
.childAttr(AttributeKey.newInstance("childAttr"), "childAttrValue")
.handler(new ServerHandler())
.childHandler(new ChannelInitializer<SocketChannel>() {
public void initChannel(SocketChannel ch) {
}
});
ChannelFuture f = b.bind(8888).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
boss线程池作用:
接收客户端的连接,初始化Channel参数。
将链路状态变更时间通知给ChannelPipeline。
worker线程池作用:
异步读取通信对端的数据报,发送读事件到ChannelPipeline。
异步发送消息到通信对端,调用ChannelPipeline的消息发送接口。
执行系统调用Task。
执行定时任务Task。
通过配置boss和worker线程池的线程个数以及是否共享线程池等方式,Netty的线程模型可以在以上三种Reactor模型之间进行切换。
netty通过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss线程池和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件
tomcat的线程模型
Tomcat支持四种接收请求的处理方式:BIO、NIO、APR和AIO
NIO
同步非阻塞,比传统BIO能更好的支持大并发,tomcat 8.0 后默认采用该模型。
使用方法(配置server.xml):<Connector port="8080" protocol="HTTP/1.1"/> 改为 protocol="org.apache.coyote.http11.Http11NioProtocol"BIO
阻塞式IO,tomcat7之前默认,采用传统的java IO进行操作,该模型下每个请求都会创建一个线程,适用于并发量小的场景。
使用方法(配置server.xml):protocol =" org.apache.coyote.http11.Http11Protocol"APR
tomcat 以JNI形式调用http服务器的核心动态链接库来处理文件读取或网络传输操作,需要编译安装APR库。
使用方法(配置server.xml):protocol ="org.apache.coyote.http11.Http11AprProtocol"AIO
异步非阻塞 (NIO2),tomcat8.0后支持。多用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
使用方法(配置server.xml):protocol ="org.apache.coyote.http11.Http11Nio2Protocol"
参考:
https://zhuanlan.zhihu.com/p/69341619
https://cloud.tencent.com/developer/article/1488120