1. 程式人生 > 實用技巧 >Reactor模式與Netty執行緒模型

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》