Reactor 線程模型以及在netty中的應用


這里我們需要理解的一點是Reactor線程模型是基於同步非阻塞IO實現的。對於異步非阻塞IO的實現是Proactor模型

 

一 Reactor 單線程模型

Reactor單線程模型就是指所有的IO操作都在同一個NIO線程上面完成的,也就是IO處理線程是單線程的。NIO線程的職責是: 
(1)作為NIO服務端,接收客戶端的TCP連接;

(2)作為NIO客戶端,向服務端發起TCP連接;

(3)讀取通信對端的請求或者應答消息;

(4)向通信對端發送消息請求或者應答消息。

Reactor單線程模型圖如下所示:

Reactor模式使用的是同步非阻塞IO(NIO),所有的IO操作都不會導致阻塞,理論上一個線程可以獨立的處理所有的IO操作(selector會主動去輪詢哪些IO操作就緒)。從架構層次看,一個NIO線程確實可以完成其承擔的職責,比如上圖的Acceptor類接收客戶端的TCP請求消息,當鏈路建立成功之后,通過Dispatch將對應的ByteBuffer轉發到指定的handler上,進行消息的處理。

對於一些小容量的應用場景下,可以使用單線程模型,但是對於高負載、大並發的應用場景卻不適合,主要原因如下: 
(1)一個NIO線程處理成千上萬的鏈路,性能無法支撐,即使CPU的負荷達到100%;

(2)當NIO線程負載過重,處理性能就會變慢,導致大量客戶端連接超時然后重發請求,導致更多堆積未處理的請求,成為性能瓶頸。

(3)可靠性低,只有一個NIO線程,萬一線程假死或則進入死循環,就完全不可用了,這是不能接受的。

 

 

二 Reactor 多線程模型

Reactor多線程模型與單線程模型最大的區別在於,IO處理線程不再是一個線程,而是一組NIO處理線程。原理如下圖所示:

 

Reactor多線程模型的特點如下: 
(1)有一個專門的NIO線程—-Acceptor線程用於監聽服務端,接收客戶端的TCP連接請求。

(2)網絡IO操作—-讀寫等操作由一個專門的線程池負責,線程池可以使用JDK標准的線程池實現,包含一個任務隊列和N個可用的線程,這些NIO線程就負責讀取、解碼、編碼、發送。

(3)一個NIO線程可以同時處理N個鏈路,但是一個鏈路只對應一個NIO線程。

Reactor多線程模型可以滿足絕大多數的場景,除了一些個別的特殊場景:比如一個NIO線程負責處理客戶所有的連接請求,但是如果連接請求中包含認證的需求(安全認證),在百萬級別的場景下,就存在性能問題了,因為認證本身就要消耗CPU,為了解決這種情景下的性能問題,產生了第三種線程模型:Reactor主從線程模型。

 

 

三 主從Reactor 多線程模型

主從Reactor線程模型的特點是:服務端用於接收客戶端連接的不再是一個單獨的NIO線程,而是一個獨立的NIO的線程池。Acceptor接收到客戶端TCP連接請求並處理完成后(可能包含接入認證),再將新創建的SocketChannel注冊到IO線程池(sub reactor)的某個IO處理線程上並處理編解碼和讀寫工作。Acceptor線程池僅負責客戶端的連接與認證,一旦鏈路連接成功,就將鏈路注冊到后端的sub Reactor的IO線程池中。 線程模型圖如下:

利用主從Reactor模型可以解決服務端監聽線程無法有效處理所有客戶連接的性能不足問題,這也是netty推薦使用的線程模型。

 

 

四 netty的線程模型

netty的線程模型是可以通過設置啟動類的參數來配置的,設置不同的啟動參數,netty支持Reactor單線程模型、多線程模型和主從Reactor多線程模型。

 

服務端啟動時創建了兩個NioEventLoopGroup,一個是boss,一個是worker。實際上他們是兩個獨立的Reactor線程池,一個用於接收客戶端的TCP連接,另一個用於處理Io相關的讀寫操作,或者執行系統/定時任務的task。

boss線程池作用: 
(1)接收客戶端的連接,初始化Channel參數 
(2)將鏈路狀態變更時間通知給ChannelPipeline

worker線程池作用: 
(1)異步讀取通信對端的數據報,發送讀事件到ChannelPipeline 
(2)異步發送消息到通信對端,調用ChannelPipeline的消息發送接口 
(3)執行系統調用Task
(4)執行定時任務Task

通過配置boss和worker線程池的線程個數以及是否共享線程池等方式,netty的線程模型可以在單線程、多線程、主從線程之間切換。

為了提升性能,netty在很多地方都進行了無鎖設計。比如在IO線程內部進行串行操作,避免多線程競爭造成的性能問題。表面上似乎串行化設計似乎CPU利用率不高,但是通過調整NIO線程池的線程參數,可以同時啟動多個串行化的線程並行運行,這種局部無鎖串行線程設計性能更優。 

