Reactor模式与Netty线程模型
简介
当我们讨论 Netty 线程模型的时候,一般首先会想到的是经典的 Reactor 线程模型,尽管不同的 NIO 框架对于 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.
从上述文字中我们可以看出以下关键点 :
- 事件驱动(event handling)
- 可以处理一个或多个输入源(one or more inputs)
- 通过Service Handler同步的将输入事件(Event)采用多路复用分发给相应的Request Handler(多个)处理
单Reactor单线程模型
我们用 Netty API 手写一个服务端:
public static void main(String[] args) throws IOException {
// 初始化
NioServerSocketChannel channel = new NioServerSocketChannel();
NioEventLoopGroup bossAndWorkGroup = new NioEventLoopGroup(1);
bossAndWorkGroup.register(channel);
channel.bind(new InetSocketAddress(8081));
channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("已建立连接, 当前接待线程" + Thread.currentThread().getName());
NioSocketChannel socketChannel = (NioSocketChannel) msg;
handleAccept(bossAndWorkGroup, socketChannel);
}
});
System.in.read();
}
private static void handleAccept(NioEventLoopGroup workGroup, NioSocketChannel socketChannel) {
workGroup.register(socketChannel);
socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] data = new byte[buf.readableBytes()];
buf.readBytes(data);
System.out.println("线程" + Thread.currentThread().getName() + " 收到消息:" + new String(data));
}
});
}
另外,我们使用 telnet 127.0.0.1 8081
命令连接服务端
实验结果:
启动多个Telnet客户端,也都是交给 nioEventLoopGroup-2-1 这一个线程来完成。单线程既承担了 Acceptor 接受连接的工作,又承担了 Handler 的读写,编码解码,业务请求的任务。
虽然一个 NIO 线程确实可以完成其承担的职责,但是对于高负载、大并发的应用场景却不适合,主要原因如下:
- 一个 NIO 线程同时处理成百上千的链路,性能上无法支撑。海量请求的循坏读取、解码、编码和发送,其中必然有大量请求超时。
- NIO 线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了 NIO 线程的负载,最终导致大量消息积压和处理超时,成为系统瓶颈。
主要原因是导致 accept 队列消息积压,来不及处理,因此服务端拒绝了新的请求。可以看看这篇文章,浅谈 Java Socket 构造函数参数 backlog - 可靠性问题:客户端逻辑也在 NIO 线程中执行,如果出现异常,意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接接收和处理外部消息,造成结点故障。
单 Reactor 多线程模型
Reactor 多线程模型与单线程模型最大的区别就是有一组 NIO 线程来处理 I/O 操作。
第一处修改,创建 NIO 线程池对象:
// NioEventLoopGroup bossAndWorkGroup = new NioEventLoopGroup(1);
// bossAndWorkGroup.register(channel);
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workGroup = new NioEventLoopGroup(8);
bossGroup.register(channel);
第二处修改,IO操作交给 NIO 线程池:
// handleAccept(bossAndWorkGroup, socketChannel);
handleAccept(workGroup, socketChannel);
建立连接,都是交给 nioEventLoopGroup-2-1,但是读写分别分配给了 nioEventLoopGroup-3-1,nioEventLoopGroup-3-2,nioEventLoopGroup-3-3
因此来说,Reactor 多线程模型的特点如下:
- 有一个专门的 NIO 线程 ———— Acceptor 线程用于监听服务端,接收客户端的 TCP 连接请求。(本文中的 Acceptor 线程是 nioEventLoopGroup-2-1)
- 网络 I/O 操作————读、写等由一个 NIO 线程池负责,线程池可以采用标准的 JDK 线程池实现,它包含一个任务队列和 N 个可用的线程,由这些 NIO 线程负责消息的读取、解码、编码和发送。
- 一个 NIO 线程可以同时处理 N 条链路,但是一个链路只对应一个 NIO 线程,防止发生并发操作问题。
在绝大多数场景下,Reactor 多线程模型可以满足性能需求。但是,在个别特殊场景中,一个 NIO 线程负责监听和处理所有客户端连接可能会存在性能问题。
例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。在这类场景下,单独一个 Acceptor 线程可能存在性能不足的问题。
为了解决性能问题,产生了第三种 Reactor 线程模型————主从 Reactor 多线程模型。
主从 Reactor 多线程模型
主从 Reactor 线程模型的特点是:服务器端用于接收客户端连接的不再是一个单独的 NIO 线程,而是一个独立的 NIO 线程池。
Acceptor 线程(本文中 Reactor 主线程)接收到客户端 TCP 连接请求并完成后(可能包含接入认证等),将新创建的 SocketChannel 注册到线程池(sub reactor 线程池)的某个 I/O 线程上,由它负责 SocketChannel 的读写和编解码工作。
Acceptor 线程池仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端 subReactor 线程池的 I/O 线程上,由 I/O 线程负责后续的 I/O 操作。
这里可能要结合 SSL 协议才能更好的理解,因此这里就不展开了
Netty 的线程模型
NioEventLoop 因为需要事件驱动和实现多路复用,自然少不了 Selector 成员变量。
另外因为 channel.register()
和 Selector.select()
在不同线程并发时可能造成死锁,因此 NioEventLoop 设计了 taskQueue:Queue<Runnable>
,并且通过调用 runAllTasks()
实现同步执行任务,避免死锁。
参考
《Netty 权威指南2》