1. 程式人生 > 其它 >Netty執行緒模型

Netty執行緒模型

1. Reactor 模型

無論是 C++ 還是 Java 編寫的網路框架,大多數都是基於 Reactor 模式進行設計和開發,Reactor 模式基於事件驅動,特別適合處理海量的 I/O 事件。

1.1 Reactor多執行緒模型

Reactor多執行緒模型與單執行緒模型最大的區別就是有一組 NIO 執行緒處理 IO 操作,它的原理圖如下:

Reactor 多執行緒模型

Reactor 多執行緒模型的特點:

1)有專門一個 NIO 執行緒 -Acceptor 執行緒用於監聽服務端,接收客戶端的 TCP 連線請求;

2)網路 IO 操作 - 讀、寫等由一個 NIO 執行緒池負責,執行緒池可以採用標準的 JDK 執行緒池實現,它包含一個任務佇列和 N 個可用的執行緒,由這些 NIO 執行緒負責訊息的讀取、解碼、編碼和傳送;

3)1 個 NIO 執行緒可以同時處理 N 條鏈路,但是 1 個鏈路只對應 1 個 NIO 執行緒,防止發生併發操作問題。

在絕大多數場景下,Reactor 多執行緒模型都可以滿足效能需求;但是,在極個別特殊場景中,一個 NIO 執行緒負責監聽和處理所有的客戶端連線可能會存在效能問題。例如併發百萬客戶端連線,或者服務端需要對客戶端握手進行安全認證,但是認證本身非常損耗效能。在這類場景下,單獨一個 Acceptor 執行緒可能會存在效能不足問題,為了解決效能問題,產生了第三種 Reactor 執行緒模型 - 主從 Reactor 多執行緒模型。

1.2 主從Reactor多執行緒模型

主從Reactor多執行緒模型的特點是:服務端用於接收客戶端連線的不再是個 1 個單獨的 NIO 執行緒,而是一個獨立的 NIO 執行緒池。Acceptor 接收到客戶端 TCP 連線請求處理完成後(可能包含接入認證等),將新建立的 SocketChannel 註冊到 IO 執行緒池(sub reactor 執行緒池)的某個 IO 執行緒上,由它負責 SocketChannel 的讀寫和編解碼工作。Acceptor 執行緒池僅僅只用於客戶端的登陸、握手和安全認證,一旦鏈路建立成功,就將鏈路註冊到後端 subReactor 執行緒池的 IO 執行緒上,由 IO 執行緒負責後續的 IO 操作。

它的執行緒模型如下圖所示:

主從 Reactor 多執行緒模型

利用主從 NIO 執行緒模型,可以解決 1 個服務端監聽執行緒無法有效處理所有客戶端連線的效能不足問題。

它的工作流程總結如下:

1)      從主執行緒池中隨機選擇一個 Reactor 執行緒作為 Acceptor 執行緒,用於繫結監聽埠,接收客戶端連線;

2)      Acceptor 執行緒接收客戶端連線請求之後建立新的 SocketChannel,將其註冊到主執行緒池的其它 Reactor 執行緒上,由其負責接入認證、IP 黑白名單過濾、握手等操作;

3)      步驟 2 完成之後,業務層的鏈路正式建立,將 SocketChannel 從主執行緒池的 Reactor 執行緒的多路複用器上摘除,重新註冊到 Sub 執行緒池的執行緒上,用於處理 I/O 的讀寫操作。

2. Netty 執行緒模型

2.1 Netty 執行緒模型分類

事實上,Netty 的執行緒模型與上面介紹的 Reactor 執行緒模型相似,下面我們通過 Netty 服務端和客戶端的執行緒處理流程圖來介紹 Netty 的執行緒模型。

2.1.1 Netty服務端執行緒模型

一種比較流行的做法是服務端監聽執行緒和 IO 執行緒分離,類似於 Reactor 的多執行緒模型,它的工作原理圖如下:

 

Netty 服務端執行緒工作流程

 

服務端建立執行緒工作流程:

1)第一步,從使用者執行緒發起建立服務端操作。

通常情況下,服務端的建立是在使用者程序啟動的時候進行,因此一般由 Main 函式或者啟動類負責建立,服務端的建立由業務執行緒負責完成。在建立服務端的時候例項化了 2 個 EventLoopGroup,1 個 EventLoopGroup 實際就是一個 EventLoop 執行緒組,負責管理 EventLoop 的申請和釋放。

EventLoopGroup 管理的執行緒數可以通過建構函式設定。預設情況下,Netty會建立“2*CPU 核數”個 EventLoop