nettyd的NioEventLoop讀取到消息之后,直接調用ChannelPipeline的fireChannelRead(Object msg),只要用戶不主動切換線程,一直都是由NioEventLoop調用用戶的Handler,期間不進行線程切換,這種串行化設計避免了多線程操作導致的鎖競爭,性能角度看是最優的。

 

 

 1 import io.netty.bootstrap.ServerBootstrap;
 2 import io.netty.channel.ChannelFuture;
 3 import io.netty.channel.ChannelInitializer;
 4 import io.netty.channel.ChannelOption;
 5 import io.netty.channel.EventLoopGroup;
 6 import io.netty.channel.nio.NioEventLoopGroup;
 7 import io.netty.channel.socket.SocketChannel;
 8 import io.netty.channel.socket.nio.NioServerSocketChannel;
 9 
10 /**
11  * Created by xxx on 2018/1/5 PM5:23.
12  */
13 public class Test {
14     public void bind(int port) throws Exception {
15         // 配置服務端的NIO線程組
16         EventLoopGroup bossGroup = new NioEventLoopGroup();
17         EventLoopGroup workerGroup = new NioEventLoopGroup();
18         try {
19             ServerBootstrap b = new ServerBootstrap();
20             b.group(bossGroup, workerGroup)
21                     .channel(NioServerSocketChannel.class)
22                     .option(ChannelOption.SO_BACKLOG, 1024)
23                     .childHandler(new ChildChannelHandler());
24             // 綁定port,同步等待成功
25             ChannelFuture f = b.bind(port).sync();
26             // 等待服務端監聽port關閉
27             f.channel().closeFuture().sync();
28         } finally {
29             // 優雅退出,釋放線程池資源
30             bossGroup.shutdownGracefully();
31             workerGroup.shutdownGracefully();
32         }
33     }
34 
35     private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
36         @Override
37         protected void initChannel(SocketChannel arg0) throws Exception {
38             arg0.pipeline().addLast(new TimeServerHandler());
39         }
40     }
41 }

 

 

netty 服務端的創建過程

 

Netty 屏蔽NIO通信的底層細節:

  1. 首先創建ServerBootstrap,他是Netty服務端的啟動輔助類

  2. 設置並綁定Reactor線程池。

    Netty的Reactor線程池是EventLoopGroup,它實際就是EventLoop線 程的數組。

    EventLoop的職責是處理全部注冊到本線程多路復用器Selector上的Channel

  3. 設置NioServerSocketChannel。Netty通過工廠類,利用反射創建NioServerSocketChannel對象

  4. 設置TCP參數

  5. 鏈路建立的時候創建並初始化ChannelPipeline.它本質就是一個負責處理網絡事件的職責鏈,負責管理和運行ChannelHandler。

    網絡事件以事件流的形式在ChannelPipeline中流轉,由ChannelPipeline依據ChannelHandler的運行策略調度ChannelHandler的運行

    1. 綁定並啟動監聽port
    2. 綁定port,並啟動。將會啟動NioEventLoop負責調度和運行Selector輪詢操作,選擇准備就緒的Channel集合。當輪詢到准備就緒的Channel之后,就由Reactor線程NioEventLoop運行ChannelPipeline的對應方法。終於調度並運行ChannelHandler。

 

NioEventLoop IO線程淺析

做為Netty的Reactor線程,由於要處理網絡IO讀寫,所以聚合一個多路復用器對象,它通過open獲取一個多路復用器。他的操作主要是在run方法的for循環中運行的。

  1. 做為bossGroup的線程 他須要綁定NioServerSocketChannel 來監聽client的connect請求,並處理連接和校驗。
  2. 作為workGroup線層組的線程。須要將連接就緒的SocketChannel綁定到線程中。所以一個client連接至相應一個線程,一個線程能夠綁定多個client連接。 

從調度層面看。也不存在在EventLoop線程中 再啟動其他類型的線程用於異步運行其他的任務。這樣就避免了多線程並發操作和鎖競爭,提升了I/O線程的處理和調度性能。

 

 

五 epoll bug

screenshot

Selector.select 沒有任務運行時,可能觸發JDK的epoll BUG。這就是著名的JDK epoll BUG,JDK1.7早期版本號得到解決。

server直接表現為IO線程的CPU非常高,可能達到100%,可能會導致節點故障!

 

為什么會發生epoll Bug

screenshot

Netty的修復策略為:

  1. 對Selector的select的操作周期進行統計

  2. 完成一次空的select操作進行一次計數

  3. 在某周期內(如100ms)連續N次空輪詢, 說明觸發了epoll死循環BUG

  4. 檢測到死循環后,重建selector的方式讓系統恢復正常

netty采用此策略,完美避免了此BUG的發生。

關於netty bug,更詳細的可以參看: http://blog.csdn.net/huoyunshen88/article/details/45672295

proactor和reactor的詳細圖解(畫的非常清晰)參看 https://www.cnblogs.com/TomSnail/p/6158249.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM