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》

原文地址:https://www.cnblogs.com/kendoziyu/p/netty-reactor-design-pattern.html