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

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

 

一 Reactor 單線程模型

Reactor單線程模型就是指全部的IO操做都在同一個NIO線程上面完成的,也就是IO處理線程是單線程的。NIO線程的職責是: 
(1)做爲NIO服務端,接收客戶端的TCP鏈接;react

(2)做爲NIO客戶端,向服務端發起TCP鏈接;bootstrap

(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的詳細圖解(畫的很是清晰)參看 http://www.javashuo.com/article/p-qarptrkw-bw.html

相關文章
相關標籤/搜索