bossGroup 執行緒組實際就是 Acceptor 執行緒池,負責處理客戶端的 TCP 連線請求,如果系統只有一個服務端埠需要監聽,則建議 bossGroup 執行緒組執行緒數設定為 1。

workerGroup 是真正負責 I/O 讀寫操作的執行緒組,通過 ServerBootstrap 的 group 方法進行設定,用於後續的 Channel 繫結。

2)第二步,Acceptor 執行緒繫結監聽埠,啟動 NIO 服務端。

服務端 Channel 建立完成之後,將其註冊到多路複用器 Selector 上,用於接收客戶端的 TCP 連線。

3)第三步,如果監聽到客戶端連線,則建立客戶端 SocketChannel 連線,重新註冊到 workerGroup IO 執行緒上。

第四步,選擇 IO 執行緒之後,將 SocketChannel 註冊到多路複用器上,監聽 READ 操作。

4)第五步,處理網路的 I/O 讀寫事件。

2.1.2 Netty客戶端執行緒模型

相比於服務端,客戶端的執行緒模型簡單一些,它的工作原理如下:

 

Netty 客戶端執行緒模型

Netty客戶端建立,執行緒模型如下:

  1. 由使用者執行緒負責初始化客戶端資源,發起連線操作;
  2. 如果連線成功,將 SocketChannel 註冊到 IO 執行緒組的 NioEventLoop 執行緒中,監聽讀操作位;
  3. 如果沒有立即連線成功,將 SocketChannel 註冊到 IO 執行緒組的 NioEventLoop 執行緒中,監聽連線操作位;
  4. 連線成功之後,修改監聽位為 READ,但是不需要切換執行緒。

2.2 Reactor 執行緒 NioEventLoop

Netty 的實現雖然參考了 Reactor 模式,但是並沒有完全照搬,Netty 中最核心的概念是事件迴圈(EventLoop),其實也就是 Reactor 模式中的 Reactor,負責監聽網路事件並呼叫事件處理器進行處理。在 4.x 版本的 Netty 中,網路連線和 EventLoop 是穩定的多對 1 關係,而 EventLoop 和 Java 執行緒是 1 對 1 關係,這裡的穩定指的是關係一旦確定就不再發生變化。也就是說一個網路連線只會對應唯一的一個 EventLoop,而一個 EventLoop 也只會對應到一個 Java 執行緒,所以一個網路連線只會對應到一個 Java 執行緒。一個網路連線對應到一個 Java 執行緒上,有什麼好處呢?最大的好處就是對於一個網路連線的事件處理是單執行緒的,這樣就避免了各種併發問題。一個 NioEventLoop 聚合了一個多路複用器 Selector,因此可以處理成百上千的客戶端連線。

Netty 中的執行緒模型可以參考下圖,這個圖和前面我們提到的理想的執行緒模型圖非常相似,核心目標都是用一個執行緒處理多個網路連線。

Netty 中的執行緒模型

Netty 中還有一個核心概念是 EventLoopGroup,顧名思義,一個 EventLoopGroup 由一組 EventLoop 組成。實際使用中,一般都會建立兩個 EventLoopGroup,一個稱為 bossGroup,一個稱為 workerGroup。為什麼會有兩個 EventLoopGroup 呢?這個和 socket 處理網路請求的機制有關,socket 處理 TCP 網路連線請求,是在一個獨立的 socket 中,每當有一個 TCP 連線成功建立,都會建立一個新的 socket,之後對 TCP 連線的讀寫都是由新建立處理的 socket 完成的。也就是說處理 TCP 連線請求和讀寫請求是通過兩個不同的 socket 完成的。上面我們在討論網路請求的時候,為了簡化模型,只是討論了讀寫請求,而沒有討論連線請求。在 Netty 中,bossGroup 就用來處理連線請求的,而 workerGroup 是用來處理讀寫請求的。bossGroup 處理完連線請求後,會將這個連線提交給 workerGroup 來處理, workerGroup 裡面有多個 EventLoop,那新的連線會交給哪個 EventLoop 來處理呢?這就需要一個負載均衡演算法,Netty 中目前使用的是輪詢演算法。

Netty 是一個款優秀的網路程式設計框架,效能非常好,為了實現高效能的目標,Netty 做了很多優化,例如優化了 ByteBuffer、支援零拷貝等等,和併發程式設計相關的就是它的執行緒模型了。Netty 的執行緒模型設計得很精巧,每個網路連線都關聯到了一個執行緒上,這樣做的好處是:對於一個網路連線,讀寫操作都是單執行緒執行的,從而避免了併發程式的各種問題。

參考文章:

https://www.infoq.cn/article/netty-threading-model

https://time.geekbang.org/column/article/97622?cid=100023901